A container registry that uses the AT Protocol for manifest storage and S3 for blob storage. atcr.io
docker container atproto go
at main 905 lines 20 kB view raw
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 `&#34;`, // 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}