|
| 1 | +// Copyright Cozystack Authors |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +package commands |
| 16 | + |
| 17 | +import ( |
| 18 | + "os" |
| 19 | + "path/filepath" |
| 20 | + "strings" |
| 21 | + "testing" |
| 22 | + |
| 23 | + "github.com/cozystack/talm/pkg/generated" |
| 24 | +) |
| 25 | + |
| 26 | +// writeVendoredTalmLibrary materializes a project's charts/talm/ tree from |
| 27 | +// the binary's embedded library, stamping version into the library |
| 28 | +// Chart.yaml so the result looks like a real `talm init` would produce. It |
| 29 | +// returns the project root. |
| 30 | +func writeVendoredTalmLibrary(t *testing.T, version string) string { |
| 31 | + t.Helper() |
| 32 | + |
| 33 | + files, err := generated.TalmLibraryFiles() |
| 34 | + if err != nil { |
| 35 | + t.Fatalf("TalmLibraryFiles: %v", err) |
| 36 | + } |
| 37 | + |
| 38 | + root := t.TempDir() |
| 39 | + for rel, content := range files { |
| 40 | + // TalmLibraryFiles normalizes Chart.yaml name/version to %s; fill |
| 41 | + // them back in (name, version) so the vendored copy mirrors init. |
| 42 | + if filepath.Base(rel) == "Chart.yaml" { |
| 43 | + content = strings.ReplaceAll(content, "name: %s", "name: talm") |
| 44 | + content = strings.ReplaceAll(content, "version: %s", "version: "+version) |
| 45 | + } |
| 46 | + |
| 47 | + dest := filepath.Join(root, "charts", "talm", filepath.FromSlash(rel)) |
| 48 | + if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil { |
| 49 | + t.Fatalf("mkdir %q: %v", dest, err) |
| 50 | + } |
| 51 | + if err := os.WriteFile(dest, []byte(content), 0o644); err != nil { |
| 52 | + t.Fatalf("write %q: %v", dest, err) |
| 53 | + } |
| 54 | + } |
| 55 | + |
| 56 | + return root |
| 57 | +} |
| 58 | + |
| 59 | +// TestCheckChartDrift_MatchingCopy_NoDrift pins the happy path: a project |
| 60 | +// whose vendored charts/talm/ matches the embedded library reports no |
| 61 | +// drift, even though its Chart.yaml carries a concrete version stamp. |
| 62 | +func TestCheckChartDrift_MatchingCopy_NoDrift(t *testing.T) { |
| 63 | + root := writeVendoredTalmLibrary(t, "0.30.0") |
| 64 | + |
| 65 | + drift, msg, err := CheckChartDrift(root, "0.30.0") |
| 66 | + if err != nil { |
| 67 | + t.Fatalf("CheckChartDrift: %v", err) |
| 68 | + } |
| 69 | + if drift { |
| 70 | + t.Errorf("reported drift for a matching vendored copy: %s", msg) |
| 71 | + } |
| 72 | +} |
| 73 | + |
| 74 | +// TestCheckChartDrift_VersionStampOnly_NoDrift is the core regression |
| 75 | +// guard. A project vendored by an older release carries an older version |
| 76 | +// stamp in charts/talm/Chart.yaml, but its helpers are byte-identical to |
| 77 | +// the running binary's. That MUST NOT be reported as drift — flagging a |
| 78 | +// pure version difference is exactly the false positive the version-number |
| 79 | +// comparison approach produced. |
| 80 | +func TestCheckChartDrift_VersionStampOnly_NoDrift(t *testing.T) { |
| 81 | + root := writeVendoredTalmLibrary(t, "0.1.0") |
| 82 | + |
| 83 | + drift, msg, err := CheckChartDrift(root, "0.30.0") |
| 84 | + if err != nil { |
| 85 | + t.Fatalf("CheckChartDrift: %v", err) |
| 86 | + } |
| 87 | + if drift { |
| 88 | + t.Errorf("reported drift for a version-only difference; this is a false positive: %s", msg) |
| 89 | + } |
| 90 | +} |
| 91 | + |
| 92 | +// TestCheckChartDrift_ContentChange_Drift pins detection: a vendored |
| 93 | +// helpers template that diverges from the embedded copy is real drift and |
| 94 | +// must be surfaced. |
| 95 | +func TestCheckChartDrift_ContentChange_Drift(t *testing.T) { |
| 96 | + root := writeVendoredTalmLibrary(t, "0.30.0") |
| 97 | + |
| 98 | + helpers := filepath.Join(root, "charts", "talm", "templates", "_helpers.tpl") |
| 99 | + data, err := os.ReadFile(helpers) |
| 100 | + if err != nil { |
| 101 | + t.Fatalf("read helpers: %v", err) |
| 102 | + } |
| 103 | + if err := os.WriteFile(helpers, append(data, []byte("\n{{- /* stale local edit */ -}}\n")...), 0o644); err != nil { |
| 104 | + t.Fatalf("write helpers: %v", err) |
| 105 | + } |
| 106 | + |
| 107 | + drift, msg, err := CheckChartDrift(root, "0.30.0") |
| 108 | + if err != nil { |
| 109 | + t.Fatalf("CheckChartDrift: %v", err) |
| 110 | + } |
| 111 | + if !drift { |
| 112 | + t.Error("did not detect a divergent vendored helpers template; real drift went unreported") |
| 113 | + } |
| 114 | + // The remediation must carry --preset: bare `talm init --update` cannot |
| 115 | + // resolve the preset from an init'd project's Chart.yaml and errors out |
| 116 | + // without re-vendoring, so pointing at it would send operators to a |
| 117 | + // command that does not clear the drift. |
| 118 | + if !strings.Contains(msg, "talm init --update --preset") { |
| 119 | + t.Errorf("drift message must point at `talm init --update --preset`; got %q", msg) |
| 120 | + } |
| 121 | +} |
| 122 | + |
| 123 | +// TestCheckChartDrift_VendoredTalmIsFile_ErrorsGracefully pins that a |
| 124 | +// charts/talm that exists as a file (not a directory) is surfaced as an |
| 125 | +// error rather than a panic or a spurious drift result. Callers |
| 126 | +// (evaluateChartDrift) downgrade this to a non-fatal warning, so a |
| 127 | +// malformed project never crashes a command. |
| 128 | +func TestCheckChartDrift_VendoredTalmIsFile_ErrorsGracefully(t *testing.T) { |
| 129 | + root := t.TempDir() |
| 130 | + if err := os.MkdirAll(filepath.Join(root, "charts"), 0o755); err != nil { |
| 131 | + t.Fatalf("mkdir: %v", err) |
| 132 | + } |
| 133 | + if err := os.WriteFile(filepath.Join(root, "charts", "talm"), []byte("not a directory"), 0o644); err != nil { |
| 134 | + t.Fatalf("write file: %v", err) |
| 135 | + } |
| 136 | + |
| 137 | + drift, _, err := CheckChartDrift(root, "0.30.0") |
| 138 | + if err == nil { |
| 139 | + t.Error("expected an error when charts/talm is a file, got nil") |
| 140 | + } |
| 141 | + if drift { |
| 142 | + t.Error("must not report drift when the vendored tree cannot be read") |
| 143 | + } |
| 144 | +} |
| 145 | + |
| 146 | +// TestCheckChartDrift_MissingVendoredDir_NoDriftNoError pins graceful |
| 147 | +// handling: a project without a charts/talm/ directory (nothing vendored |
| 148 | +// yet) yields no drift and no error, so the check never blocks a command |
| 149 | +// on a tree it cannot compare. |
| 150 | +func TestCheckChartDrift_MissingVendoredDir_NoDriftNoError(t *testing.T) { |
| 151 | + root := t.TempDir() |
| 152 | + |
| 153 | + drift, msg, err := CheckChartDrift(root, "0.30.0") |
| 154 | + if err != nil { |
| 155 | + t.Fatalf("CheckChartDrift returned an error for a missing vendored dir: %v", err) |
| 156 | + } |
| 157 | + if drift { |
| 158 | + t.Errorf("reported drift with no vendored library to compare: %s", msg) |
| 159 | + } |
| 160 | +} |
0 commit comments