1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package usecase
4
5import (
6 "strings"
7 "testing"
8
9 "github.com/stretchr/testify/assert"
10
11 "bnema.dev/ermite/internal/ports"
12)
13
14func TestParseActionRef(t *testing.T) {
15 t.Parallel()
16 tests := []struct {
17 uses string
18 owner, repo, path, ref, fail string
19 }{
20 {uses: "actions/checkout@v4", owner: "actions", repo: "checkout", ref: "v4"},
21 {uses: "actions/cache/save@v4", owner: "actions", repo: "cache", path: "save", ref: "v4"},
22 {uses: "o/r/a/b/c@feature/x", owner: "o", repo: "r", path: "a/b/c", ref: "feature/x"},
23 {uses: "o\\r\\p@v1", owner: "o", repo: "r", path: "p", ref: "v1"}, // GitHub splits on both separators
24 {uses: "o//r@v1", owner: "o", repo: "r", ref: "v1"}, // and ignores empty segments
25 {uses: "noslash@v1", fail: "Expected format {org}/{repo}[/path]@ref. Actual 'noslash@v1'"},
26 {uses: "o/r", fail: "Expected format {org}/{repo}[/path]@ref. Actual 'o/r'"},
27 {uses: "o/r@", fail: "Expected format {org}/{repo}[/path]@ref. Actual 'o/r@'"},
28 {uses: "@v1", fail: "Expected format {org}/{repo}[/path]@ref. Actual '@v1'"},
29 {uses: "o/r@a@b", fail: "Expected format {org}/{repo}[/path]@ref. Actual 'o/r@a@b'"},
30 {uses: "o/r/../x@v1", fail: "Expected format {org}/{repo}[/path]@ref. Actual 'o/r/../x@v1'"},
31 {uses: "o/r/./x@v1", fail: "Expected format {org}/{repo}[/path]@ref. Actual 'o/r/./x@v1'"},
32 {uses: "a/...@v1", fail: "Expected format {org}/{repo}[/path]@ref. Actual 'a/...@v1'"},
33 {uses: "../..@" + strings.Repeat("a", 40), fail: "Expected format {org}/{repo}[/path]@ref. Actual '../..@" + strings.Repeat("a", 40) + "'"},
34 {uses: "a%2f/b@v1", fail: "Expected format {org}/{repo}[/path]@ref. Actual 'a%2f/b@v1'"},
35 {uses: "a/b?x@v1", fail: "Expected format {org}/{repo}[/path]@ref. Actual 'a/b?x@v1'"},
36 {uses: "a/b#x@v1", fail: "Expected format {org}/{repo}[/path]@ref. Actual 'a/b#x@v1'"},
37 {uses: "-a/_b.c@v1", owner: "-a", repo: "_b.c", ref: "v1"},
38 {uses: "", fail: "Expected format {org}/{repo}[/path]@ref. Actual ''"},
39 }
40 for _, tt := range tests {
41 t.Run(tt.uses, func(t *testing.T) {
42 t.Parallel()
43 a := parseActionRef(tt.uses)
44 assert.Equal(t, tt.fail, a.err)
45 if tt.fail == "" {
46 assert.Equal(t, []string{tt.owner, tt.repo, tt.path, tt.ref}, []string{a.owner, a.repo, a.path, a.ref})
47 }
48 })
49 }
50 assert.Equal(t, parseActionRef("Actions/Checkout@v4").key(), parseActionRef("actions/checkout@v4").key(), "names are case-insensitive")
51 assert.NotEqual(t, parseActionRef("a/b@V4").key(), parseActionRef("a/b@v4").key(), "refs are not")
52}
53
54func TestPickRemoteRef(t *testing.T) {
55 t.Parallel()
56 refs := []ports.RemoteRef{
57 {Name: "refs/heads/v1", ID: "b1"},
58 {Name: "refs/tags/v1", ID: "t1"},
59 {Name: "refs/tags/v2", ID: "tobj", Peeled: "c2"},
60 {Name: "refs/heads/main", ID: "m"},
61 }
62 assert.Equal(t, "t1", pickRemoteRef(refs, "v1"), "a tag before a branch")
63 assert.Equal(t, "c2", pickRemoteRef(refs, "v2"), "the peeled commit of an annotated tag")
64 assert.Equal(t, "m", pickRemoteRef(refs, "main"))
65 assert.Empty(t, pickRemoteRef(refs, "refs/heads/main"), "short names only")
66 assert.Empty(t, pickRemoteRef(refs, "nope"))
67 assert.Empty(t, pickRemoteRef(nil, "v1"))
68}