A container registry that uses the AT Protocol for manifest storage and S3 for blob storage.
atcr.io
docker
container
atproto
go
1package appview
2
3import (
4 "bytes"
5 "strings"
6 "testing"
7 "time"
8)
9
10func TestTimeAgo(t *testing.T) {
11 now := time.Now()
12
13 tests := []struct {
14 name string
15 time time.Time
16 expected string
17 }{
18 {
19 name: "just now - 30 seconds ago",
20 time: now.Add(-30 * time.Second),
21 expected: "just now",
22 },
23 {
24 name: "1 minute ago",
25 time: now.Add(-1 * time.Minute),
26 expected: "1 minute ago",
27 },
28 {
29 name: "5 minutes ago",
30 time: now.Add(-5 * time.Minute),
31 expected: "5 minutes ago",
32 },
33 {
34 name: "45 minutes ago",
35 time: now.Add(-45 * time.Minute),
36 expected: "45 minutes ago",
37 },
38 {
39 name: "1 hour ago",
40 time: now.Add(-1 * time.Hour),
41 expected: "1 hour ago",
42 },
43 {
44 name: "3 hours ago",
45 time: now.Add(-3 * time.Hour),
46 expected: "3 hours ago",
47 },
48 {
49 name: "23 hours ago",
50 time: now.Add(-23 * time.Hour),
51 expected: "23 hours ago",
52 },
53 {
54 name: "1 day ago",
55 time: now.Add(-24 * time.Hour),
56 expected: "1 day ago",
57 },
58 {
59 name: "5 days ago",
60 time: now.Add(-5 * 24 * time.Hour),
61 expected: "5 days ago",
62 },
63 {
64 name: "30 days ago",
65 time: now.Add(-30 * 24 * time.Hour),
66 expected: "30 days ago",
67 },
68 }
69
70 for _, tt := range tests {
71 t.Run(tt.name, func(t *testing.T) {
72 // Get fresh template for each test case
73 tmpl, err := Templates()
74 if err != nil {
75 t.Fatalf("Templates() error = %v", err)
76 }
77
78 // Execute template using timeAgo function
79 templateStr := `{{ timeAgo . }}`
80 buf := new(bytes.Buffer)
81 temp, err := tmpl.New("test").Parse(templateStr)
82 if err != nil {
83 t.Fatalf("Failed to parse template: %v", err)
84 }
85
86 err = temp.Execute(buf, tt.time)
87 if err != nil {
88 t.Fatalf("Failed to execute template: %v", err)
89 }
90
91 got := buf.String()
92 if got != tt.expected {
93 t.Errorf("timeAgo() = %q, want %q", got, tt.expected)
94 }
95 })
96 }
97}
98
99func TestHumanizeBytes(t *testing.T) {
100 tests := []struct {
101 name string
102 bytes int64
103 expected string
104 }{
105 {
106 name: "0 bytes",
107 bytes: 0,
108 expected: "0 B",
109 },
110 {
111 name: "512 bytes",
112 bytes: 512,
113 expected: "512 B",
114 },
115 {
116 name: "1023 bytes",
117 bytes: 1023,
118 expected: "1023 B",
119 },
120 {
121 name: "1 KB",
122 bytes: 1024,
123 expected: "1.0 KB",
124 },
125 {
126 name: "1.5 KB",
127 bytes: 1536,
128 expected: "1.5 KB",
129 },
130 {
131 name: "1 MB",
132 bytes: 1024 * 1024,
133 expected: "1.0 MB",
134 },
135 {
136 name: "2.5 MB",
137 bytes: 2621440, // 2.5 * 1024 * 1024
138 expected: "2.5 MB",
139 },
140 {
141 name: "1 GB",
142 bytes: 1024 * 1024 * 1024,
143 expected: "1.0 GB",
144 },
145 {
146 name: "5.2 GB",
147 bytes: 5583457485, // ~5.2 GB
148 expected: "5.2 GB",
149 },
150 {
151 name: "1 TB",
152 bytes: 1024 * 1024 * 1024 * 1024,
153 expected: "1.0 TB",
154 },
155 {
156 name: "1.5 PB",
157 bytes: 1688849860263936, // 1.5 PB
158 expected: "1.5 PB",
159 },
160 }
161
162 for _, tt := range tests {
163 t.Run(tt.name, func(t *testing.T) {
164 // Get fresh template for each test case
165 tmpl, err := Templates()
166 if err != nil {
167 t.Fatalf("Templates() error = %v", err)
168 }
169
170 templateStr := `{{ humanizeBytes . }}`
171 buf := new(bytes.Buffer)
172 temp, err := tmpl.New("test").Parse(templateStr)
173 if err != nil {
174 t.Fatalf("Failed to parse template: %v", err)
175 }
176
177 err = temp.Execute(buf, tt.bytes)
178 if err != nil {
179 t.Fatalf("Failed to execute template: %v", err)
180 }
181
182 got := buf.String()
183 if got != tt.expected {
184 t.Errorf("humanizeBytes(%d) = %q, want %q", tt.bytes, got, tt.expected)
185 }
186 })
187 }
188}
189
190func TestTruncateDigest(t *testing.T) {
191 tests := []struct {
192 name string
193 digest string
194 length int
195 expected string
196 }{
197 {
198 name: "short digest - no truncation needed",
199 digest: "sha256:abc",
200 length: 20,
201 expected: "sha256:abc",
202 },
203 {
204 name: "truncate to 12 chars",
205 digest: "sha256:abcdef123456789",
206 length: 12,
207 expected: "sha256:abcde...",
208 },
209 {
210 name: "truncate to 8 chars",
211 digest: "sha256:1234567890abcdef",
212 length: 8,
213 expected: "sha256:1...",
214 },
215 {
216 name: "exact length match",
217 digest: "sha256:abc",
218 length: 10,
219 expected: "sha256:abc",
220 },
221 {
222 name: "empty digest",
223 digest: "",
224 length: 10,
225 expected: "",
226 },
227 {
228 name: "long sha256 digest",
229 digest: "sha256:f1c8f6a4b7e9d2c0a3f5b8e1d4c7a0b3e6f9c2d5a8b1e4f7c0d3a6b9e2f5c8a1",
230 length: 16,
231 expected: "sha256:f1c8f6a4b...",
232 },
233 }
234
235 for _, tt := range tests {
236 t.Run(tt.name, func(t *testing.T) {
237 // Get fresh template for each test case
238 tmpl, err := Templates()
239 if err != nil {
240 t.Fatalf("Templates() error = %v", err)
241 }
242
243 templateStr := `{{ truncateDigest .Digest .Length }}`
244 buf := new(bytes.Buffer)
245 temp, err := tmpl.New("test").Parse(templateStr)
246 if err != nil {
247 t.Fatalf("Failed to parse template: %v", err)
248 }
249
250 data := struct {
251 Digest string
252 Length int
253 }{
254 Digest: tt.digest,
255 Length: tt.length,
256 }
257
258 err = temp.Execute(buf, data)
259 if err != nil {
260 t.Fatalf("Failed to execute template: %v", err)
261 }
262
263 got := buf.String()
264 if got != tt.expected {
265 t.Errorf("truncateDigest(%q, %d) = %q, want %q", tt.digest, tt.length, got, tt.expected)
266 }
267 })
268 }
269}
270
271func TestFirstChar(t *testing.T) {
272 tests := []struct {
273 name string
274 input string
275 expected string
276 }{
277 {
278 name: "normal string",
279 input: "hello",
280 expected: "h",
281 },
282 {
283 name: "uppercase",
284 input: "World",
285 expected: "W",
286 },
287 {
288 name: "single character",
289 input: "a",
290 expected: "a",
291 },
292 {
293 name: "empty string",
294 input: "",
295 expected: "?",
296 },
297 {
298 name: "unicode character",
299 input: "😀 emoji",
300 expected: "😀",
301 },
302 {
303 name: "chinese character",
304 input: "你好",
305 expected: "你",
306 },
307 {
308 name: "number",
309 input: "123",
310 expected: "1",
311 },
312 {
313 name: "special character",
314 input: "@user",
315 expected: "@",
316 },
317 }
318
319 for _, tt := range tests {
320 t.Run(tt.name, func(t *testing.T) {
321 // Get fresh template for each test case
322 tmpl, err := Templates()
323 if err != nil {
324 t.Fatalf("Templates() error = %v", err)
325 }
326
327 templateStr := `{{ firstChar . }}`
328 buf := new(bytes.Buffer)
329 temp, err := tmpl.New("test").Parse(templateStr)
330 if err != nil {
331 t.Fatalf("Failed to parse template: %v", err)
332 }
333
334 err = temp.Execute(buf, tt.input)
335 if err != nil {
336 t.Fatalf("Failed to execute template: %v", err)
337 }
338
339 got := buf.String()
340 if got != tt.expected {
341 t.Errorf("firstChar(%q) = %q, want %q", tt.input, got, tt.expected)
342 }
343 })
344 }
345}
346
347func TestTrimPrefix(t *testing.T) {
348 tests := []struct {
349 name string
350 prefix string
351 input string
352 expected string
353 }{
354 {
355 name: "trim sha256 prefix",
356 prefix: "sha256:",
357 input: "sha256:abcdef123456",
358 expected: "abcdef123456",
359 },
360 {
361 name: "no prefix match",
362 prefix: "sha256:",
363 input: "md5:abcdef123456",
364 expected: "md5:abcdef123456",
365 },
366 {
367 name: "empty prefix",
368 prefix: "",
369 input: "hello",
370 expected: "hello",
371 },
372 {
373 name: "empty string",
374 prefix: "prefix:",
375 input: "",
376 expected: "",
377 },
378 {
379 name: "prefix longer than string",
380 prefix: "very-long-prefix",
381 input: "short",
382 expected: "short",
383 },
384 {
385 name: "exact match",
386 prefix: "prefix",
387 input: "prefix",
388 expected: "",
389 },
390 {
391 name: "partial prefix match",
392 prefix: "sha256:",
393 input: "sha25",
394 expected: "sha25",
395 },
396 {
397 name: "trim docker.io prefix",
398 prefix: "docker.io/",
399 input: "docker.io/library/alpine",
400 expected: "library/alpine",
401 },
402 }
403
404 for _, tt := range tests {
405 t.Run(tt.name, func(t *testing.T) {
406 // Get fresh template for each test case
407 tmpl, err := Templates()
408 if err != nil {
409 t.Fatalf("Templates() error = %v", err)
410 }
411
412 templateStr := `{{ trimPrefix .Prefix .Input }}`
413 buf := new(bytes.Buffer)
414 temp, err := tmpl.New("test").Parse(templateStr)
415 if err != nil {
416 t.Fatalf("Failed to parse template: %v", err)
417 }
418
419 data := struct {
420 Prefix string
421 Input string
422 }{
423 Prefix: tt.prefix,
424 Input: tt.input,
425 }
426
427 err = temp.Execute(buf, data)
428 if err != nil {
429 t.Fatalf("Failed to execute template: %v", err)
430 }
431
432 got := buf.String()
433 if got != tt.expected {
434 t.Errorf("trimPrefix(%q, %q) = %q, want %q", tt.prefix, tt.input, got, tt.expected)
435 }
436 })
437 }
438}
439
440func TestSanitizeID(t *testing.T) {
441 tests := []struct {
442 name string
443 input string
444 expected string
445 }{
446 {
447 name: "digest with colon",
448 input: "sha256:abc123",
449 expected: "sha256-abc123",
450 },
451 {
452 name: "full digest",
453 input: "sha256:f1c8f6a4b7e9d2c0a3f5b8e1d4c7a0b3e6f9c2d5a8b1e4f7c0d3a6b9e2f5c8a1",
454 expected: "sha256-f1c8f6a4b7e9d2c0a3f5b8e1d4c7a0b3e6f9c2d5a8b1e4f7c0d3a6b9e2f5c8a1",
455 },
456 {
457 name: "multiple colons",
458 input: "sha256:abc:def:ghi",
459 expected: "sha256-abc-def-ghi",
460 },
461 {
462 name: "no colons",
463 input: "abcdef123456",
464 expected: "abcdef123456",
465 },
466 {
467 name: "empty string",
468 input: "",
469 expected: "",
470 },
471 {
472 name: "only colon",
473 input: ":",
474 expected: "-",
475 },
476 {
477 name: "leading colon",
478 input: ":abc",
479 expected: "-abc",
480 },
481 {
482 name: "trailing colon",
483 input: "abc:",
484 expected: "abc-",
485 },
486 {
487 name: "version tag with periods",
488 input: "v0.0.2",
489 expected: "v0-0-2",
490 },
491 {
492 name: "colons and periods",
493 input: "sha256:abc.def",
494 expected: "sha256-abc-def",
495 },
496 {
497 name: "only period",
498 input: ".",
499 expected: "-",
500 },
501 }
502
503 for _, tt := range tests {
504 t.Run(tt.name, func(t *testing.T) {
505 // Get fresh template for each test case
506 tmpl, err := Templates()
507 if err != nil {
508 t.Fatalf("Templates() error = %v", err)
509 }
510
511 templateStr := `{{ sanitizeID . }}`
512 buf := new(bytes.Buffer)
513 temp, err := tmpl.New("test").Parse(templateStr)
514 if err != nil {
515 t.Fatalf("Failed to parse template: %v", err)
516 }
517
518 err = temp.Execute(buf, tt.input)
519 if err != nil {
520 t.Fatalf("Failed to execute template: %v", err)
521 }
522
523 got := buf.String()
524 if got != tt.expected {
525 t.Errorf("sanitizeID(%q) = %q, want %q", tt.input, got, tt.expected)
526 }
527 })
528 }
529}
530
531func TestTemplates(t *testing.T) {
532 tmpl, err := Templates()
533 if err != nil {
534 t.Fatalf("Templates() error = %v", err)
535 }
536
537 if tmpl == nil {
538 t.Fatal("Templates() returned nil template")
539 }
540
541 // Test that all expected templates are loaded
542 expectedTemplates := []string{
543 "nav",
544 "repo-card",
545 "repository",
546 "home.html",
547 "search.html",
548 "user.html",
549 "login.html",
550 "settings.html",
551 "install.html",
552 "manifest-modal",
553 "push-list.html",
554 }
555
556 for _, name := range expectedTemplates {
557 t.Run("template_"+name, func(t *testing.T) {
558 temp := tmpl.Lookup(name)
559 if temp == nil {
560 t.Errorf("Expected template %q not found", name)
561 }
562 })
563 }
564}
565
566func TestTemplateExecution_RepoCard(t *testing.T) {
567 tmpl, err := Templates()
568 if err != nil {
569 t.Fatalf("Templates() error = %v", err)
570 }
571
572 // Sample data for repo-card template
573 data := struct {
574 OwnerHandle string
575 Repository string
576 IconURL string
577 Description string
578 StarCount int
579 PullCount int
580 IsStarred bool
581 }{
582 OwnerHandle: "alice.bsky.social",
583 Repository: "myapp",
584 IconURL: "",
585 Description: "A cool container image",
586 StarCount: 42,
587 PullCount: 1337,
588 IsStarred: true,
589 }
590
591 buf := new(bytes.Buffer)
592 err = tmpl.ExecuteTemplate(buf, "repo-card", data)
593 if err != nil {
594 t.Fatalf("Failed to execute repo-card template: %v", err)
595 }
596
597 output := buf.String()
598
599 // Verify expected content in output
600 expectedContent := []string{
601 "alice.bsky.social",
602 "myapp",
603 "A cool container image",
604 "42", // star count
605 "1337", // pull count
606 "featured-icon-placeholder", // no icon URL provided
607 }
608
609 for _, expected := range expectedContent {
610 if !strings.Contains(output, expected) {
611 t.Errorf("Template output missing expected content %q", expected)
612 }
613 }
614
615 // Verify firstChar function is working
616 if !strings.Contains(output, ">m<") { // first char of "myapp"
617 t.Error("Template output missing firstChar result")
618 }
619}
620
621func TestTemplateExecution_WithFuncMap(t *testing.T) {
622 // Test that templates can use FuncMap functions
623 tests := []struct {
624 name string
625 templateStr string
626 data any
627 expectInOutput string
628 }{
629 {
630 name: "timeAgo in template",
631 templateStr: `{{ define "test1" }}{{ timeAgo . }}{{ end }}`,
632 data: time.Now().Add(-5 * time.Minute),
633 expectInOutput: "5 minutes ago",
634 },
635 {
636 name: "humanizeBytes in template",
637 templateStr: `{{ define "test2" }}{{ humanizeBytes . }}{{ end }}`,
638 data: int64(1024 * 1024 * 10), // 10 MB
639 expectInOutput: "10.0 MB",
640 },
641 {
642 name: "multiple functions in template",
643 templateStr: `{{ define "test3" }}{{ truncateDigest .Digest 12 }} - {{ firstChar .Name }}{{ end }}`,
644 data: struct {
645 Digest string
646 Name string
647 }{
648 Digest: "sha256:abcdef1234567890",
649 Name: "myapp",
650 },
651 expectInOutput: "sha256:abcde... - m",
652 },
653 }
654
655 for _, tt := range tests {
656 t.Run(tt.name, func(t *testing.T) {
657 // Get fresh template for each test case
658 tmpl, err := Templates()
659 if err != nil {
660 t.Fatalf("Templates() error = %v", err)
661 }
662
663 temp, err := tmpl.Parse(tt.templateStr)
664 if err != nil {
665 t.Fatalf("Failed to parse template: %v", err)
666 }
667
668 buf := new(bytes.Buffer)
669 // Extract the template name from the define
670 templateName := strings.Split(strings.TrimPrefix(tt.templateStr, `{{ define "`), `"`)[0]
671 err = temp.ExecuteTemplate(buf, templateName, tt.data)
672 if err != nil {
673 t.Fatalf("Failed to execute template: %v", err)
674 }
675
676 output := buf.String()
677 if !strings.Contains(output, tt.expectInOutput) {
678 t.Errorf("Template output %q does not contain expected %q", output, tt.expectInOutput)
679 }
680 })
681 }
682}
683
684func TestStaticHandler(t *testing.T) {
685 handler := StaticHandler()
686 if handler == nil {
687 t.Fatal("StaticHandler() returned nil")
688 }
689
690 // Test that it returns an http.Handler
691 // Further testing would require HTTP request/response testing
692 // which is typically done in integration tests
693}