//go:build linux package hostfiles import ( "context" "crypto/sha256" "encoding/hex" "os" "path/filepath" "reflect" "testing" "github.com/carlosframework/platform/internal/release" ) // Astra round 2 finding 9: after a good install, a truncated env, another // key, a directory or a symlink in its place is not present. A manifest // without the env entry wants no env file only by the caller's evidence // (Task 8c: absence is never evidence). func TestPresentReadsTheEnvBack(t *testing.T) { env := []byte("CARLOS_ADMIN_TOKEN=t\nCARLOS_INSTANCE_KEY=k1\n") sum := sha256.Sum256(env) m := Manifest{V: ManifestVersion, Entries: map[string]Entry{}, Platform: map[string]Entry{EnvItem: {Object: "o", SHA256: hex.EncodeToString(sum[:]), Size: int64(len(env)), Seq: 1}}} for name, tc := range map[string]struct { make func(path string) error want bool }{ "the published bytes": {func(p string) error { return os.WriteFile(p, env, 0o600) }, true}, "truncated": {func(p string) error { return os.WriteFile(p, env[:10], 0o600) }, false}, "another key, same size": {func(p string) error { other := append([]byte(nil), env...) other[len(other)-2] = '2' return os.WriteFile(p, other, 0o600) }, false}, "a directory": {func(p string) error { return os.Mkdir(p, 0o700) }, false}, "a symlink to the bytes": {func(p string) error { real := p + ".real" if err := os.WriteFile(real, env, 0o600); err != nil { return err } return os.Symlink(real, p) }, false}, "absent": {func(string) error { return nil }, false}, } { t.Run(name, func(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "shop.example.env") if err := tc.make(path); err != nil { t.Fatal(err) } if ok, err := Present(t.TempDir(), path, m, false); err != nil || ok != tc.want { t.Fatalf("Present = %v, %v; want %v", ok, err, tc.want) } }) } // Production never writes a platform tombstone: StagePlatformDelete // drops the item. A manifest without the env entry is what a deleted // env file and one never published both publish. none := m none.Platform = map[string]Entry{} dir := t.TempDir() path := filepath.Join(dir, "shop.example.env") if ok, err := Present(dir, path, none, false); err != nil || ok { t.Fatalf("no env entry and no evidence read as no env file: %v %v", ok, err) } if ok, err := Present(dir, path, none, true); err != nil || !ok { t.Fatalf("evidenced no env file, none there: %v %v", ok, err) } if err := os.WriteFile(path, env, 0o600); err != nil { t.Fatal(err) } if ok, _ := Present(dir, path, none, true); ok { t.Fatal("evidenced no env file accepted an env file") } // The evidence never overrides a live entry, and a tombstone (which no // publisher writes) is never present. if ok, err := Present(dir, path, m, true); err != nil || !ok { t.Fatalf("a live entry with its bytes: %v %v", ok, err) } if err := os.Remove(path); err != nil { t.Fatal(err) } if ok, _ := Present(dir, path, m, true); ok { t.Fatal("the evidence excused a live entry's missing env file") } tomb := m tomb.Platform = map[string]Entry{EnvItem: {Deleted: true, Seq: 2}} for _, evidenced := range []bool{false, true} { if ok, err := Present(dir, path, tomb, evidenced); err != nil || ok { t.Fatalf("a tombstone read as no env file (evidence %v): %v %v", evidenced, ok, err) } } } // Ruling 52: Present reads every live entry back at its size and digest; // deleted, torn and skipped entries are not looked for; a missing, short, // changed or non-regular file is not present; extra files are not checked. func TestPresentReadsTheManifestBack(t *testing.T) { entry := func(body string) Entry { sum := sha256.Sum256([]byte(body)) return Entry{Object: "o", SHA256: hex.EncodeToString(sum[:]), Size: int64(len(body)), Seq: 1} } m := Manifest{V: ManifestVersion, Platform: map[string]Entry{}, Entries: map[string]Entry{ "public/a.txt": entry("alpha"), "b/c/d.bin": entry("delta"), "gone.txt": {Deleted: true, Seq: 2}, "torn.db": {Torn: true, Seq: 2}, "skipped/big": {Skipped: "too large", Seq: 2}, }} write := func(t *testing.T, dir string, files map[string]string) { t.Helper() for rel, body := range files { p := filepath.Join(dir, filepath.FromSlash(rel)) if err := os.MkdirAll(filepath.Dir(p), 0o700); err != nil { t.Fatal(err) } if err := os.WriteFile(p, []byte(body), 0o600); err != nil { t.Fatal(err) } } } env := filepath.Join(t.TempDir(), "shop.example.env") // no EnvItem: absence evidenced by the caller for name, tc := range map[string]struct { files map[string]string mutate func(t *testing.T, dir string) want bool }{ "every live entry": {map[string]string{"public/a.txt": "alpha", "b/c/d.bin": "delta"}, nil, true}, "plus files the app wrote": {map[string]string{"public/a.txt": "alpha", "b/c/d.bin": "delta", "extra": "x"}, nil, true}, "one missing": {map[string]string{"public/a.txt": "alpha"}, nil, false}, "one short": {map[string]string{"public/a.txt": "alph", "b/c/d.bin": "delta"}, nil, false}, "same size, other bytes": {map[string]string{"public/a.txt": "alphA", "b/c/d.bin": "delta"}, nil, false}, "a directory in its place": {map[string]string{"b/c/d.bin": "delta"}, func(t *testing.T, dir string) { if err := os.MkdirAll(filepath.Join(dir, "public", "a.txt"), 0o700); err != nil { t.Fatal(err) } }, false}, } { t.Run(name, func(t *testing.T) { dir := t.TempDir() write(t, dir, tc.files) if tc.mutate != nil { tc.mutate(t, dir) } if ok, err := Present(dir, env, m, true); err != nil || ok != tc.want { t.Fatalf("Present = %v, %v; want %v", ok, err, tc.want) } }) } dir := t.TempDir() write(t, dir, map[string]string{"public/a.txt": "alpha", "b/c/d.bin": "delta"}) if err := os.WriteFile(env, []byte("X=1\n"), 0o600); err != nil { t.Fatal(err) } if ok, err := Present(dir, env, m, true); err != nil || ok { t.Fatalf("an env file with no EnvItem was accepted: %v %v", ok, err) } } // Stage B final review A1 (Astra 1): Restore resolves a live file and a // live entry beneath it by sequence and leaves the loser unwritten; // Present accepts exactly that, in both directions, through the one rule // (superseded). Without it VerifyLocal answers false forever and // EnsureLocal repairs on every pass. func TestPresentAcceptsWhatRestoreWroteForOverlappingEntries(t *testing.T) { ctx := context.Background() for name, tc := range map[string]struct { stage [][2]string // in order: the later one is the newer winner string loser string }{ "the file is newer than the entry beneath it": {[][2]string{{"a/b", "child"}, {"a", "file"}}, "a", "a/b"}, "the entry beneath is newer than the file": {[][2]string{{"a", "file"}, {"a/b", "child"}}, "a/b", "a"}, } { t.Run(name, func(t *testing.T) { s := release.NewDirStore(t.TempDir()) p := open(t, s) for _, st := range tc.stage { stage(t, p, st[0], st[1]) } commit(t, p) dir := t.TempDir() m, st, err := Restore(ctx, s, pfx, dir, nil) if err != nil || st.Written != 1 || !reflect.DeepEqual(st.Superseded, []string{tc.loser}) { t.Fatalf("Restore: written %d, superseded %v, %v", st.Written, st.Superseded, err) } env := filepath.Join(t.TempDir(), "shop.example.env") if ok, err := Present(dir, env, m, true); err != nil || !ok { t.Fatalf("Present after Restore = %v, %v; want true", ok, err) } // The winner is still checked: without it the install is not whole. if err := os.RemoveAll(filepath.Join(dir, filepath.FromSlash(tc.winner))); err != nil { t.Fatal(err) } if ok, err := Present(dir, env, m, true); err != nil || ok { t.Fatalf("Present without %s = %v, %v; want false", tc.winner, ok, err) } }) } }