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