package store import ( "bytes" "encoding/json" "errors" "linkport/internal/model" "strings" "testing" ) func backupAsset() model.Asset { return model.Asset{ID: "cluster-asset", Name: "cluster", Type: "kubeconfig", Content: `apiVersion: v1 kind: Config current-context: test clusters: - name: cluster cluster: server: https://cluster.invalid contexts: - name: test context: cluster: cluster user: user users: - name: user user: token: asset-private-token `} } func TestAssetBackupRoundTripAndRedaction(t *testing.T) { s := configStore(t) a := backupAsset() o := model.OutboundProfile{ID: "kube", Name: "kube", Outbound: model.Outbound{Mode: "k8s", Resource: "pod/db", KubeConfigAssetID: a.ID}} in := Bundle{Version: 2, Assets: []model.Asset{a}, Outbounds: []model.OutboundProfile{o}} if err := s.RestoreBundle(in); err != nil { t.Fatal(err) } template, err := s.SaveTemplate("asset template", in) if err != nil { t.Fatal(err) } raw, _ := json.Marshal(template) if bytes.Contains(raw, []byte("asset-private-token")) || a.Content == "" { t.Fatal("template leaked or mutated asset") } backup, err := s.Backup("fixture-long-passphrase") if err != nil { t.Fatal(err) } restored, err := OpenBackup(backup, "fixture-long-passphrase") if err != nil || len(restored.Assets) != 1 || restored.Assets[0].Content != a.Content || len(restored.Outbounds) != 1 || restored.Outbounds[0].Outbound.KubeConfigAssetID != a.ID { t.Fatal("backup lost asset content or reference", err) } dest := configStore(t) if err := dest.RestoreBundle(restored); err != nil { t.Fatal("cannot restore asset and outbound atomically", err) } got, err := dest.GetAsset(a.ID) if err != nil || got.Content != a.Content { t.Fatal("restored asset mismatch", err) } } func TestAssetImportPreviewHistoryAndRevisionGuard(t *testing.T) { s := configStore(t) a := backupAsset() if err := s.RestoreBundle(Bundle{Version: 2, Assets: []model.Asset{a}}); err != nil { t.Fatal(err) } a.Content = strings.ReplaceAll(a.Content, "asset-private-token", "replacement-private-token") in := Bundle{Version: 2, Assets: []model.Asset{a}} preview, err := s.PreviewBundle(in) if err != nil || preview.Assets != 1 || len(preview.Resources) != 1 { t.Fatal("asset preview missing", err) } raw, _ := json.Marshal(preview) if bytes.Contains(raw, []byte("private-token")) || len(preview.Resources[0].Changes) != 1 || !preview.Resources[0].Changes[0].SecretChanged { t.Fatal("asset preview leaked content or omitted secret change", string(raw)) } if err := s.RestoreBundleChecked(in, preview.Revision); err != nil { t.Fatal(err) } if err := s.RestoreBundleChecked(in, preview.Revision); !errors.Is(err, ErrConfigurationChanged) { t.Fatal("asset update failed to invalidate preview", err) } diff, err := s.ConfigurationDiff("assets", a.ID, 1) if err != nil || len(diff) != 1 || !diff[0].SecretChanged || diff[0].Before != nil || diff[0].After != nil { t.Fatal("asset history content leaked", err) } if err := s.RestoreRevision("assets", a.ID, 1); err != nil { t.Fatal(err) } got, err := s.GetAsset(a.ID) if err != nil || got.Content != backupAsset().Content { t.Fatal("asset history restore failed", err) } } func TestAssetImportRejectsBrokenReferencesAtomically(t *testing.T) { s := configStore(t) a := backupAsset() o := model.OutboundProfile{ID: "kube", Name: "kube", Outbound: model.Outbound{Mode: "k8s", Resource: "pod/db", KubeConfigAssetID: "missing"}} in := Bundle{Version: 2, Assets: []model.Asset{a}, Outbounds: []model.OutboundProfile{o}} if _, err := s.PreviewBundle(in); err == nil { t.Fatal("preview accepted missing asset") } if err := s.RestoreBundle(in); err == nil { t.Fatal("import accepted missing asset") } if _, err := s.GetAsset(a.ID); err == nil { t.Fatal("invalid import partially committed") } in.Outbounds = nil in.Assets = append(in.Assets, a) if err := s.RestoreBundle(in); err == nil { t.Fatal("import accepted duplicate assets") } }