From 7afb754fbd52f155c7f41f7634d08b6ff815d232 Mon Sep 17 00:00:00 2001 From: Eneko Galan Date: Thu, 9 Jul 2026 18:12:25 +0200 Subject: [PATCH 1/2] refactor(tests): reorganize and enhance test structure for buildhistory and dockercli - Moved existing tests from internal packages to dedicated test packages for better organization. - Added new tests for buildhistory and dockercli functionalities, including context handling and device login. - Removed outdated tests from internal packages to streamline the codebase and improve maintainability. --- .cursor/rules/calf.mdc | 7 +- CLAUDE.md | 7 +- .../internal/buildhistory/artifacts_test.go | 39 ----- backend/internal/buildhistory/history_test.go | 56 ------ backend/internal/buildhistory/inspect_test.go | 39 ----- .../migration/compose_migration_test.go | 161 ------------------ backend/internal/migration/disk_test.go | 53 ------ .../internal/runtime/inspect_decode_test.go | 42 ----- backend/test/buildhistory/history_test.go | 37 ++++ .../dockercli/context_test.go | 22 ++- .../oauth => test}/dockerhub/device_test.go | 0 .../runtime/build_enrich_test.go | 8 +- .../runtime/build_parser_test.go | 30 +--- .../volumeexport/name_pattern_test.go | 26 +-- .../volumeexport/schedule_timing_test.go | 56 +++--- 15 files changed, 121 insertions(+), 462 deletions(-) delete mode 100644 backend/internal/buildhistory/artifacts_test.go delete mode 100644 backend/internal/buildhistory/history_test.go delete mode 100644 backend/internal/buildhistory/inspect_test.go delete mode 100644 backend/internal/migration/compose_migration_test.go delete mode 100644 backend/internal/migration/disk_test.go delete mode 100644 backend/internal/runtime/inspect_decode_test.go create mode 100644 backend/test/buildhistory/history_test.go rename backend/{internal => test}/dockercli/context_test.go (54%) rename backend/{internal/oauth => test}/dockerhub/device_test.go (100%) rename backend/{internal => test}/runtime/build_enrich_test.go (76%) rename backend/{internal => test}/runtime/build_parser_test.go (76%) rename backend/{internal => test}/volumeexport/name_pattern_test.go (61%) rename backend/{internal => test}/volumeexport/schedule_timing_test.go (70%) diff --git a/.cursor/rules/calf.mdc b/.cursor/rules/calf.mdc index 07e6235..58153d8 100644 --- a/.cursor/rules/calf.mdc +++ b/.cursor/rules/calf.mdc @@ -106,8 +106,13 @@ calf/ │ │ └── open.go Cross-platform "open URL" helper (open/xdg-open/rundll32) │ ├── test/ External test packages (see Testing Conventions) │ │ ├── api/api_test.go +│ │ ├── buildhistory/history_test.go +│ │ ├── buildstore/buildstore_test.go │ │ ├── config/config_test.go -│ │ └── runtime/ command_error, image_history, localhost_proxy, nerdctl, registry, volume_detail tests +│ │ ├── dockercli/context_test.go +│ │ ├── dockerhub/device_test.go +│ │ ├── runtime/ build_enrich, build_parser, command_error, image_history, localhost_proxy, nerdctl, network, registry, volume_detail tests +│ │ └── volumeexport/ name_pattern, schedule_timing tests │ ├── version/version.go Single Version constant │ └── go.mod / go.sum Module github.com/enegalan/calf/backend, Go 1.22.1 ├── ui/ Flutter application diff --git a/CLAUDE.md b/CLAUDE.md index 7afe92f..432dc82 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -104,8 +104,13 @@ calf/ │ │ └── open.go Cross-platform "open URL" helper (open/xdg-open/rundll32) │ ├── test/ External test packages (see Testing Conventions) │ │ ├── api/api_test.go +│ │ ├── buildhistory/history_test.go +│ │ ├── buildstore/buildstore_test.go │ │ ├── config/config_test.go -│ │ └── runtime/ command_error, image_history, localhost_proxy, nerdctl, registry, volume_detail tests +│ │ ├── dockercli/context_test.go +│ │ ├── dockerhub/device_test.go +│ │ ├── runtime/ build_enrich, build_parser, command_error, image_history, localhost_proxy, nerdctl, network, registry, volume_detail tests +│ │ └── volumeexport/ name_pattern, schedule_timing tests │ ├── version/version.go Single Version constant │ └── go.mod / go.sum Module github.com/enegalan/calf/backend, Go 1.22.1 ├── ui/ Flutter application diff --git a/backend/internal/buildhistory/artifacts_test.go b/backend/internal/buildhistory/artifacts_test.go deleted file mode 100644 index ded55e4..0000000 --- a/backend/internal/buildhistory/artifacts_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package buildhistory - -import ( - "testing" - - "github.com/enegalan/calf/backend/internal/runtime" -) - -func TestParseAttachmentRows(t *testing.T) { - rows := parseAttachmentRows(`TYPE=application/vnd.oci.image.index.v1+json|PLATFORM=|DIGEST=sha256:abc -TYPE=https://slsa.dev/provenance/v0.2|PLATFORM=|DIGEST=sha256:def -TYPE=application/vnd.oci.image.manifest.v1+json|PLATFORM=linux/arm64|DIGEST=sha256:ghi`) - - if len(rows) != 3 { - t.Fatalf("expected 3 rows, got %d", len(rows)) - } - - if rows[2].Platform != "linux/arm64" { - t.Fatalf("unexpected platform: %q", rows[2].Platform) - } -} - -func TestOrderBuildArtifacts(t *testing.T) { - ordered := orderBuildArtifacts([]runtime.BuildArtifact{ - {Name: "Provenance v1"}, - {Name: "application/vnd.oci.image.config.v1+json"}, - }) - - if len(ordered) != 2 { - t.Fatalf("expected 2 artifacts, got %d", len(ordered)) - } - - if ordered[0].Name != "application/vnd.oci.image.config.v1+json" { - t.Fatalf("unexpected first artifact: %q", ordered[0].Name) - } - if ordered[1].Name != "Provenance v1" { - t.Fatalf("unexpected second artifact: %q", ordered[1].Name) - } -} diff --git a/backend/internal/buildhistory/history_test.go b/backend/internal/buildhistory/history_test.go deleted file mode 100644 index 89b4f29..0000000 --- a/backend/internal/buildhistory/history_test.go +++ /dev/null @@ -1,56 +0,0 @@ -package buildhistory - -import ( - "testing" -) - -func TestParseDurationMs(t *testing.T) { - if got := ParseDurationMs("39.9s"); got < 39900 || got > 40000 { - t.Fatalf("expected ~39900ms, got %d", got) - } - - if got := ParseDurationMs("1m 1s"); got < 61000 || got > 62000 { - t.Fatalf("expected ~61000ms, got %d", got) - } - - if got := ParseDurationMs("1h5m10s"); got < 3910000 || got > 3920000 { - t.Fatalf("expected ~3910000ms, got %d", got) - } -} - -func TestMergeRowsSkipsDuplicates(t *testing.T) { - refs := map[string]struct{}{"abc123": {}} - rows := []Row{ - {Ref: "default/default/abc123", NameLower: "demo"}, - {Ref: "default/default/def456", NameLower: "other"}, - } - - imported := MergeRows(refs, rows) - if len(imported) != 1 { - t.Fatalf("expected 1 imported row, got %d", len(imported)) - } - if imported[0].HistoryID() != "def456" { - t.Fatalf("unexpected imported id %q", imported[0].HistoryID()) - } -} - -func TestParseBuildxHistoryJSON(t *testing.T) { - rows := parseRows([]byte(`{"cached_steps":1,"completed_at":"2026-07-04T01:10:03.861183581Z","completed_steps":5,"created_at":"2026-07-04T01:10:03.808406711Z","name":"p2p-lan-p2p-lan","ref":"default/default/szzsnkdslb4c2ees6aok7xln6","status":"Completed","total_steps":5}`)) - if len(rows) != 1 { - t.Fatalf("expected 1 row, got %d", len(rows)) - } - - row := rows[0] - if row.BuildName() != "p2p-lan-p2p-lan" { - t.Fatalf("unexpected name %q", row.BuildName()) - } - if row.HistoryID() != "szzsnkdslb4c2ees6aok7xln6" { - t.Fatalf("unexpected history id %q", row.HistoryID()) - } - if row.CachedSteps != 1 || row.TotalSteps != 5 { - t.Fatalf("unexpected step counts") - } - if row.BuildDurationMs() <= 0 { - t.Fatalf("expected positive duration") - } -} diff --git a/backend/internal/buildhistory/inspect_test.go b/backend/internal/buildhistory/inspect_test.go deleted file mode 100644 index 62327c6..0000000 --- a/backend/internal/buildhistory/inspect_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package buildhistory - -import "testing" - -func TestParseInspectDetail(t *testing.T) { - detail, err := parseInspectDetail(`{"Context":"/Users/egalan/git/toth","Dockerfile":"apps/api/Dockerfile"}`) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - if detail.Context != "/Users/egalan/git/toth" { - t.Fatalf("unexpected context: %q", detail.Context) - } - if detail.Dockerfile != "apps/api/Dockerfile" { - t.Fatalf("unexpected dockerfile: %q", detail.Dockerfile) - } -} - -func TestParseInspectDetailDefaultsDockerfile(t *testing.T) { - detail, err := parseInspectDetail(`{"Context":"/tmp/build"}`) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - if detail.Dockerfile != "Dockerfile" { - t.Fatalf("unexpected dockerfile: %q", detail.Dockerfile) - } -} - -func TestParseInspectDetailPreservesContextWithSpaces(t *testing.T) { - detail, err := parseInspectDetail(`{"Context":"/Users/egalan/git/my project","Dockerfile":"Dockerfile"}`) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - if detail.Context != "/Users/egalan/git/my project" { - t.Fatalf("unexpected context: %q", detail.Context) - } -} diff --git a/backend/internal/migration/compose_migration_test.go b/backend/internal/migration/compose_migration_test.go deleted file mode 100644 index e236449..0000000 --- a/backend/internal/migration/compose_migration_test.go +++ /dev/null @@ -1,161 +0,0 @@ -package migration - -import ( - "os" - "path/filepath" - "strings" - "testing" -) - -func TestGroupContainersByComposeProject(t *testing.T) { - inspects := []containerInspect{ - { - Name: "/typing-app-backend", - Config: struct { - Image string `json:"Image"` - Env []string `json:"Env"` - Cmd []string `json:"Cmd"` - Entrypoint []string `json:"Entrypoint"` - WorkingDir string `json:"WorkingDir"` - Hostname string `json:"Hostname"` - User string `json:"User"` - Labels map[string]string `json:"Labels"` - }{ - Image: "keycode-backend:latest", - Labels: map[string]string{ - composeProjectLabel: "keycode", - composeServiceLabel: "backend", - composeWorkingDirLabel: "/Users/test/keycode", - composeConfigFilesLabel: "/Users/test/keycode/docker-compose.yml", - }, - }, - }, - { - Name: "/standalone", - Config: struct { - Image string `json:"Image"` - Env []string `json:"Env"` - Cmd []string `json:"Cmd"` - Entrypoint []string `json:"Entrypoint"` - WorkingDir string `json:"WorkingDir"` - Hostname string `json:"Hostname"` - User string `json:"User"` - Labels map[string]string `json:"Labels"` - }{ - Image: "alpine:latest", - }, - }, - } - - groups, standalone := groupContainersByComposeProject(inspects, map[string]bool{ - "typing-app-backend": true, - "standalone": false, - }) - - if len(groups) != 1 { - t.Fatalf("expected 1 compose group, got %d", len(groups)) - } - - if groups[0].Name != "keycode" { - t.Fatalf("expected project keycode, got %q", groups[0].Name) - } - - if len(groups[0].Containers) != 1 { - t.Fatalf("expected 1 container in group, got %d", len(groups[0].Containers)) - } - - if len(standalone) != 1 || standalone[0].Name != "/standalone" { - t.Fatalf("expected standalone container, got %#v", standalone) - } -} - -func TestPatchComposeForMigration(t *testing.T) { - dir := t.TempDir() - composePath := filepath.Join(dir, "docker-compose.yml") - compose := `services: - backend: - build: - context: ./backend - container_name: typing-app-backend - frontend: - image: keycode-frontend:latest -` - if err := os.WriteFile(composePath, []byte(compose), 0o644); err != nil { - t.Fatal(err) - } - - if err := patchComposeForMigration(composePath, map[string]string{ - "backend": "keycode-backend:latest", - }); err != nil { - t.Fatal(err) - } - - patched, err := os.ReadFile(composePath) - if err != nil { - t.Fatal(err) - } - - content := string(patched) - if strings.Contains(content, "build:") { - t.Fatalf("expected build removed, got %s", content) - } - - if !strings.Contains(content, "image: keycode-backend:latest") { - t.Fatalf("expected backend image patched, got %s", content) - } -} - -func TestMigrationLabelsPreservesComposeMetadata(t *testing.T) { - inspect := containerInspect{ - Config: struct { - Image string `json:"Image"` - Env []string `json:"Env"` - Cmd []string `json:"Cmd"` - Entrypoint []string `json:"Entrypoint"` - WorkingDir string `json:"WorkingDir"` - Hostname string `json:"Hostname"` - User string `json:"User"` - Labels map[string]string `json:"Labels"` - }{ - Labels: map[string]string{ - composeProjectLabel: "keycode", - composeServiceLabel: "backend", - "empty": "", - }, - }, - } - - labels := migrationLabels(inspect) - if len(labels) != 2 { - t.Fatalf("expected 2 labels, got %d", len(labels)) - } - - if labels[0][0] != composeProjectLabel || labels[0][1] != "keycode" { - t.Fatalf("unexpected first label: %#v", labels[0]) - } -} - -func TestContainerBindMountsUsesVolumeName(t *testing.T) { - inspect := containerInspect{ - Mounts: []struct { - Type string `json:"Type"` - Name string `json:"Name"` - Source string `json:"Source"` - Destination string `json:"Destination"` - Mode string `json:"Mode"` - RW bool `json:"RW"` - }{ - { - Type: "volume", - Name: "keycode_mongodb_data", - Destination: "/data/db", - RW: true, - }, - }, - } - - binds := containerBindMounts(inspect) - if len(binds) != 1 || binds[0] != "keycode_mongodb_data:/data/db" { - t.Fatalf("unexpected binds: %#v", binds) - } -} diff --git a/backend/internal/migration/disk_test.go b/backend/internal/migration/disk_test.go deleted file mode 100644 index 46eb9c0..0000000 --- a/backend/internal/migration/disk_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package migration - -import "testing" - -func TestParseHumanSize(t *testing.T) { - tests := []struct { - input string - gib float64 - mib float64 - }{ - {"17.82GB", 17.82, 0}, - {"10.12GB", 10.12, 0}, - {"227.8MB", 0, 227.8}, - {"0B", 0, 0}, - } - - for _, test := range tests { - got, err := parseHumanSize(test.input) - if err != nil { - t.Fatalf("parseHumanSize(%q) error: %v", test.input, err) - } - - var want int64 - if test.gib > 0 { - want = int64(test.gib * 1024 * 1024 * 1024) - } else if test.mib > 0 { - want = int64(test.mib * 1024 * 1024) - } - - diff := got - want - if diff < 0 { - diff = -diff - } - if diff > 1024 { - t.Fatalf("parseHumanSize(%q) = %d, want ~%d", test.input, got, want) - } - } -} - -func TestParseAvailBytesIgnoresLimaWarnings(t *testing.T) { - output := `time="2026-07-02T16:27:05+02:00" level=warning msg="Non-strict YAML detected" -Avail -107374182400` - - got, err := parseAvailBytes([]byte(output)) - if err != nil { - t.Fatalf("parseAvailBytes() error: %v", err) - } - - if got != 107374182400 { - t.Fatalf("parseAvailBytes() = %d, want 107374182400", got) - } -} diff --git a/backend/internal/runtime/inspect_decode_test.go b/backend/internal/runtime/inspect_decode_test.go deleted file mode 100644 index e81078a..0000000 --- a/backend/internal/runtime/inspect_decode_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package runtime - -import "testing" - -func TestDecodeInspectDocumentsSingleArray(t *testing.T) { - output := []byte(`[{"Name":"a"},{"Name":"b"}]`) - - rows, err := decodeInspectDocuments[map[string]string](output) - if err != nil { - t.Fatalf("decodeInspectDocuments() error: %v", err) - } - - if len(rows) != 2 { - t.Fatalf("expected 2 rows, got %d", len(rows)) - } -} - -func TestDecodeInspectDocumentsConcatenatedArrays(t *testing.T) { - output := []byte(`[{"Name":"a"}][{"Name":"b"}]`) - - rows, err := decodeInspectDocuments[map[string]string](output) - if err != nil { - t.Fatalf("decodeInspectDocuments() error: %v", err) - } - - if len(rows) != 2 { - t.Fatalf("expected 2 rows, got %d", len(rows)) - } -} - -func TestDecodeInspectDocumentsSkipsLogLines(t *testing.T) { - output := []byte("time=2026-07-04T00:37:02 level=info msg=inspect\n[{\"Name\":\"a\"}]\ntime=2026-07-04T00:37:03 level=info msg=done\n[{\"Name\":\"b\"}]") - - rows, err := decodeInspectDocuments[map[string]string](output) - if err != nil { - t.Fatalf("decodeInspectDocuments() error: %v", err) - } - - if len(rows) != 2 { - t.Fatalf("expected 2 rows, got %d", len(rows)) - } -} diff --git a/backend/test/buildhistory/history_test.go b/backend/test/buildhistory/history_test.go new file mode 100644 index 0000000..0d00943 --- /dev/null +++ b/backend/test/buildhistory/history_test.go @@ -0,0 +1,37 @@ +package buildhistory_test + +import ( + "testing" + + "github.com/enegalan/calf/backend/internal/buildhistory" +) + +func TestParseDurationMs(t *testing.T) { + if got := buildhistory.ParseDurationMs("39.9s"); got < 39900 || got > 40000 { + t.Fatalf("expected ~39900ms, got %d", got) + } + + if got := buildhistory.ParseDurationMs("1m 1s"); got < 61000 || got > 62000 { + t.Fatalf("expected ~61000ms, got %d", got) + } + + if got := buildhistory.ParseDurationMs("1h5m10s"); got < 3910000 || got > 3920000 { + t.Fatalf("expected ~3910000ms, got %d", got) + } +} + +func TestMergeRowsSkipsDuplicates(t *testing.T) { + refs := map[string]struct{}{"abc123": {}} + rows := []buildhistory.Row{ + {Ref: "default/default/abc123", NameLower: "demo"}, + {Ref: "default/default/def456", NameLower: "other"}, + } + + imported := buildhistory.MergeRows(refs, rows) + if len(imported) != 1 { + t.Fatalf("expected 1 imported row, got %d", len(imported)) + } + if imported[0].HistoryID() != "def456" { + t.Fatalf("unexpected imported id %q", imported[0].HistoryID()) + } +} diff --git a/backend/internal/dockercli/context_test.go b/backend/test/dockercli/context_test.go similarity index 54% rename from backend/internal/dockercli/context_test.go rename to backend/test/dockercli/context_test.go index 159e314..1b43114 100644 --- a/backend/internal/dockercli/context_test.go +++ b/backend/test/dockercli/context_test.go @@ -1,17 +1,24 @@ -package dockercli +package dockercli_test import ( "os" "path/filepath" "testing" + + "github.com/enegalan/calf/backend/internal/dockercli" ) func TestReadCurrentContextMissingFile(t *testing.T) { dir := t.TempDir() t.Setenv("HOME", dir) - if got := readCurrentContext(); got != "" { - t.Fatalf("expected empty context, got %q", got) + status, err := dockercli.StatusFor("", false) + if err != nil { + t.Fatalf("StatusFor() error: %v", err) + } + + if status.CurrentContext != "" { + t.Fatalf("expected empty context, got %q", status.CurrentContext) } } @@ -29,7 +36,12 @@ func TestReadCurrentContextFromConfig(t *testing.T) { t.Fatalf("WriteFile() error: %v", err) } - if got := readCurrentContext(); got != "calf" { - t.Fatalf("expected calf, got %q", got) + status, err := dockercli.StatusFor("", false) + if err != nil { + t.Fatalf("StatusFor() error: %v", err) + } + + if status.CurrentContext != "calf" { + t.Fatalf("expected calf, got %q", status.CurrentContext) } } diff --git a/backend/internal/oauth/dockerhub/device_test.go b/backend/test/dockerhub/device_test.go similarity index 100% rename from backend/internal/oauth/dockerhub/device_test.go rename to backend/test/dockerhub/device_test.go diff --git a/backend/internal/runtime/build_enrich_test.go b/backend/test/runtime/build_enrich_test.go similarity index 76% rename from backend/internal/runtime/build_enrich_test.go rename to backend/test/runtime/build_enrich_test.go index 6556858..8ba5917 100644 --- a/backend/internal/runtime/build_enrich_test.go +++ b/backend/test/runtime/build_enrich_test.go @@ -1,9 +1,11 @@ -package runtime +package runtime_test import ( "os" "path/filepath" "testing" + + "github.com/enegalan/calf/backend/internal/runtime" ) func TestNormalizeDockerfilePathUsesExistingRelativePath(t *testing.T) { @@ -13,7 +15,7 @@ func TestNormalizeDockerfilePathUsesExistingRelativePath(t *testing.T) { t.Fatalf("WriteFile() error: %v", err) } - got := NormalizeDockerfilePath(dir, "examples/hello-world/Dockerfile") + got := runtime.NormalizeDockerfilePath(dir, "examples/hello-world/Dockerfile") if got != "Dockerfile" { t.Fatalf("expected Dockerfile, got %q", got) } @@ -23,7 +25,7 @@ func TestParseImageRefFromBuildLog(t *testing.T) { rawLog := `#5 naming to docker.io/library/calf-sync-test:latest done #6 naming to docker.io/library/toth-api:latest 0.0s done` - got := ParseImageRefFromBuildLog(rawLog) + got := runtime.ParseImageRefFromBuildLog(rawLog) if got != "toth-api:latest" { t.Fatalf("expected toth-api:latest, got %q", got) } diff --git a/backend/internal/runtime/build_parser_test.go b/backend/test/runtime/build_parser_test.go similarity index 76% rename from backend/internal/runtime/build_parser_test.go rename to backend/test/runtime/build_parser_test.go index d1aa5d9..614e796 100644 --- a/backend/internal/runtime/build_parser_test.go +++ b/backend/test/runtime/build_parser_test.go @@ -1,10 +1,12 @@ -package runtime +package runtime_test import ( "os" "path/filepath" "strings" "testing" + + "github.com/enegalan/calf/backend/internal/runtime" ) func TestParseBuildOutputSteps(t *testing.T) { @@ -28,7 +30,7 @@ func TestParseBuildOutputSteps(t *testing.T) { #6 DONE 0.3s ` - result := ParseBuildOutput(output) + result := runtime.ParseBuildOutput(output) if len(result.Steps) < 4 { t.Fatalf("expected at least 4 steps, got %d", len(result.Steps)) @@ -74,7 +76,7 @@ func TestParseBuildOutputSteps(t *testing.T) { } func TestApplyBuildLogsPopulatesTiming(t *testing.T) { - build := Build{Steps: []BuildStep{}} + build := runtime.Build{Steps: []runtime.BuildStep{}} logs := `#1 [internal] load build definition from Dockerfile #1 DONE 0.0s @@ -85,7 +87,7 @@ func TestApplyBuildLogsPopulatesTiming(t *testing.T) { #3 DONE 1.2s ` - ApplyBuildLogs(&build, logs) + runtime.ApplyBuildLogs(&build, logs) if len(build.Steps) != 3 { t.Fatalf("expected 3 steps, got %d", len(build.Steps)) @@ -100,31 +102,13 @@ func TestApplyBuildLogsPopulatesTiming(t *testing.T) { } } -func TestParseDockerfileDependencies(t *testing.T) { - dir := t.TempDir() - dockerfile := filepath.Join(dir, "Dockerfile") - content := "FROM php:8.4-fpm-alpine\nFROM node:20 AS assets\nCOPY . .\n" - if err := os.WriteFile(dockerfile, []byte(content), 0o644); err != nil { - t.Fatalf("WriteFile() error: %v", err) - } - - deps := parseDockerfileDependencies(dir, "Dockerfile", "linux/arm64") - if len(deps) != 2 { - t.Fatalf("expected 2 dependencies, got %d", len(deps)) - } - - if deps[0].Source != "php:8.4-fpm-alpine" { - t.Fatalf("unexpected first dependency: %q", deps[0].Source) - } -} - func TestReadBuildSourceRejectsTraversal(t *testing.T) { dir := t.TempDir() if err := os.WriteFile(filepath.Join(dir, "Dockerfile"), []byte("FROM alpine"), 0o644); err != nil { t.Fatalf("WriteFile() error: %v", err) } - _, err := ReadBuildSource(dir, "../Dockerfile", "linux/arm64") + _, err := runtime.ReadBuildSource(dir, "../Dockerfile", "linux/arm64") if err == nil { t.Fatalf("expected path traversal to be rejected") } diff --git a/backend/internal/volumeexport/name_pattern_test.go b/backend/test/volumeexport/name_pattern_test.go similarity index 61% rename from backend/internal/volumeexport/name_pattern_test.go rename to backend/test/volumeexport/name_pattern_test.go index e87f9de..2c062b2 100644 --- a/backend/internal/volumeexport/name_pattern_test.go +++ b/backend/test/volumeexport/name_pattern_test.go @@ -1,44 +1,46 @@ -package volumeexport +package volumeexport_test import ( "strings" "testing" "time" + + "github.com/enegalan/calf/backend/internal/volumeexport" ) func TestExpandExportNamePattern(t *testing.T) { runTime := time.Date(2026, 7, 5, 14, 30, 0, 0, time.UTC) - expanded := ExpandExportNamePattern("{volume}-{timestamp}.tar.gz", "my/vol", runTime) + expanded := volumeexport.ExpandExportNamePattern("{volume}-{timestamp}.tar.gz", "my/vol", runTime) if expanded != "my_vol-20260705-143000.tar.gz" { t.Fatalf("unexpected expansion: %q", expanded) } - static := ExpandExportNamePattern("backup.tar.gz", "my/vol", runTime) + static := volumeexport.ExpandExportNamePattern("backup.tar.gz", "my/vol", runTime) if static != "backup.tar.gz" { t.Fatalf("static pattern should be preserved, got %q", static) } } func TestHasUniqueNameToken(t *testing.T) { - if !HasUniqueNameToken("{volume}-{timestamp}.tar.gz") { + if !volumeexport.HasUniqueNameToken("{volume}-{timestamp}.tar.gz") { t.Fatal("expected timestamp token to be detected") } - if HasUniqueNameToken("backup.tar.gz") { + if volumeexport.HasUniqueNameToken("backup.tar.gz") { t.Fatal("expected static pattern to have no unique token") } } func TestResolveScheduledExportNames(t *testing.T) { runTime := time.Date(2026, 7, 5, 8, 0, 0, 0, time.UTC) - schedule := Schedule{ + schedule := volumeexport.Schedule{ Volume: "calf-data", - Type: TypeLocalFile, + Type: volumeexport.TypeLocalFile, FileName: "{volume}-{timestamp}.tar.gz", } - fileName, imageRef := ResolveScheduledExportNames(schedule, runTime) + fileName, imageRef := volumeexport.ResolveScheduledExportNames(schedule, runTime) if imageRef != "" { t.Fatalf("expected empty image ref, got %q", imageRef) } @@ -47,13 +49,13 @@ func TestResolveScheduledExportNames(t *testing.T) { t.Fatalf("unexpected resolved file name: %q", fileName) } - staticSchedule := Schedule{ + staticSchedule := volumeexport.Schedule{ Volume: "calf-data", - Type: TypeLocalFile, + Type: volumeexport.TypeLocalFile, FileName: "backup.tar.gz", } - staticName, _ := ResolveScheduledExportNames(staticSchedule, runTime) + staticName, _ := volumeexport.ResolveScheduledExportNames(staticSchedule, runTime) if staticName != "backup.tar.gz" { t.Fatalf("static file name should be preserved, got %q", staticName) } @@ -62,7 +64,7 @@ func TestResolveScheduledExportNames(t *testing.T) { func TestExpandExportImageRefPattern(t *testing.T) { runTime := time.Date(2026, 7, 5, 14, 30, 0, 0, time.UTC) - expanded := ExpandExportImageRefPattern("{volume}-backup:{timestamp}", "My/Vol", runTime) + expanded := volumeexport.ExpandExportImageRefPattern("{volume}-backup:{timestamp}", "My/Vol", runTime) if expanded != "my_vol-backup:20260705-143000" { t.Fatalf("unexpected image ref expansion: %q", expanded) } diff --git a/backend/internal/volumeexport/schedule_timing_test.go b/backend/test/volumeexport/schedule_timing_test.go similarity index 70% rename from backend/internal/volumeexport/schedule_timing_test.go rename to backend/test/volumeexport/schedule_timing_test.go index fc32810..eb56374 100644 --- a/backend/internal/volumeexport/schedule_timing_test.go +++ b/backend/test/volumeexport/schedule_timing_test.go @@ -1,23 +1,25 @@ -package volumeexport +package volumeexport_test import ( "testing" "time" + + "github.com/enegalan/calf/backend/internal/volumeexport" ) func TestComputeNextRunCronSameDay(t *testing.T) { loc := time.Local now := time.Date(2026, 7, 5, 10, 0, 0, 0, loc) - schedule := Schedule{ + schedule := volumeexport.Schedule{ Enabled: true, DaysOfWeek: []int{int(now.Weekday())}, Times: []string{"15:04", "18:30"}, - Type: TypeLocalFile, + Type: volumeexport.TypeLocalFile, Folder: "/tmp", } - next, err := ComputeNextRun(schedule, now) + next, err := volumeexport.ComputeNextRun(schedule, now) if err != nil { t.Fatalf("ComputeNextRun() error: %v", err) } @@ -32,15 +34,15 @@ func TestComputeNextRunCronRollsToNextSelectedDay(t *testing.T) { loc := time.Local now := time.Date(2026, 7, 5, 20, 0, 0, 0, loc) - schedule := Schedule{ + schedule := volumeexport.Schedule{ Enabled: true, DaysOfWeek: []int{int(time.Monday)}, Times: []string{"09:30"}, - Type: TypeLocalFile, + Type: volumeexport.TypeLocalFile, Folder: "/tmp", } - next, err := ComputeNextRun(schedule, now) + next, err := volumeexport.ComputeNextRun(schedule, now) if err != nil { t.Fatalf("ComputeNextRun() error: %v", err) } @@ -54,7 +56,7 @@ func TestComputeNextRunCronRollsToNextSelectedDay(t *testing.T) { func TestComputeNextRunDisabledReturnsZero(t *testing.T) { now := time.Now() - next, err := ComputeNextRun(Schedule{Enabled: false}, now) + next, err := volumeexport.ComputeNextRun(volumeexport.Schedule{Enabled: false}, now) if err != nil { t.Fatalf("ComputeNextRun() error: %v", err) } @@ -65,12 +67,12 @@ func TestComputeNextRunDisabledReturnsZero(t *testing.T) { } func TestNormalizeScheduleLegacyDaily(t *testing.T) { - schedule := Schedule{ - Frequency: FrequencyDaily, + schedule := volumeexport.Schedule{ + Frequency: volumeexport.FrequencyDaily, TimeOfDay: "03:00", } - NormalizeSchedule(&schedule) + volumeexport.NormalizeSchedule(&schedule) if len(schedule.DayTimes) != 7 { t.Fatalf("expected 7 day entries, got %d", len(schedule.DayTimes)) @@ -87,17 +89,17 @@ func TestComputeNextRunPerDayDifferentTimes(t *testing.T) { loc := time.Local now := time.Date(2026, 7, 5, 10, 0, 0, 0, loc) - schedule := Schedule{ + schedule := volumeexport.Schedule{ Enabled: true, - DayTimes: []DayTimeSchedule{ + DayTimes: []volumeexport.DayTimeSchedule{ {Day: int(time.Monday), Times: []string{"15:04", "18:30"}}, {Day: int(time.Tuesday), Times: []string{"09:30"}}, }, - Type: TypeLocalFile, + Type: volumeexport.TypeLocalFile, Folder: "/tmp", } - next, err := ComputeNextRun(schedule, now) + next, err := volumeexport.ComputeNextRun(schedule, now) if err != nil { t.Fatalf("ComputeNextRun() error: %v", err) } @@ -112,16 +114,16 @@ func TestComputeNextRunIncludesCurrentMinuteSlot(t *testing.T) { loc := time.Local now := time.Date(2026, 7, 5, 17, 11, 30, 0, loc) - schedule := Schedule{ + schedule := volumeexport.Schedule{ Enabled: true, - DayTimes: []DayTimeSchedule{ + DayTimes: []volumeexport.DayTimeSchedule{ {Day: int(now.Weekday()), Times: []string{"17:11", "17:20"}}, }, - Type: TypeLocalFile, + Type: volumeexport.TypeLocalFile, Folder: "/tmp", } - next, err := ComputeNextRun(schedule, now) + next, err := volumeexport.ComputeNextRun(schedule, now) if err != nil { t.Fatalf("ComputeNextRun() error: %v", err) } @@ -136,16 +138,16 @@ func TestComputeNextRunAfterRunSkipsCurrentSlot(t *testing.T) { loc := time.Local now := time.Date(2026, 7, 5, 17, 11, 30, 0, loc) - schedule := Schedule{ + schedule := volumeexport.Schedule{ Enabled: true, - DayTimes: []DayTimeSchedule{ + DayTimes: []volumeexport.DayTimeSchedule{ {Day: int(now.Weekday()), Times: []string{"17:11", "17:20"}}, }, - Type: TypeLocalFile, + Type: volumeexport.TypeLocalFile, Folder: "/tmp", } - next, err := ComputeNextRunAfterRun(schedule, now) + next, err := volumeexport.ComputeNextRunAfterRun(schedule, now) if err != nil { t.Fatalf("ComputeNextRunAfterRun() error: %v", err) } @@ -160,19 +162,19 @@ func TestScheduleDueWithinGraceWindow(t *testing.T) { loc := time.Local nextRun := time.Date(2026, 7, 5, 17, 11, 0, 0, loc).UTC().Format(time.RFC3339) - if !ScheduleDue(nextRun, time.Date(2026, 7, 5, 17, 11, 45, 0, loc)) { + if !volumeexport.ScheduleDue(nextRun, time.Date(2026, 7, 5, 17, 11, 45, 0, loc)) { t.Fatalf("expected schedule to be due inside grace window") } - if !ScheduleDue(nextRun, time.Date(2026, 7, 5, 17, 12, 0, 0, loc)) { + if !volumeexport.ScheduleDue(nextRun, time.Date(2026, 7, 5, 17, 12, 0, 0, loc)) { t.Fatalf("expected schedule to be due at grace boundary") } - if ScheduleDue(nextRun, time.Date(2026, 7, 5, 17, 10, 59, 0, loc)) { + if volumeexport.ScheduleDue(nextRun, time.Date(2026, 7, 5, 17, 10, 59, 0, loc)) { t.Fatalf("expected schedule not to be due before scheduled minute") } - if ScheduleDue(nextRun, time.Date(2026, 7, 5, 17, 12, 1, 0, loc)) { + if volumeexport.ScheduleDue(nextRun, time.Date(2026, 7, 5, 17, 12, 1, 0, loc)) { t.Fatalf("expected schedule not to be due after grace window") } } From 3206244fcdea01e256283387be12a56bec71a7a9 Mon Sep 17 00:00:00 2001 From: Eneko Galan Date: Thu, 9 Jul 2026 18:28:56 +0200 Subject: [PATCH 2/2] fix(dockercli): update StatusFor function to include CurrentContext in status - Added CurrentContext to the status returned by the StatusFor function, improving context handling. - Removed redundant assignment of CurrentContext to streamline the code. --- backend/internal/dockercli/context.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/internal/dockercli/context.go b/backend/internal/dockercli/context.go index cceb8c8..5284934 100644 --- a/backend/internal/dockercli/context.go +++ b/backend/internal/dockercli/context.go @@ -30,8 +30,9 @@ type dockerConfig struct { func StatusFor(socket string, managed bool) (Status, error) { status := Status{ - Managed: managed, - Socket: socket, + Managed: managed, + Socket: socket, + CurrentContext: readCurrentContext(), } if _, err := exec.LookPath("docker"); err != nil { @@ -39,7 +40,6 @@ func StatusFor(socket string, managed bool) (Status, error) { } status.Available = true - status.CurrentContext = readCurrentContext() status.CalfActive = status.CurrentContext == ContextName ctx, cancel := context.WithTimeout(context.Background(), cliTimeout)