//nolint:testpackage // exercises the unexported generateBlobManifest package snapshot import ( "context" "path/filepath" "testing" "time" "github.com/spf13/afero" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sneak.berlin/go/vaultik/internal/config" "sneak.berlin/go/vaultik/internal/database" "sneak.berlin/go/vaultik/internal/log" "sneak.berlin/go/vaultik/internal/types" ) // TestGenerateBlobManifest_MissingBlobFails is the regression guard for // issue #157: a blob the snapshot references but that is absent from the // blobs table used to be logged and skipped, yielding a manifest with // fewer blobs than the snapshot needs. Since prune trusts the manifest // alone, that omitted blob would be deleted at the next prune. Manifest // generation must fail instead. func TestGenerateBlobManifest_MissingBlobFails(t *testing.T) { log.Initialize(log.Config{}) t.Parallel() ctx := context.Background() dbPath := filepath.Join(t.TempDir(), "snapshot.db") db, err := database.New(ctx, dbPath) require.NoError(t, err) repos := database.NewRepositories(db) // A real blob row satisfies the snapshot_blobs foreign key on // blob_id; the snapshot then references a different, absent hash. presentBlob := &database.Blob{ ID: types.NewBlobID(), Hash: types.BlobHash("present-blob-hash"), CreatedTS: time.Now().Truncate(time.Second), } require.NoError(t, repos.Blobs.Create(ctx, nil, presentBlob)) snap := &database.Snapshot{ ID: "testhost_home_2026-05-01T00:00:00Z", Hostname: "testhost", } require.NoError(t, repos.Snapshots.Create(ctx, nil, snap)) require.NoError(t, repos.Snapshots.AddBlob(ctx, nil, snap.ID.String(), presentBlob.ID, types.BlobHash("absent-blob-hash"))) require.NoError(t, db.Close()) sm := &SnapshotManager{ config: &config.Config{CompressionLevel: 3}, fs: afero.NewOsFs(), } _, err = sm.generateBlobManifest(ctx, dbPath, snap.ID.String()) require.Error(t, err, "manifest generation must fail on a missing blob") assert.Contains(t, err.Error(), "absent-blob-hash") }