133 lines
5.0 KiB
Go
133 lines
5.0 KiB
Go
package backup
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const testRevision = "0123456789abcdef0123456789abcdef01234567"
|
|
|
|
func TestManifestFinalizationNormalizesAndOrdersEntriesDeterministically(t *testing.T) {
|
|
manifest := Manifest{
|
|
SchemaVersion: CurrentSchemaVersion,
|
|
InstallationID: "local-dev",
|
|
CreatedAt: time.Date(2026, 8, 16, 10, 11, 12, 123, time.FixedZone("test", 2*60*60)),
|
|
SourceRevision: testRevision,
|
|
ComposeProject: "thothii-test",
|
|
Entries: []Entry{
|
|
{Path: `volumes\sessions.tar`, Kind: EntryVolume, Owner: "volume:sessions", SHA256: DigestBytes([]byte("sessions")), Size: 8, Archived: true},
|
|
{Path: "configuration/pi/models.json", Kind: EntryFile, Owner: "pi", SHA256: DigestBytes([]byte("models")), Size: 6, Archived: true},
|
|
},
|
|
Images: []ImageIdentity{
|
|
{Service: "frontend", Reference: "frontend:test", ID: "sha256:front"},
|
|
{Service: "core", Reference: "core:test", ID: "sha256:core"},
|
|
},
|
|
Volumes: []VolumeMetadata{
|
|
{LogicalName: "sessions", Name: "thothii-test_sessions", Driver: "local"},
|
|
{LogicalName: "pi-state", Name: "thothii-test_pi-state", Driver: "local"},
|
|
},
|
|
}
|
|
|
|
if err := manifest.Finalize(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, want := manifest.CreatedAt, time.Date(2026, 8, 16, 8, 11, 12, 123, time.UTC); !got.Equal(want) || got.Location() != time.UTC {
|
|
t.Fatalf("CreatedAt = %v (%v), want %v UTC", got, got.Location(), want)
|
|
}
|
|
if got := []string{manifest.Entries[0].Path, manifest.Entries[1].Path}; got[0] != "configuration/pi/models.json" || got[1] != "volumes/sessions.tar" {
|
|
t.Fatalf("entry order = %v", got)
|
|
}
|
|
if manifest.Images[0].Service != "core" || manifest.Volumes[0].LogicalName != "pi-state" {
|
|
t.Fatalf("metadata was not deterministically ordered: images=%v volumes=%v", manifest.Images, manifest.Volumes)
|
|
}
|
|
first, err := manifest.JSON()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
second, err := manifest.JSON()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(first) != string(second) {
|
|
t.Fatalf("manifest encoding is not deterministic:\n%s\n%s", first, second)
|
|
}
|
|
}
|
|
|
|
func TestManifestRejectsUnsafePathsInvalidChecksumsAndUnsupportedSchemas(t *testing.T) {
|
|
valid := Manifest{
|
|
SchemaVersion: CurrentSchemaVersion,
|
|
InstallationID: "local-dev",
|
|
CreatedAt: time.Now().UTC(),
|
|
SourceRevision: testRevision,
|
|
ComposeProject: "thothii-test",
|
|
Entries: []Entry{{Path: "configuration/operator.env", Kind: EntryFile, Owner: "installation", SHA256: DigestBytes(nil), Archived: true}},
|
|
}
|
|
|
|
for _, mutate := range []func(*Manifest){
|
|
func(value *Manifest) { value.SchemaVersion = CurrentSchemaVersion + 1 },
|
|
func(value *Manifest) { value.Entries[0].Path = "../outside" },
|
|
func(value *Manifest) { value.Entries[0].Path = "/absolute" },
|
|
func(value *Manifest) { value.Entries[0].SHA256 = "sha256:not-a-digest" },
|
|
func(value *Manifest) { value.Entries = append(value.Entries, value.Entries[0]) },
|
|
} {
|
|
candidate := valid
|
|
candidate.Entries = append([]Entry(nil), valid.Entries...)
|
|
mutate(&candidate)
|
|
if err := candidate.Finalize(); err == nil {
|
|
t.Fatalf("Finalize() accepted invalid manifest: %#v", candidate)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestManifestSecretMarkerRequiresIncludedExternalSecretPayload(t *testing.T) {
|
|
base := Manifest{
|
|
SchemaVersion: CurrentSchemaVersion,
|
|
InstallationID: "local-dev",
|
|
CreatedAt: time.Now().UTC(),
|
|
SourceRevision: testRevision,
|
|
ComposeProject: "thothii-test",
|
|
Entries: []Entry{
|
|
{Path: "volumes/workspace-secrets.tar", Kind: EntryVolume, Owner: "volume:workspace-secrets", SHA256: DigestBytes([]byte("managed")), Size: 7, Archived: true, Sensitive: true},
|
|
{Path: "external-secrets/000", Kind: EntrySecretReference, Owner: "external-secret", SHA256: DigestBytes([]byte("external")), Size: 8, Archived: false, Sensitive: true, SourcePath: "/protected/secret"},
|
|
},
|
|
}
|
|
if err := base.Finalize(); err != nil {
|
|
t.Fatalf("managed workspace secrets and an excluded external reference must be valid: %v", err)
|
|
}
|
|
|
|
base.IncludesSecrets = true
|
|
if err := base.Finalize(); err == nil || !strings.Contains(err.Error(), "external secret") {
|
|
t.Fatalf("Finalize() error = %v, want missing included external secret", err)
|
|
}
|
|
for index := range base.Entries {
|
|
if base.Entries[index].Kind == EntrySecretReference {
|
|
base.Entries[index].Kind = EntryExternalSecret
|
|
base.Entries[index].Archived = true
|
|
base.Entries[index].Path = "external-secrets/000-secret"
|
|
}
|
|
}
|
|
if err := base.Finalize(); err != nil {
|
|
t.Fatalf("included external secret marker was rejected: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDecodeManifestRejectsASecondDocument(t *testing.T) {
|
|
manifest := Manifest{
|
|
SchemaVersion: CurrentSchemaVersion,
|
|
InstallationID: "local-dev",
|
|
CreatedAt: time.Date(2026, 8, 16, 8, 11, 12, 0, time.UTC),
|
|
SourceRevision: testRevision,
|
|
ComposeProject: "thothii-test",
|
|
}
|
|
encoded, err := json.Marshal(manifest)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
withSecondDocument := append(append([]byte(nil), encoded...), append([]byte(" \n"), encoded...)...)
|
|
if _, err := DecodeManifest(withSecondDocument); err == nil {
|
|
t.Fatal("DecodeManifest() accepted a second JSON document")
|
|
}
|
|
}
|