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(nil)
74 if err != nil {
75 t.Fatalf("Templates(nil) 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(nil)
166 if err != nil {
167 t.Fatalf("Templates(nil) 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(nil)
239 if err != nil {
240 t.Fatalf("Templates(nil) 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(nil)
323 if err != nil {
324 t.Fatalf("Templates(nil) 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(nil)
408 if err != nil {
409 t.Fatalf("Templates(nil) 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(nil)
507 if err != nil {
508 t.Fatalf("Templates(nil) 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(nil)
533 if err != nil {
534 t.Fatalf("Templates(nil) error = %v", err)
535 }
536
537 if tmpl == nil {
538 t.Fatal("Templates(nil) 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 "search-results.html",
554 "health-badge",
555 "alert",
556 }
557
558 for _, name := range expectedTemplates {
559 t.Run("template_"+name, func(t *testing.T) {
560 temp := tmpl.Lookup(name)
561 if temp == nil {
562 t.Errorf("Expected template %q not found", name)
563 }
564 })
565 }
566}
567
568func TestTemplateExecution_RepoCard(t *testing.T) {
569 tmpl, err := Templates(nil)
570 if err != nil {
571 t.Fatalf("Templates(nil) error = %v", err)
572 }
573
574 // Sample data for repo-card template
575 data := struct {
576 OwnerHandle string
577 OwnerAvatarURL string
578 Repository string
579 IconURL string
580 Description string
581 StarCount int
582 PullCount int
583 IsStarred bool
584 ArtifactType string
585 Tag string
586 Digest string
587 LastUpdated time.Time
588 RegistryURL string
589 }{
590 OwnerHandle: "alice.bsky.social",
591 OwnerAvatarURL: "",
592 Repository: "myapp",
593 IconURL: "",
594 Description: "A cool container image",
595 StarCount: 42,
596 PullCount: 1337,
597 IsStarred: true,
598 ArtifactType: "container-image",
599 Tag: "latest",
600 Digest: "sha256:abc123def456",
601 LastUpdated: time.Now().Add(-24 * time.Hour),
602 RegistryURL: "atcr.io",
603 }
604
605 buf := new(bytes.Buffer)
606 err = tmpl.ExecuteTemplate(buf, "repo-card", data)
607 if err != nil {
608 t.Fatalf("Failed to execute repo-card template: %v", err)
609 }
610
611 output := buf.String()
612
613 // Verify expected content in output
614 expectedContent := []string{
615 "alice.bsky.social",
616 "myapp",
617 "A cool container image",
618 "42", // star count
619 "1337", // pull count
620 "avatar-placeholder", // DaisyUI avatar placeholder when no icon URL
621 }
622
623 for _, expected := range expectedContent {
624 if !strings.Contains(output, expected) {
625 t.Errorf("Template output missing expected content %q", expected)
626 }
627 }
628
629 // Verify firstChar function is working
630 if !strings.Contains(output, ">m<") { // first char of "myapp"
631 t.Error("Template output missing firstChar result")
632 }
633}
634
635func TestTemplateExecution_WithFuncMap(t *testing.T) {
636 // Test that templates can use FuncMap functions
637 tests := []struct {
638 name string
639 templateStr string
640 data any
641 expectInOutput string
642 }{
643 {
644 name: "timeAgo in template",
645 templateStr: `{{ define "test1" }}{{ timeAgo . }}{{ end }}`,
646 data: time.Now().Add(-5 * time.Minute),
647 expectInOutput: "5 minutes ago",
648 },
649 {
650 name: "humanizeBytes in template",
651 templateStr: `{{ define "test2" }}{{ humanizeBytes . }}{{ end }}`,
652 data: int64(1024 * 1024 * 10), // 10 MB
653 expectInOutput: "10.0 MB",
654 },
655 {
656 name: "multiple functions in template",
657 templateStr: `{{ define "test3" }}{{ truncateDigest .Digest 12 }} - {{ firstChar .Name }}{{ end }}`,
658 data: struct {
659 Digest string
660 Name string
661 }{
662 Digest: "sha256:abcdef1234567890",
663 Name: "myapp",
664 },
665 expectInOutput: "sha256:abcde... - m",
666 },
667 }
668
669 for _, tt := range tests {
670 t.Run(tt.name, func(t *testing.T) {
671 // Get fresh template for each test case
672 tmpl, err := Templates(nil)
673 if err != nil {
674 t.Fatalf("Templates(nil) error = %v", err)
675 }
676
677 temp, err := tmpl.Parse(tt.templateStr)
678 if err != nil {
679 t.Fatalf("Failed to parse template: %v", err)
680 }
681
682 buf := new(bytes.Buffer)
683 // Extract the template name from the define
684 templateName := strings.Split(strings.TrimPrefix(tt.templateStr, `{{ define "`), `"`)[0]
685 err = temp.ExecuteTemplate(buf, templateName, tt.data)
686 if err != nil {
687 t.Fatalf("Failed to execute template: %v", err)
688 }
689
690 output := buf.String()
691 if !strings.Contains(output, tt.expectInOutput) {
692 t.Errorf("Template output %q does not contain expected %q", output, tt.expectInOutput)
693 }
694 })
695 }
696}
697
698func TestTemplateExecution_HealthBadge(t *testing.T) {
699 tmpl, err := Templates(nil)
700 if err != nil {
701 t.Fatalf("Templates(nil) error = %v", err)
702 }
703
704 tests := []struct {
705 name string
706 data map[string]any
707 expectInOutput string
708 expectMissing string
709 }{
710 {
711 name: "pending state",
712 data: map[string]any{
713 "Pending": true,
714 "Reachable": false,
715 "RetryURL": "http%3A%2F%2Fexample.com",
716 },
717 expectInOutput: "badge-info",
718 expectMissing: "badge-warning",
719 },
720 {
721 name: "offline state",
722 data: map[string]any{
723 "Pending": false,
724 "Reachable": false,
725 "RetryURL": "",
726 },
727 expectInOutput: "badge-warning",
728 expectMissing: "badge-info",
729 },
730 {
731 name: "online state - empty output",
732 data: map[string]any{
733 "Pending": false,
734 "Reachable": true,
735 "RetryURL": "",
736 },
737 expectMissing: "badge",
738 },
739 }
740
741 for _, tt := range tests {
742 t.Run(tt.name, func(t *testing.T) {
743 buf := new(bytes.Buffer)
744 err := tmpl.ExecuteTemplate(buf, "health-badge", tt.data)
745 if err != nil {
746 t.Fatalf("Failed to execute template: %v", err)
747 }
748
749 output := buf.String()
750 if tt.expectInOutput != "" && !strings.Contains(output, tt.expectInOutput) {
751 t.Errorf("Template output %q does not contain expected %q", output, tt.expectInOutput)
752 }
753 if tt.expectMissing != "" && strings.Contains(output, tt.expectMissing) {
754 t.Errorf("Template output %q should not contain %q", output, tt.expectMissing)
755 }
756 })
757 }
758}
759
760func TestTemplateExecution_Alert(t *testing.T) {
761 tmpl, err := Templates(nil)
762 if err != nil {
763 t.Fatalf("Templates(nil) error = %v", err)
764 }
765
766 data := map[string]string{
767 "Type": "success",
768 "Message": "Operation completed!",
769 }
770
771 buf := new(bytes.Buffer)
772 err = tmpl.ExecuteTemplate(buf, "alert", data)
773 if err != nil {
774 t.Fatalf("Failed to execute template: %v", err)
775 }
776
777 output := buf.String()
778 expectedParts := []string{"success", "check", "Operation completed!"}
779 for _, expected := range expectedParts {
780 if !strings.Contains(output, expected) {
781 t.Errorf("Template output %q does not contain expected %q", output, expected)
782 }
783 }
784}
785
786func TestPublicHandler(t *testing.T) {
787 handler := PublicHandler(nil)
788 if handler == nil {
789 t.Fatal("StaticHandler() returned nil")
790 }
791
792 // Test that it returns an http.Handler
793 // Further testing would require HTTP request/response testing
794 // which is typically done in integration tests
795}
796
797func TestJSONLDScript(t *testing.T) {
798 tests := []struct {
799 name string
800 input any
801 expectContains []string
802 expectMissing []string
803 }{
804 {
805 name: "struct input - renders script block with JSON",
806 input: struct {
807 Context string `json:"@context"`
808 Type string `json:"@type"`
809 Name string `json:"name"`
810 }{
811 Context: "https://schema.org",
812 Type: "Organization",
813 Name: "ATCR",
814 },
815 expectContains: []string{
816 `<script type="application/ld+json">`,
817 `"@context": "https://schema.org"`,
818 `"@type": "Organization"`,
819 `"name": "ATCR"`,
820 `</script>`,
821 },
822 expectMissing: []string{
823 `"`, // Should NOT contain HTML-escaped quotes
824 },
825 },
826 {
827 name: "string input - returns as-is in script block",
828 input: `{"@context": "https://schema.org", "@type": "Thing"}`,
829 expectContains: []string{
830 `<script type="application/ld+json">`,
831 `{"@context": "https://schema.org", "@type": "Thing"}`,
832 `</script>`,
833 },
834 },
835 {
836 name: "nested struct - proper JSON nesting",
837 input: struct {
838 Context string `json:"@context"`
839 Author struct {
840 Type string `json:"@type"`
841 Name string `json:"name"`
842 } `json:"author"`
843 }{
844 Context: "https://schema.org",
845 Author: struct {
846 Type string `json:"@type"`
847 Name string `json:"name"`
848 }{
849 Type: "Person",
850 Name: "Alice",
851 },
852 },
853 expectContains: []string{
854 `"@context": "https://schema.org"`,
855 `"author": {`,
856 `"@type": "Person"`,
857 `"name": "Alice"`,
858 },
859 },
860 {
861 name: "empty struct - returns empty JSON object in script block",
862 input: struct{}{},
863 expectContains: []string{
864 `<script type="application/ld+json">`,
865 `{}`,
866 `</script>`,
867 },
868 },
869 }
870
871 for _, tt := range tests {
872 t.Run(tt.name, func(t *testing.T) {
873 tmpl, err := Templates(nil)
874 if err != nil {
875 t.Fatalf("Templates(nil) error = %v", err)
876 }
877
878 templateStr := `{{ jsonldScript . }}`
879 buf := new(bytes.Buffer)
880 temp, err := tmpl.New("test").Parse(templateStr)
881 if err != nil {
882 t.Fatalf("Failed to parse template: %v", err)
883 }
884
885 err = temp.Execute(buf, tt.input)
886 if err != nil {
887 t.Fatalf("Failed to execute template: %v", err)
888 }
889
890 got := buf.String()
891
892 for _, expected := range tt.expectContains {
893 if !strings.Contains(got, expected) {
894 t.Errorf("jsonldScript output missing expected %q\nGot: %s", expected, got)
895 }
896 }
897
898 for _, notExpected := range tt.expectMissing {
899 if strings.Contains(got, notExpected) {
900 t.Errorf("jsonldScript output should not contain %q\nGot: %s", notExpected, got)
901 }
902 }
903 })
904 }
905}