A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
1package hold
2
3import (
4 "os"
5 "path/filepath"
6 "testing"
7 "time"
8)
9
10// setupEnv sets environment variables for testing and returns a cleanup function
11func setupEnv(t *testing.T, vars map[string]string) func() {
12 // Save original env
13 original := make(map[string]string)
14 for k := range vars {
15 original[k] = os.Getenv(k)
16 }
17
18 // Set test env vars
19 for k, v := range vars {
20 if err := os.Setenv(k, v); err != nil {
21 t.Fatalf("Failed to set env %s: %v", k, err)
22 }
23 }
24
25 // Return cleanup function
26 return func() {
27 for k, v := range original {
28 if v == "" {
29 os.Unsetenv(k)
30 } else {
31 os.Setenv(k, v)
32 }
33 }
34 }
35}
36
37func TestLoadConfigFromEnv_Success(t *testing.T) {
38 cleanup := setupEnv(t, map[string]string{
39 "HOLD_PUBLIC_URL": "https://hold.example.com",
40 "HOLD_SERVER_ADDR": ":9000",
41 "HOLD_PUBLIC": "true",
42 "TEST_MODE": "true",
43 "HOLD_OWNER": "did:plc:owner123",
44 "HOLD_ALLOW_ALL_CREW": "true",
45 "STORAGE_DRIVER": "filesystem",
46 "STORAGE_ROOT_DIR": "/tmp/test-storage",
47 "HOLD_DATABASE_DIR": "/tmp/test-db",
48 "HOLD_KEY_PATH": "/tmp/test-key.pem",
49 })
50 defer cleanup()
51
52 cfg, err := LoadConfigFromEnv()
53 if err != nil {
54 t.Fatalf("Expected success, got error: %v", err)
55 }
56
57 // Verify server config
58 if cfg.Server.PublicURL != "https://hold.example.com" {
59 t.Errorf("Expected PublicURL=https://hold.example.com, got %s", cfg.Server.PublicURL)
60 }
61 if cfg.Server.Addr != ":9000" {
62 t.Errorf("Expected Addr=:9000, got %s", cfg.Server.Addr)
63 }
64 if !cfg.Server.Public {
65 t.Error("Expected Public=true")
66 }
67 if !cfg.Server.TestMode {
68 t.Error("Expected TestMode=true")
69 }
70 if cfg.Server.ReadTimeout != 5*time.Minute {
71 t.Errorf("Expected ReadTimeout=5m, got %v", cfg.Server.ReadTimeout)
72 }
73
74 // Verify registration config
75 if cfg.Registration.OwnerDID != "did:plc:owner123" {
76 t.Errorf("Expected OwnerDID=did:plc:owner123, got %s", cfg.Registration.OwnerDID)
77 }
78 if !cfg.Registration.AllowAllCrew {
79 t.Error("Expected AllowAllCrew=true")
80 }
81
82 // Verify database config
83 if cfg.Database.Path != "/tmp/test-db" {
84 t.Errorf("Expected Database.Path=/tmp/test-db, got %s", cfg.Database.Path)
85 }
86 if cfg.Database.KeyPath != "/tmp/test-key.pem" {
87 t.Errorf("Expected Database.KeyPath=/tmp/test-key.pem, got %s", cfg.Database.KeyPath)
88 }
89}
90
91func TestLoadConfigFromEnv_MissingPublicURL(t *testing.T) {
92 cleanup := setupEnv(t, map[string]string{
93 "HOLD_PUBLIC_URL": "", // Missing required field
94 "STORAGE_DRIVER": "filesystem",
95 })
96 defer cleanup()
97
98 _, err := LoadConfigFromEnv()
99 if err == nil {
100 t.Error("Expected error for missing HOLD_PUBLIC_URL")
101 }
102}
103
104func TestLoadConfigFromEnv_Defaults(t *testing.T) {
105 cleanup := setupEnv(t, map[string]string{
106 "HOLD_PUBLIC_URL": "https://hold.example.com",
107 "STORAGE_DRIVER": "filesystem",
108 // Don't set optional vars - test defaults
109 "HOLD_SERVER_ADDR": "",
110 "HOLD_PUBLIC": "",
111 "TEST_MODE": "",
112 "HOLD_OWNER": "",
113 "HOLD_ALLOW_ALL_CREW": "",
114 "AWS_REGION": "",
115 "STORAGE_ROOT_DIR": "",
116 "HOLD_DATABASE_DIR": "",
117 })
118 defer cleanup()
119
120 cfg, err := LoadConfigFromEnv()
121 if err != nil {
122 t.Fatalf("Expected success, got error: %v", err)
123 }
124
125 // Verify defaults
126 if cfg.Server.Addr != ":8080" {
127 t.Errorf("Expected default Addr=:8080, got %s", cfg.Server.Addr)
128 }
129 if cfg.Server.Public {
130 t.Error("Expected default Public=false")
131 }
132 if cfg.Server.TestMode {
133 t.Error("Expected default TestMode=false")
134 }
135 if cfg.Server.DisablePresignedURLs {
136 t.Error("Expected default DisablePresignedURLs=false")
137 }
138 if cfg.Registration.OwnerDID != "" {
139 t.Error("Expected default OwnerDID to be empty")
140 }
141 if cfg.Registration.AllowAllCrew {
142 t.Error("Expected default AllowAllCrew=false")
143 }
144 if cfg.Database.Path != "/var/lib/atcr-hold" {
145 t.Errorf("Expected default Database.Path=/var/lib/atcr-hold, got %s", cfg.Database.Path)
146 }
147}
148
149func TestLoadConfigFromEnv_KeyPathDefault(t *testing.T) {
150 cleanup := setupEnv(t, map[string]string{
151 "HOLD_PUBLIC_URL": "https://hold.example.com",
152 "STORAGE_DRIVER": "filesystem",
153 "HOLD_DATABASE_DIR": "/custom/db/path",
154 "HOLD_KEY_PATH": "", // Should default to {Database.Path}/signing.key
155 })
156 defer cleanup()
157
158 cfg, err := LoadConfigFromEnv()
159 if err != nil {
160 t.Fatalf("Expected success, got error: %v", err)
161 }
162
163 expectedKeyPath := filepath.Join("/custom/db/path", "signing.key")
164 if cfg.Database.KeyPath != expectedKeyPath {
165 t.Errorf("Expected KeyPath=%s, got %s", expectedKeyPath, cfg.Database.KeyPath)
166 }
167}
168
169func TestLoadConfigFromEnv_DisablePresignedURLs(t *testing.T) {
170 cleanup := setupEnv(t, map[string]string{
171 "HOLD_PUBLIC_URL": "https://hold.example.com",
172 "STORAGE_DRIVER": "filesystem",
173 "DISABLE_PRESIGNED_URLS": "true",
174 })
175 defer cleanup()
176
177 cfg, err := LoadConfigFromEnv()
178 if err != nil {
179 t.Fatalf("Expected success, got error: %v", err)
180 }
181
182 if !cfg.Server.DisablePresignedURLs {
183 t.Error("Expected DisablePresignedURLs=true")
184 }
185}
186
187func TestBuildStorageConfig_S3_Complete(t *testing.T) {
188 cleanup := setupEnv(t, map[string]string{
189 "AWS_ACCESS_KEY_ID": "test-access-key",
190 "AWS_SECRET_ACCESS_KEY": "test-secret-key",
191 "AWS_REGION": "us-west-2",
192 "S3_BUCKET": "test-bucket",
193 "S3_ENDPOINT": "https://s3.example.com",
194 })
195 defer cleanup()
196
197 cfg, err := buildStorageConfig("s3")
198 if err != nil {
199 t.Fatalf("Expected success, got error: %v", err)
200 }
201
202 s3Params, ok := cfg.Storage["s3"]
203 if !ok {
204 t.Fatal("Expected s3 storage config")
205 }
206
207 params := map[string]any(s3Params)
208
209 if params["accesskey"] != "test-access-key" {
210 t.Errorf("Expected accesskey=test-access-key, got %v", params["accesskey"])
211 }
212 if params["secretkey"] != "test-secret-key" {
213 t.Errorf("Expected secretkey=test-secret-key, got %v", params["secretkey"])
214 }
215 if params["region"] != "us-west-2" {
216 t.Errorf("Expected region=us-west-2, got %v", params["region"])
217 }
218 if params["bucket"] != "test-bucket" {
219 t.Errorf("Expected bucket=test-bucket, got %v", params["bucket"])
220 }
221 if params["regionendpoint"] != "https://s3.example.com" {
222 t.Errorf("Expected regionendpoint=https://s3.example.com, got %v", params["regionendpoint"])
223 }
224}
225
226func TestBuildStorageConfig_S3_NoEndpoint(t *testing.T) {
227 cleanup := setupEnv(t, map[string]string{
228 "AWS_ACCESS_KEY_ID": "test-key",
229 "AWS_SECRET_ACCESS_KEY": "test-secret",
230 "S3_BUCKET": "test-bucket",
231 "S3_ENDPOINT": "", // No custom endpoint
232 "AWS_REGION": "", // Test default region
233 })
234 defer cleanup()
235
236 cfg, err := buildStorageConfig("s3")
237 if err != nil {
238 t.Fatalf("Expected success, got error: %v", err)
239 }
240
241 s3Params, ok := cfg.Storage["s3"]
242 if !ok {
243 t.Fatal("Expected s3 storage config")
244 }
245
246 params := map[string]any(s3Params)
247
248 // Should have default region
249 if params["region"] != "us-east-1" {
250 t.Errorf("Expected default region=us-east-1, got %v", params["region"])
251 }
252
253 // Should not have regionendpoint
254 if _, exists := params["regionendpoint"]; exists {
255 t.Error("Expected no regionendpoint when S3_ENDPOINT not set")
256 }
257}
258
259func TestBuildStorageConfig_S3_MissingBucket(t *testing.T) {
260 cleanup := setupEnv(t, map[string]string{
261 "AWS_ACCESS_KEY_ID": "test-key",
262 "AWS_SECRET_ACCESS_KEY": "test-secret",
263 "S3_BUCKET": "", // Missing required field
264 })
265 defer cleanup()
266
267 _, err := buildStorageConfig("s3")
268 if err == nil {
269 t.Error("Expected error for missing S3_BUCKET")
270 }
271}
272
273func TestBuildStorageConfig_Filesystem(t *testing.T) {
274 cleanup := setupEnv(t, map[string]string{
275 "STORAGE_ROOT_DIR": "/custom/storage/path",
276 })
277 defer cleanup()
278
279 cfg, err := buildStorageConfig("filesystem")
280 if err != nil {
281 t.Fatalf("Expected success, got error: %v", err)
282 }
283
284 fsParams, ok := cfg.Storage["filesystem"]
285 if !ok {
286 t.Fatal("Expected filesystem storage config")
287 }
288
289 params := map[string]any(fsParams)
290
291 if params["rootdirectory"] != "/custom/storage/path" {
292 t.Errorf("Expected rootdirectory=/custom/storage/path, got %v", params["rootdirectory"])
293 }
294}
295
296func TestBuildStorageConfig_Filesystem_Default(t *testing.T) {
297 cleanup := setupEnv(t, map[string]string{
298 "STORAGE_ROOT_DIR": "", // Test default
299 })
300 defer cleanup()
301
302 cfg, err := buildStorageConfig("filesystem")
303 if err != nil {
304 t.Fatalf("Expected success, got error: %v", err)
305 }
306
307 fsParams, ok := cfg.Storage["filesystem"]
308 if !ok {
309 t.Fatal("Expected filesystem storage config")
310 }
311
312 params := map[string]any(fsParams)
313
314 if params["rootdirectory"] != "/var/lib/atcr/hold" {
315 t.Errorf("Expected default rootdirectory=/var/lib/atcr/hold, got %v", params["rootdirectory"])
316 }
317}
318
319func TestBuildStorageConfig_UnsupportedDriver(t *testing.T) {
320 cleanup := setupEnv(t, map[string]string{})
321 defer cleanup()
322
323 _, err := buildStorageConfig("azure")
324 if err == nil {
325 t.Error("Expected error for unsupported driver")
326 }
327}
328
329func TestGetEnvOrDefault_Set(t *testing.T) {
330 cleanup := setupEnv(t, map[string]string{
331 "TEST_VAR": "custom-value",
332 })
333 defer cleanup()
334
335 result := getEnvOrDefault("TEST_VAR", "default-value")
336 if result != "custom-value" {
337 t.Errorf("Expected custom-value, got %s", result)
338 }
339}
340
341func TestGetEnvOrDefault_NotSet(t *testing.T) {
342 cleanup := setupEnv(t, map[string]string{
343 "TEST_VAR": "",
344 })
345 defer cleanup()
346
347 result := getEnvOrDefault("TEST_VAR", "default-value")
348 if result != "default-value" {
349 t.Errorf("Expected default-value, got %s", result)
350 }
351}
352
353func TestGetEnvOrDefault_EmptyString(t *testing.T) {
354 cleanup := setupEnv(t, map[string]string{
355 "TEST_VAR": "",
356 })
357 defer cleanup()
358
359 result := getEnvOrDefault("TEST_VAR", "")
360 if result != "" {
361 t.Errorf("Expected empty string, got %s", result)
362 }
363}