package appview import ( "bytes" "strings" "testing" "time" ) func TestTimeAgo(t *testing.T) { now := time.Now() tests := []struct { name string time time.Time expected string }{ { name: "just now - 30 seconds ago", time: now.Add(-30 * time.Second), expected: "just now", }, { name: "1 minute ago", time: now.Add(-1 * time.Minute), expected: "1 minute ago", }, { name: "5 minutes ago", time: now.Add(-5 * time.Minute), expected: "5 minutes ago", }, { name: "45 minutes ago", time: now.Add(-45 * time.Minute), expected: "45 minutes ago", }, { name: "1 hour ago", time: now.Add(-1 * time.Hour), expected: "1 hour ago", }, { name: "3 hours ago", time: now.Add(-3 * time.Hour), expected: "3 hours ago", }, { name: "23 hours ago", time: now.Add(-23 * time.Hour), expected: "23 hours ago", }, { name: "1 day ago", time: now.Add(-24 * time.Hour), expected: "1 day ago", }, { name: "5 days ago", time: now.Add(-5 * 24 * time.Hour), expected: "5 days ago", }, { name: "30 days ago", time: now.Add(-30 * 24 * time.Hour), expected: "30 days ago", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Get fresh template for each test case tmpl, err := Templates() if err != nil { t.Fatalf("Templates() error = %v", err) } // Execute template using timeAgo function templateStr := `{{ timeAgo . }}` buf := new(bytes.Buffer) temp, err := tmpl.New("test").Parse(templateStr) if err != nil { t.Fatalf("Failed to parse template: %v", err) } err = temp.Execute(buf, tt.time) if err != nil { t.Fatalf("Failed to execute template: %v", err) } got := buf.String() if got != tt.expected { t.Errorf("timeAgo() = %q, want %q", got, tt.expected) } }) } } func TestHumanizeBytes(t *testing.T) { tests := []struct { name string bytes int64 expected string }{ { name: "0 bytes", bytes: 0, expected: "0 B", }, { name: "512 bytes", bytes: 512, expected: "512 B", }, { name: "1023 bytes", bytes: 1023, expected: "1023 B", }, { name: "1 KB", bytes: 1024, expected: "1.0 KB", }, { name: "1.5 KB", bytes: 1536, expected: "1.5 KB", }, { name: "1 MB", bytes: 1024 * 1024, expected: "1.0 MB", }, { name: "2.5 MB", bytes: 2621440, // 2.5 * 1024 * 1024 expected: "2.5 MB", }, { name: "1 GB", bytes: 1024 * 1024 * 1024, expected: "1.0 GB", }, { name: "5.2 GB", bytes: 5583457485, // ~5.2 GB expected: "5.2 GB", }, { name: "1 TB", bytes: 1024 * 1024 * 1024 * 1024, expected: "1.0 TB", }, { name: "1.5 PB", bytes: 1688849860263936, // 1.5 PB expected: "1.5 PB", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Get fresh template for each test case tmpl, err := Templates() if err != nil { t.Fatalf("Templates() error = %v", err) } templateStr := `{{ humanizeBytes . }}` buf := new(bytes.Buffer) temp, err := tmpl.New("test").Parse(templateStr) if err != nil { t.Fatalf("Failed to parse template: %v", err) } err = temp.Execute(buf, tt.bytes) if err != nil { t.Fatalf("Failed to execute template: %v", err) } got := buf.String() if got != tt.expected { t.Errorf("humanizeBytes(%d) = %q, want %q", tt.bytes, got, tt.expected) } }) } } func TestTruncateDigest(t *testing.T) { tests := []struct { name string digest string length int expected string }{ { name: "short digest - no truncation needed", digest: "sha256:abc", length: 20, expected: "sha256:abc", }, { name: "truncate to 12 chars", digest: "sha256:abcdef123456789", length: 12, expected: "sha256:abcde...", }, { name: "truncate to 8 chars", digest: "sha256:1234567890abcdef", length: 8, expected: "sha256:1...", }, { name: "exact length match", digest: "sha256:abc", length: 10, expected: "sha256:abc", }, { name: "empty digest", digest: "", length: 10, expected: "", }, { name: "long sha256 digest", digest: "sha256:f1c8f6a4b7e9d2c0a3f5b8e1d4c7a0b3e6f9c2d5a8b1e4f7c0d3a6b9e2f5c8a1", length: 16, expected: "sha256:f1c8f6a4b...", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Get fresh template for each test case tmpl, err := Templates() if err != nil { t.Fatalf("Templates() error = %v", err) } templateStr := `{{ truncateDigest .Digest .Length }}` buf := new(bytes.Buffer) temp, err := tmpl.New("test").Parse(templateStr) if err != nil { t.Fatalf("Failed to parse template: %v", err) } data := struct { Digest string Length int }{ Digest: tt.digest, Length: tt.length, } err = temp.Execute(buf, data) if err != nil { t.Fatalf("Failed to execute template: %v", err) } got := buf.String() if got != tt.expected { t.Errorf("truncateDigest(%q, %d) = %q, want %q", tt.digest, tt.length, got, tt.expected) } }) } } func TestFirstChar(t *testing.T) { tests := []struct { name string input string expected string }{ { name: "normal string", input: "hello", expected: "h", }, { name: "uppercase", input: "World", expected: "W", }, { name: "single character", input: "a", expected: "a", }, { name: "empty string", input: "", expected: "?", }, { name: "unicode character", input: "😀 emoji", expected: "😀", }, { name: "chinese character", input: "你好", expected: "你", }, { name: "number", input: "123", expected: "1", }, { name: "special character", input: "@user", expected: "@", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Get fresh template for each test case tmpl, err := Templates() if err != nil { t.Fatalf("Templates() error = %v", err) } templateStr := `{{ firstChar . }}` buf := new(bytes.Buffer) temp, err := tmpl.New("test").Parse(templateStr) if err != nil { t.Fatalf("Failed to parse template: %v", err) } err = temp.Execute(buf, tt.input) if err != nil { t.Fatalf("Failed to execute template: %v", err) } got := buf.String() if got != tt.expected { t.Errorf("firstChar(%q) = %q, want %q", tt.input, got, tt.expected) } }) } } func TestTrimPrefix(t *testing.T) { tests := []struct { name string prefix string input string expected string }{ { name: "trim sha256 prefix", prefix: "sha256:", input: "sha256:abcdef123456", expected: "abcdef123456", }, { name: "no prefix match", prefix: "sha256:", input: "md5:abcdef123456", expected: "md5:abcdef123456", }, { name: "empty prefix", prefix: "", input: "hello", expected: "hello", }, { name: "empty string", prefix: "prefix:", input: "", expected: "", }, { name: "prefix longer than string", prefix: "very-long-prefix", input: "short", expected: "short", }, { name: "exact match", prefix: "prefix", input: "prefix", expected: "", }, { name: "partial prefix match", prefix: "sha256:", input: "sha25", expected: "sha25", }, { name: "trim docker.io prefix", prefix: "docker.io/", input: "docker.io/library/alpine", expected: "library/alpine", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Get fresh template for each test case tmpl, err := Templates() if err != nil { t.Fatalf("Templates() error = %v", err) } templateStr := `{{ trimPrefix .Prefix .Input }}` buf := new(bytes.Buffer) temp, err := tmpl.New("test").Parse(templateStr) if err != nil { t.Fatalf("Failed to parse template: %v", err) } data := struct { Prefix string Input string }{ Prefix: tt.prefix, Input: tt.input, } err = temp.Execute(buf, data) if err != nil { t.Fatalf("Failed to execute template: %v", err) } got := buf.String() if got != tt.expected { t.Errorf("trimPrefix(%q, %q) = %q, want %q", tt.prefix, tt.input, got, tt.expected) } }) } } func TestSanitizeID(t *testing.T) { tests := []struct { name string input string expected string }{ { name: "digest with colon", input: "sha256:abc123", expected: "sha256-abc123", }, { name: "full digest", input: "sha256:f1c8f6a4b7e9d2c0a3f5b8e1d4c7a0b3e6f9c2d5a8b1e4f7c0d3a6b9e2f5c8a1", expected: "sha256-f1c8f6a4b7e9d2c0a3f5b8e1d4c7a0b3e6f9c2d5a8b1e4f7c0d3a6b9e2f5c8a1", }, { name: "multiple colons", input: "sha256:abc:def:ghi", expected: "sha256-abc-def-ghi", }, { name: "no colons", input: "abcdef123456", expected: "abcdef123456", }, { name: "empty string", input: "", expected: "", }, { name: "only colon", input: ":", expected: "-", }, { name: "leading colon", input: ":abc", expected: "-abc", }, { name: "trailing colon", input: "abc:", expected: "abc-", }, { name: "version tag with periods", input: "v0.0.2", expected: "v0-0-2", }, { name: "colons and periods", input: "sha256:abc.def", expected: "sha256-abc-def", }, { name: "only period", input: ".", expected: "-", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Get fresh template for each test case tmpl, err := Templates() if err != nil { t.Fatalf("Templates() error = %v", err) } templateStr := `{{ sanitizeID . }}` buf := new(bytes.Buffer) temp, err := tmpl.New("test").Parse(templateStr) if err != nil { t.Fatalf("Failed to parse template: %v", err) } err = temp.Execute(buf, tt.input) if err != nil { t.Fatalf("Failed to execute template: %v", err) } got := buf.String() if got != tt.expected { t.Errorf("sanitizeID(%q) = %q, want %q", tt.input, got, tt.expected) } }) } } func TestTemplates(t *testing.T) { tmpl, err := Templates() if err != nil { t.Fatalf("Templates() error = %v", err) } if tmpl == nil { t.Fatal("Templates() returned nil template") } // Test that all expected templates are loaded expectedTemplates := []string{ "nav", "repo-card", "repository", "home.html", "search.html", "user.html", "login.html", "settings.html", "install.html", "manifest-modal", "push-list.html", } for _, name := range expectedTemplates { t.Run("template_"+name, func(t *testing.T) { temp := tmpl.Lookup(name) if temp == nil { t.Errorf("Expected template %q not found", name) } }) } } func TestTemplateExecution_RepoCard(t *testing.T) { tmpl, err := Templates() if err != nil { t.Fatalf("Templates() error = %v", err) } // Sample data for repo-card template data := struct { OwnerHandle string Repository string IconURL string Description string StarCount int PullCount int IsStarred bool }{ OwnerHandle: "alice.bsky.social", Repository: "myapp", IconURL: "", Description: "A cool container image", StarCount: 42, PullCount: 1337, IsStarred: true, } buf := new(bytes.Buffer) err = tmpl.ExecuteTemplate(buf, "repo-card", data) if err != nil { t.Fatalf("Failed to execute repo-card template: %v", err) } output := buf.String() // Verify expected content in output expectedContent := []string{ "alice.bsky.social", "myapp", "A cool container image", "42", // star count "1337", // pull count "featured-icon-placeholder", // no icon URL provided } for _, expected := range expectedContent { if !strings.Contains(output, expected) { t.Errorf("Template output missing expected content %q", expected) } } // Verify firstChar function is working if !strings.Contains(output, ">m<") { // first char of "myapp" t.Error("Template output missing firstChar result") } } func TestTemplateExecution_WithFuncMap(t *testing.T) { // Test that templates can use FuncMap functions tests := []struct { name string templateStr string data any expectInOutput string }{ { name: "timeAgo in template", templateStr: `{{ define "test1" }}{{ timeAgo . }}{{ end }}`, data: time.Now().Add(-5 * time.Minute), expectInOutput: "5 minutes ago", }, { name: "humanizeBytes in template", templateStr: `{{ define "test2" }}{{ humanizeBytes . }}{{ end }}`, data: int64(1024 * 1024 * 10), // 10 MB expectInOutput: "10.0 MB", }, { name: "multiple functions in template", templateStr: `{{ define "test3" }}{{ truncateDigest .Digest 12 }} - {{ firstChar .Name }}{{ end }}`, data: struct { Digest string Name string }{ Digest: "sha256:abcdef1234567890", Name: "myapp", }, expectInOutput: "sha256:abcde... - m", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Get fresh template for each test case tmpl, err := Templates() if err != nil { t.Fatalf("Templates() error = %v", err) } temp, err := tmpl.Parse(tt.templateStr) if err != nil { t.Fatalf("Failed to parse template: %v", err) } buf := new(bytes.Buffer) // Extract the template name from the define templateName := strings.Split(strings.TrimPrefix(tt.templateStr, `{{ define "`), `"`)[0] err = temp.ExecuteTemplate(buf, templateName, tt.data) if err != nil { t.Fatalf("Failed to execute template: %v", err) } output := buf.String() if !strings.Contains(output, tt.expectInOutput) { t.Errorf("Template output %q does not contain expected %q", output, tt.expectInOutput) } }) } } func TestStaticHandler(t *testing.T) { handler := StaticHandler() if handler == nil { t.Fatal("StaticHandler() returned nil") } // Test that it returns an http.Handler // Further testing would require HTTP request/response testing // which is typically done in integration tests }