mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-09-10 12:16:06 +00:00
The client switcher built every command as "<client> pull <ref>". That is valid for docker, podman, buildah and nerdctl, but crane requires a destination: $ crane pull seamark.cr/user/bench8x1:v2 Error: requires at least 2 arg(s), only received 1 So selecting crane handed the user a command that cannot run. pullPrefix is a prefix-only helper, which is precisely why it could not express this; add a matching pullPostfix that returns " <image>.tar" for crane and "" for everything else, including "none" (image reference only), which must get neither prefix nor postfix. Both render paths change together, since fixing one leaves the bug visible in the other: the Go template helper paints first, and updatePullCommand in app.js re-renders when the dropdown changes. Repository names may contain slashes, so only the last path segment is used — otherwise the destination would name a subdirectory that does not exist. A name ending in "/" yields no destination at all rather than a bare ".tar", on the grounds that a visibly wrong-arity command beats silently writing a hidden file. That input is not reachable through the real repo-name path. The test asserts the whole command string rather than just the postfix, so it covers the prefix/postfix interaction and the "none" case where both vanish. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PDqoCE1j3njokkZ9b1C5n9
1024 lines
22 KiB
Go
1024 lines
22 KiB
Go
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(nil)
|
|
if err != nil {
|
|
t.Fatalf("Templates(nil) 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(nil)
|
|
if err != nil {
|
|
t.Fatalf("Templates(nil) 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 TestHumanizeCount(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
count any
|
|
expected string
|
|
}{
|
|
{"zero", 0, "0"},
|
|
{"under thousand", 999, "999"},
|
|
{"exactly 1K", 1000, "1K"},
|
|
{"1.5K", 1500, "1.5K"},
|
|
{"2049 (bug repro)", 2049, "2K"},
|
|
{"5000 (whole-thousand)", 5000, "5K"},
|
|
{"exactly 1M", 1_000_000, "1M"},
|
|
{"2.5M", 2_500_000, "2.5M"},
|
|
{"exactly 1B", 1_000_000_000, "1B"},
|
|
{"1.5B", 1_500_000_000, "1.5B"},
|
|
{"negative", -2049, "-2K"},
|
|
{"int64 input", int64(2049), "2K"},
|
|
{"non-integer", "oops", "0"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := humanizeCount(tt.count)
|
|
if got != tt.expected {
|
|
t.Errorf("humanizeCount(%v) = %q, want %q", tt.count, 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(nil)
|
|
if err != nil {
|
|
t.Fatalf("Templates(nil) 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(nil)
|
|
if err != nil {
|
|
t.Fatalf("Templates(nil) 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(nil)
|
|
if err != nil {
|
|
t.Fatalf("Templates(nil) 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(nil)
|
|
if err != nil {
|
|
t.Fatalf("Templates(nil) 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(nil)
|
|
if err != nil {
|
|
t.Fatalf("Templates(nil) error = %v", err)
|
|
}
|
|
|
|
if tmpl == nil {
|
|
t.Fatal("Templates(nil) 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",
|
|
"search-results.html",
|
|
"health-badge",
|
|
"alert",
|
|
}
|
|
|
|
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(nil)
|
|
if err != nil {
|
|
t.Fatalf("Templates(nil) error = %v", err)
|
|
}
|
|
|
|
// Sample data for repo-card template
|
|
data := struct {
|
|
OwnerHandle string
|
|
OwnerAvatarURL string
|
|
Repository string
|
|
IconURL string
|
|
Description string
|
|
StarCount int
|
|
PullCount int
|
|
IsStarred bool
|
|
ArtifactType string
|
|
Tag string
|
|
Digest string
|
|
LastUpdated time.Time
|
|
RegistryURL string
|
|
OciClient string
|
|
}{
|
|
OwnerHandle: "alice.bsky.social",
|
|
OwnerAvatarURL: "",
|
|
Repository: "myapp",
|
|
IconURL: "",
|
|
Description: "A cool container image",
|
|
StarCount: 42,
|
|
PullCount: 1337,
|
|
IsStarred: true,
|
|
ArtifactType: "container-image",
|
|
Tag: "latest",
|
|
Digest: "sha256:abc123def456",
|
|
LastUpdated: time.Now().Add(-24 * time.Hour),
|
|
RegistryURL: "atcr.io",
|
|
}
|
|
|
|
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
|
|
"avatar-placeholder", // DaisyUI avatar placeholder when no icon URL
|
|
}
|
|
|
|
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(nil)
|
|
if err != nil {
|
|
t.Fatalf("Templates(nil) 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 TestTemplateExecution_HealthBadge(t *testing.T) {
|
|
tmpl, err := Templates(nil)
|
|
if err != nil {
|
|
t.Fatalf("Templates(nil) error = %v", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
data map[string]any
|
|
expectInOutput string
|
|
expectMissing string
|
|
}{
|
|
{
|
|
name: "pending state",
|
|
data: map[string]any{
|
|
"Pending": true,
|
|
"Reachable": false,
|
|
"RetryURL": "http%3A%2F%2Fexample.com",
|
|
},
|
|
expectInOutput: "badge-info",
|
|
expectMissing: "badge-warning",
|
|
},
|
|
{
|
|
name: "offline state",
|
|
data: map[string]any{
|
|
"Pending": false,
|
|
"Reachable": false,
|
|
"RetryURL": "",
|
|
},
|
|
expectInOutput: "badge-warning",
|
|
expectMissing: "badge-info",
|
|
},
|
|
{
|
|
name: "online state - empty output",
|
|
data: map[string]any{
|
|
"Pending": false,
|
|
"Reachable": true,
|
|
"RetryURL": "",
|
|
},
|
|
expectMissing: "badge",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
buf := new(bytes.Buffer)
|
|
err := tmpl.ExecuteTemplate(buf, "health-badge", tt.data)
|
|
if err != nil {
|
|
t.Fatalf("Failed to execute template: %v", err)
|
|
}
|
|
|
|
output := buf.String()
|
|
if tt.expectInOutput != "" && !strings.Contains(output, tt.expectInOutput) {
|
|
t.Errorf("Template output %q does not contain expected %q", output, tt.expectInOutput)
|
|
}
|
|
if tt.expectMissing != "" && strings.Contains(output, tt.expectMissing) {
|
|
t.Errorf("Template output %q should not contain %q", output, tt.expectMissing)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTemplateExecution_Alert(t *testing.T) {
|
|
tmpl, err := Templates(nil)
|
|
if err != nil {
|
|
t.Fatalf("Templates(nil) error = %v", err)
|
|
}
|
|
|
|
data := map[string]string{
|
|
"Type": "success",
|
|
"Message": "Operation completed!",
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
err = tmpl.ExecuteTemplate(buf, "alert", data)
|
|
if err != nil {
|
|
t.Fatalf("Failed to execute template: %v", err)
|
|
}
|
|
|
|
output := buf.String()
|
|
expectedParts := []string{"success", "check", "Operation completed!"}
|
|
for _, expected := range expectedParts {
|
|
if !strings.Contains(output, expected) {
|
|
t.Errorf("Template output %q does not contain expected %q", output, expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPublicHandler(t *testing.T) {
|
|
handler := PublicHandler(nil)
|
|
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
|
|
}
|
|
|
|
func TestJSONLDScript(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input any
|
|
expectContains []string
|
|
expectMissing []string
|
|
}{
|
|
{
|
|
name: "struct input - renders script block with JSON",
|
|
input: struct {
|
|
Context string `json:"@context"`
|
|
Type string `json:"@type"`
|
|
Name string `json:"name"`
|
|
}{
|
|
Context: "https://schema.org",
|
|
Type: "Organization",
|
|
Name: "ATCR",
|
|
},
|
|
expectContains: []string{
|
|
`<script type="application/ld+json">`,
|
|
`"@context": "https://schema.org"`,
|
|
`"@type": "Organization"`,
|
|
`"name": "ATCR"`,
|
|
`</script>`,
|
|
},
|
|
expectMissing: []string{
|
|
`"`, // Should NOT contain HTML-escaped quotes
|
|
},
|
|
},
|
|
{
|
|
name: "string input - returns as-is in script block",
|
|
input: `{"@context": "https://schema.org", "@type": "Thing"}`,
|
|
expectContains: []string{
|
|
`<script type="application/ld+json">`,
|
|
`{"@context": "https://schema.org", "@type": "Thing"}`,
|
|
`</script>`,
|
|
},
|
|
},
|
|
{
|
|
name: "nested struct - proper JSON nesting",
|
|
input: struct {
|
|
Context string `json:"@context"`
|
|
Author struct {
|
|
Type string `json:"@type"`
|
|
Name string `json:"name"`
|
|
} `json:"author"`
|
|
}{
|
|
Context: "https://schema.org",
|
|
Author: struct {
|
|
Type string `json:"@type"`
|
|
Name string `json:"name"`
|
|
}{
|
|
Type: "Person",
|
|
Name: "Alice",
|
|
},
|
|
},
|
|
expectContains: []string{
|
|
`"@context": "https://schema.org"`,
|
|
`"author": {`,
|
|
`"@type": "Person"`,
|
|
`"name": "Alice"`,
|
|
},
|
|
},
|
|
{
|
|
name: "empty struct - returns empty JSON object in script block",
|
|
input: struct{}{},
|
|
expectContains: []string{
|
|
`<script type="application/ld+json">`,
|
|
`{}`,
|
|
`</script>`,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
tmpl, err := Templates(nil)
|
|
if err != nil {
|
|
t.Fatalf("Templates(nil) error = %v", err)
|
|
}
|
|
|
|
templateStr := `{{ jsonldScript . }}`
|
|
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()
|
|
|
|
for _, expected := range tt.expectContains {
|
|
if !strings.Contains(got, expected) {
|
|
t.Errorf("jsonldScript output missing expected %q\nGot: %s", expected, got)
|
|
}
|
|
}
|
|
|
|
for _, notExpected := range tt.expectMissing {
|
|
if strings.Contains(got, notExpected) {
|
|
t.Errorf("jsonldScript output should not contain %q\nGot: %s", notExpected, got)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPullCommandGrammar exercises pullPrefix and pullPostfix together, since
|
|
// the rendered command is the concatenation of both around the image
|
|
// reference. crane is the only client that requires a destination argument.
|
|
func TestPullCommandGrammar(t *testing.T) {
|
|
tmpl, err := Templates(nil)
|
|
if err != nil {
|
|
t.Fatalf("Templates(nil) error = %v", err)
|
|
}
|
|
|
|
const src = `{{ define "pullcmd" }}{{ pullPrefix .Client }}reg.example/alice/{{ .Image }}:v1{{ pullPostfix .Client .Image }}{{ end }}`
|
|
|
|
tests := []struct {
|
|
name string
|
|
client string
|
|
image string
|
|
want string
|
|
}{
|
|
{
|
|
name: "crane gets a tarball destination",
|
|
client: "crane",
|
|
image: "bench8x1",
|
|
want: "crane pull reg.example/alice/bench8x1:v1 bench8x1.tar",
|
|
},
|
|
{
|
|
name: "docker gets no destination",
|
|
client: "docker",
|
|
image: "bench8x1",
|
|
want: "docker pull reg.example/alice/bench8x1:v1",
|
|
},
|
|
{
|
|
name: "empty client defaults to docker",
|
|
client: "",
|
|
image: "bench8x1",
|
|
want: "docker pull reg.example/alice/bench8x1:v1",
|
|
},
|
|
{
|
|
name: "none emits neither prefix nor postfix",
|
|
client: "none",
|
|
image: "bench8x1",
|
|
want: "reg.example/alice/bench8x1:v1",
|
|
},
|
|
{
|
|
name: "crane with a slashed image name uses the last segment",
|
|
client: "crane",
|
|
image: "team/sub/bench8x1",
|
|
want: "crane pull reg.example/alice/team/sub/bench8x1:v1 bench8x1.tar",
|
|
},
|
|
{
|
|
name: "none with a slashed image name stays bare",
|
|
client: "none",
|
|
image: "team/sub/bench8x1",
|
|
want: "reg.example/alice/team/sub/bench8x1:v1",
|
|
},
|
|
{
|
|
name: "crane with a trailing slash emits no destination",
|
|
client: "crane",
|
|
image: "team/",
|
|
want: "crane pull reg.example/alice/team/:v1",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
temp, err := tmpl.Clone()
|
|
if err != nil {
|
|
t.Fatalf("Clone() error = %v", err)
|
|
}
|
|
if _, err := temp.Parse(src); err != nil {
|
|
t.Fatalf("Parse() error = %v", err)
|
|
}
|
|
|
|
buf := new(bytes.Buffer)
|
|
data := struct {
|
|
Client string
|
|
Image string
|
|
}{Client: tt.client, Image: tt.image}
|
|
if err := temp.ExecuteTemplate(buf, "pullcmd", data); err != nil {
|
|
t.Fatalf("ExecuteTemplate() error = %v", err)
|
|
}
|
|
|
|
if got := buf.String(); got != tt.want {
|
|
t.Errorf("pull command = %q, want %q", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|