1// SPDX-License-Identifier: AGPL-3.0-or-later
2
3package domain_test
4
5import (
6 "strings"
7 "testing"
8
9 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/require"
11
12 "bnema.dev/ermite/internal/domain"
13)
14
15func TestParseObjectID(t *testing.T) {
16 t.Parallel()
17 good := strings.Repeat("ab12", 10)
18 id, err := domain.ParseObjectID(good)
19 require.NoError(t, err)
20 assert.Equal(t, good, id.String())
21 assert.False(t, id.IsZero())
22 assert.True(t, domain.ObjectID{}.IsZero())
23
24 for _, bad := range []string{"", "abc", strings.ToUpper(good), good + "0", good[:39], strings.Repeat("g", 40), "main"} {
25 _, err := domain.ParseObjectID(bad)
26 assert.ErrorIs(t, err, domain.ErrInvalidObjectID, bad)
27 }
28}
29
30func TestParseRevision(t *testing.T) {
31 t.Parallel()
32 valid := []string{
33 "", "main", "feature/x", "refs/heads/feature/x", "v1.0.0", "HEAD",
34 "abc1234", strings.Repeat("a", 40), "release-2026.10", "café",
35 }
36 for _, s := range valid {
37 r, err := domain.ParseRevision(s)
38 require.NoError(t, err, s)
39 assert.Equal(t, s, r.String())
40 }
41 invalid := []string{
42 "-x", "--upload-pack=x", "/x", "x/", "x.", "a..b", "a@{1}", "@", "a b", "a~1", "a^", "a:b", "a?", "a*", "a[",
43 "a\\b", "a\nb", "a\x00b", "a//b", ".hidden", "a/.b", "x.lock", "a/x.lock/b", "\xff", strings.Repeat("a", 513),
44 }
45 for _, s := range invalid {
46 _, err := domain.ParseRevision(s)
47 assert.ErrorIs(t, err, domain.ErrInvalidRevision, "%q", s)
48 }
49}
50
51func TestRevisionKinds(t *testing.T) {
52 t.Parallel()
53 head, _ := domain.ParseRevision("")
54 assert.True(t, head.IsHead())
55 full, _ := domain.ParseRevision(strings.Repeat("a", 40))
56 assert.True(t, full.IsObjectID())
57 assert.False(t, full.IsAbbrevID())
58 short, _ := domain.ParseRevision("abcdef1")
59 assert.True(t, short.IsAbbrevID())
60 tooShort, _ := domain.ParseRevision("abcdef")
61 assert.False(t, tooShort.IsAbbrevID())
62 branch, _ := domain.ParseRevision("deadbeefx")
63 assert.False(t, branch.IsAbbrevID())
64
65 id, _ := domain.ParseObjectID(strings.Repeat("c", 40))
66 assert.True(t, domain.RevisionOf(id).IsObjectID())
67}
68
69func TestParseTreePath(t *testing.T) {
70 t.Parallel()
71 root, err := domain.ParseTreePath("")
72 require.NoError(t, err)
73 assert.True(t, root.IsRoot())
74 assert.Equal(t, "x", root.Join("x"))
75
76 p, err := domain.ParseTreePath("a/b/")
77 require.NoError(t, err)
78 assert.Equal(t, "a/b", p.String())
79 assert.Equal(t, "a/b/c", p.Join("c"))
80}
81
82// ParseTreePath and IsAddressablePath share one rule. The only differences
83// are the root ("" is a valid path to parse, and not addressable) and the
84// trailing slash (ParseTreePath drops one, IsAddressablePath wants the path
85// as it is).
86func TestTreePathRule(t *testing.T) {
87 long := strings.Repeat("a", 4096)
88 for _, tt := range []struct {
89 path string
90 ok bool // parses, and is addressable
91 }{
92 {"a", true}, {"a/b.txt", true}, {"dir/ünï.go", true}, {"a b/c", true}, {".github/ci.yml", true}, {long, true},
93 {"/a", false}, {"a//b", false}, {"./a", false}, {"a/..", false}, {"..", false}, {".", false},
94 {"a/../b", false}, {"tab\tname", false}, {"a\nb", false}, {"a\x00b", false}, {"a\x7fb", false},
95 {"\xff", false}, {"a/\xfe/b", false}, {long + "a", false}, {"//", false},
96 } {
97 _, err := domain.ParseTreePath(tt.path)
98 assert.Equal(t, tt.ok, err == nil, "ParseTreePath(%q)", tt.path)
99 if !tt.ok {
100 assert.ErrorIs(t, err, domain.ErrInvalidPath, "%q", tt.path)
101 }
102 assert.Equal(t, tt.ok, domain.IsAddressablePath(tt.path), "IsAddressablePath(%q)", tt.path)
103 assert.Equal(t, tt.ok, domain.IsAddressablePathBytes([]byte(tt.path)), "IsAddressablePathBytes(%q)", tt.path)
104 }
105
106 // The exceptions.
107 _, err := domain.ParseTreePath("")
108 assert.NoError(t, err, "the root parses")
109 assert.False(t, domain.IsAddressablePath(""), "the root is not addressable")
110 assert.False(t, domain.IsAddressablePathBytes(nil))
111 _, err = domain.ParseTreePath("a/")
112 assert.NoError(t, err, "one trailing slash is dropped")
113 assert.False(t, domain.IsAddressablePath("a/"), "an addressable path has none")
114 assert.False(t, domain.IsAddressablePathBytes([]byte("a/")))
115 _, err = domain.ParseTreePath("/")
116 assert.NoError(t, err, `"/" is the root with its trailing slash dropped`)
117 assert.False(t, domain.IsAddressablePath("/"))
118}
119
120func TestPathListAdd(t *testing.T) {
121 for _, tt := range []struct {
122 name string
123 adds []string
124 maxPaths, maxBytes int
125 want []string
126 wantStopAt int // index of the Add that returned false, -1 for none
127 truncated bool
128 }{
129 {"all fit", []string{"a", "b/c"}, 10, 100, []string{"a", "b/c"}, -1, false},
130 {"not addressable are skipped", []string{"a", "", "b//c", "d/", "e\nf", "g"}, 10, 100, []string{"a", "g"}, -1, false},
131 {"skipped paths do not count", []string{"", "..", "a"}, 1, 1, []string{"a"}, -1, false},
132 {"exactly maxPaths", []string{"a", "b"}, 2, 100, []string{"a", "b"}, -1, false},
133 {"over maxPaths", []string{"a", "b", "c"}, 2, 100, []string{"a", "b"}, 2, true},
134 {"exactly maxBytes", []string{"ab", "cd"}, 10, 4, []string{"ab", "cd"}, -1, false},
135 {"over maxBytes", []string{"ab", "cde", "f"}, 10, 4, []string{"ab"}, 1, true},
136 {"nothing fits", []string{"abc"}, 10, 2, nil, 0, true},
137 {"no paths allowed", []string{"a"}, 0, 100, nil, 0, true},
138 {"unaddressable past the limit is no truncation", []string{"a", "b/"}, 1, 100, []string{"a"}, -1, false},
139 } {
140 t.Run(tt.name, func(t *testing.T) {
141 var l domain.PathList
142 stop := -1
143 for i, p := range tt.adds {
144 if !l.Add(p, tt.maxPaths, tt.maxBytes) {
145 stop = i
146 break
147 }
148 }
149 assert.Equal(t, tt.want, l.Paths)
150 assert.Equal(t, tt.wantStopAt, stop)
151 assert.Equal(t, tt.truncated, l.Truncated)
152 })
153 }
154}
155
156func TestEntryKindOf(t *testing.T) {
157 t.Parallel()
158 tests := map[uint32]domain.EntryKind{
159 0o100644: domain.EntryFile,
160 0o100664: domain.EntryFile,
161 0o100755: domain.EntryExecutable,
162 0o040000: domain.EntryDirectory,
163 0o120000: domain.EntrySymlink,
164 0o160000: domain.EntrySubmodule,
165 0o000000: "",
166 0o060000: "",
167 }
168 for mode, want := range tests {
169 assert.Equal(t, want, domain.EntryKindOf(mode), "%o", mode)
170 }
171}
172
173func TestDefaultDiffLimits(t *testing.T) {
174 t.Parallel()
175 assert.Equal(t, domain.DiffLimits{MaxFiles: 300, MaxFileLines: 2000, MaxTotalLines: 20000, MaxTotalBytes: 8 << 20}, domain.DefaultDiffLimits())
176}