1package domain
2
3import (
4 "encoding/base64"
5 "strings"
6 "testing"
7
8 "github.com/stretchr/testify/assert"
9 "github.com/stretchr/testify/require"
10)
11
12func TestSanitizeDomainForEnvFile(t *testing.T) {
13 tests := []struct {
14 name string
15 domain string
16 want string
17 wantErr bool
18 }{
19 {name: "simple domain", domain: "example.com", want: base64.RawURLEncoding.EncodeToString([]byte("example.com"))},
20 {name: "subdomain", domain: "app.example.com", want: base64.RawURLEncoding.EncodeToString([]byte("app.example.com"))},
21 {name: "domain with port", domain: "example.com:8080", want: base64.RawURLEncoding.EncodeToString([]byte("example.com:8080"))},
22 {name: "domain with path", domain: "example.com/path", want: base64.RawURLEncoding.EncodeToString([]byte("example.com/path"))},
23 {name: "single char", domain: "a", want: base64.RawURLEncoding.EncodeToString([]byte("a"))},
24 {name: "hyphenated", domain: "my-app.example.com", want: base64.RawURLEncoding.EncodeToString([]byte("my-app.example.com"))},
25 {name: "empty", domain: "", wantErr: true},
26 {name: "path traversal", domain: "../etc/passwd", wantErr: true},
27 {name: "double dots", domain: "foo..bar", wantErr: true},
28 {name: "starts with dot", domain: ".hidden", wantErr: true},
29 {name: "ends with dot", domain: "trailing.", wantErr: true},
30 {name: "space", domain: "has space", wantErr: true},
31 {name: "special chars", domain: "bad$domain", wantErr: true},
32 {name: "underscore rejected as invalid domain character", domain: "example_com", wantErr: true},
33 }
34
35 for _, tt := range tests {
36 t.Run(tt.name, func(t *testing.T) {
37 got, err := SanitizeDomainForEnvFile(tt.domain)
38 if tt.wantErr {
39 assert.Error(t, err)
40 return
41 }
42 require.NoError(t, err)
43 assert.Equal(t, tt.want, got)
44 })
45 }
46}
47
48func TestSanitizeDomainForEnvFile_DistinguishesSeparators(t *testing.T) {
49 gotDot, err := SanitizeDomainForEnvFile("app.example.com")
50 require.NoError(t, err)
51
52 gotPort, err := SanitizeDomainForEnvFile("app:example:com")
53 require.NoError(t, err)
54
55 gotPath, err := SanitizeDomainForEnvFile("app/example/com")
56 require.NoError(t, err)
57
58 assert.NotEqual(t, gotDot, gotPort)
59 assert.NotEqual(t, gotDot, gotPath)
60 assert.NotEqual(t, gotPort, gotPath)
61}
62
63func TestParseEnvData(t *testing.T) {
64 tests := []struct {
65 name string
66 data string
67 want map[string]string
68 wantErr bool
69 }{
70 {
71 name: "simple key-value",
72 data: "FOO=bar\nBAZ=qux",
73 want: map[string]string{"FOO": "bar", "BAZ": "qux"},
74 },
75 {
76 name: "double-quoted value",
77 data: `KEY="hello world"`,
78 want: map[string]string{"KEY": "hello world"},
79 },
80 {
81 name: "single-quoted value",
82 data: `KEY='hello world'`,
83 want: map[string]string{"KEY": "hello world"},
84 },
85 {
86 name: "comment lines",
87 data: "# this is a comment\nKEY=val",
88 want: map[string]string{"KEY": "val"},
89 },
90 {
91 name: "empty lines",
92 data: "\n\nKEY=val\n\n",
93 want: map[string]string{"KEY": "val"},
94 },
95 {
96 name: "value with equals",
97 data: "URL=postgres://host:5432/db?opt=1",
98 want: map[string]string{"URL": "postgres://host:5432/db?opt=1"},
99 },
100 {
101 name: "no value",
102 data: "NOVALUE",
103 want: map[string]string{},
104 },
105 {
106 name: "empty value",
107 data: "KEY=",
108 want: map[string]string{"KEY": ""},
109 },
110 {
111 name: "empty input",
112 data: "",
113 want: map[string]string{},
114 },
115 {
116 name: "large value within buffer",
117 data: "BIG=" + strings.Repeat("x", 100000),
118 want: map[string]string{"BIG": strings.Repeat("x", 100000)},
119 },
120 {
121 name: "quoted value with inner spaces",
122 data: `KEY=" hello "`,
123 want: map[string]string{"KEY": " hello "},
124 },
125 {
126 name: "unquoted value with outer spaces",
127 data: "KEY= hello ",
128 want: map[string]string{"KEY": "hello"},
129 },
130 }
131
132 for _, tt := range tests {
133 t.Run(tt.name, func(t *testing.T) {
134 got, err := ParseEnvData([]byte(tt.data))
135 if tt.wantErr {
136 assert.Error(t, err)
137 return
138 }
139 require.NoError(t, err)
140 assert.Equal(t, tt.want, got)
141 })
142 }
143}
144
145func TestValidateEnvKey(t *testing.T) {
146 tests := []struct {
147 name string
148 key string
149 wantErr bool
150 wantErrType error
151 }{
152 {name: "simple", key: "FOO", wantErr: false},
153 {name: "with underscore", key: "FOO_BAR", wantErr: false},
154 {name: "starts with underscore", key: "_PRIVATE", wantErr: false},
155 {name: "lowercase", key: "foo", wantErr: false},
156 {name: "mixed", key: "myApp_v2", wantErr: false},
157 {name: "empty", key: "", wantErr: true, wantErrType: ErrInvalidEnvKey},
158 {name: "starts with number", key: "1BAD", wantErr: true, wantErrType: ErrInvalidEnvKey},
159 {name: "contains dot", key: "FOO.BAR", wantErr: true, wantErrType: ErrInvalidEnvKey},
160 {name: "contains slash", key: "FOO/BAR", wantErr: true, wantErrType: ErrPathTraversal},
161 {name: "contains backslash", key: `FOO\BAR`, wantErr: true, wantErrType: ErrPathTraversal},
162 {name: "path traversal", key: "..", wantErr: true, wantErrType: ErrPathTraversal},
163 {name: "contains hyphen", key: "FOO-BAR", wantErr: true, wantErrType: ErrInvalidEnvKey},
164 {name: "contains space", key: "FOO BAR", wantErr: true, wantErrType: ErrInvalidEnvKey},
165 }
166
167 for _, tt := range tests {
168 t.Run(tt.name, func(t *testing.T) {
169 err := ValidateEnvKey(tt.key)
170 if tt.wantErr {
171 require.Error(t, err)
172 if tt.wantErrType != nil {
173 assert.ErrorIs(t, err, tt.wantErrType)
174 }
175 } else {
176 assert.NoError(t, err)
177 }
178 })
179 }
180}
181
182func TestContainsSecretReference(t *testing.T) {
183 tests := []struct {
184 name string
185 value string
186 expected bool
187 }{
188 {name: "simple pass reference", value: "${pass:myapp/db}", expected: true},
189 {name: "simple sops reference", value: "${sops:secrets.yaml#/key}", expected: true},
190 {name: "pass reference in text", value: "prefix-${pass:path}-suffix", expected: true},
191 {name: "sops reference in text", value: "${sops:file.yaml}", expected: true},
192 {name: "no reference - plain text", value: "hello world", expected: false},
193 {name: "no reference - dollar sign only", value: "$FOO", expected: false},
194 {name: "no reference - empty braces", value: "${}", expected: false},
195 {name: "no reference - missing colon", value: "${pass}", expected: false},
196 {name: "no reference - extra braces", value: "${{pass:path}}", expected: false},
197 {name: "normal env var", value: "${FOO}", expected: false},
198 {name: "value with equals and reference", value: "key=${pass:secret}", expected: true},
199 {name: "quoted reference", value: `"${pass:secret}"`, expected: true},
200 {name: "multiple references", value: "${pass:a}${sops:b}", expected: true},
201 {name: "case sensitive - uppercase", value: "${PASS:secret}", expected: false},
202 {name: "case sensitive - mixed", value: "${Pass:secret}", expected: false},
203 }
204
205 for _, tt := range tests {
206 t.Run(tt.name, func(t *testing.T) {
207 result := ContainsSecretReference(tt.value)
208 assert.Equal(t, tt.expected, result)
209 })
210 }
211}
212
213func TestValidateContainerName(t *testing.T) {
214 tests := []struct {
215 name string
216 container string
217 wantErr bool
218 wantErrType error
219 }{
220 {name: "simple", container: "postgres", wantErr: false},
221 {name: "with hyphen", container: "gitea-postgres", wantErr: false},
222 {name: "with underscore", container: "gitea_postgres", wantErr: false},
223 {name: "complex real container", container: "gordon-git-example-com-gitea-postgres", wantErr: false},
224 {name: "starts with letter", container: "a-b", wantErr: false},
225 {name: "lowercase", container: "my-container", wantErr: false},
226 {name: "mixed case", container: "MyContainer-123", wantErr: false},
227 {name: "empty", container: "", wantErr: true, wantErrType: ErrInvalidContainerName},
228 {name: "starts with number", container: "1container", wantErr: true, wantErrType: ErrInvalidContainerName},
229 {name: "starts with hyphen", container: "-container", wantErr: true, wantErrType: ErrInvalidContainerName},
230 {name: "starts with underscore", container: "_container", wantErr: true, wantErrType: ErrInvalidContainerName},
231 {name: "contains slash", container: "container/name", wantErr: true, wantErrType: ErrPathTraversal},
232 {name: "contains backslash", container: `container\name`, wantErr: true, wantErrType: ErrPathTraversal},
233 {name: "path traversal", container: "..", wantErr: true, wantErrType: ErrPathTraversal},
234 {name: "contains dot", container: "container.name", wantErr: true, wantErrType: ErrInvalidContainerName},
235 {name: "contains space", container: "container name", wantErr: true, wantErrType: ErrInvalidContainerName},
236 {name: "contains special char", container: "container$name", wantErr: true, wantErrType: ErrInvalidContainerName},
237 }
238
239 for _, tt := range tests {
240 t.Run(tt.name, func(t *testing.T) {
241 err := ValidateContainerName(tt.container)
242 if tt.wantErr {
243 require.Error(t, err)
244 if tt.wantErrType != nil {
245 assert.ErrorIs(t, err, tt.wantErrType)
246 }
247 } else {
248 assert.NoError(t, err)
249 }
250 })
251 }
252}