package vaultik //nolint:testpackage // inspects unexported snapshot-db materialization import ( "bytes" "context" "os" "path/filepath" "testing" "filippo.io/age" "github.com/spf13/afero" "github.com/stretchr/testify/require" "sneak.berlin/go/vaultik/internal/blobgen" "sneak.berlin/go/vaultik/internal/database" ) // genuineSnapshotDBBytes returns the on-disk bytes of a real snapshot // database (the full schema applied). func genuineSnapshotDBBytes(t *testing.T) []byte { t.Helper() path := filepath.Join(t.TempDir(), "snapshot.db") db, err := database.New(context.Background(), path) require.NoError(t, err) require.NoError(t, db.Close()) data, err := os.ReadFile(path) //nolint:gosec // G304: test-controlled temp path require.NoError(t, err) return data } // TestMaterializeSnapshotDBPrivateDir proves the decrypted database lands // in a private (0700) directory and opens read-only. func TestMaterializeSnapshotDBPrivateDir(t *testing.T) { dbData := genuineSnapshotDBBytes(t) t.Setenv("TMPDIR", t.TempDir()) v := &Vaultik{ctx: context.Background(), Fs: afero.NewOsFs()} db, dir, err := v.materializeSnapshotDB(bytes.NewReader(dbData)) require.NoError(t, err) t.Cleanup(func() { _ = db.Close() _ = os.RemoveAll(dir) }) info, err := os.Stat(dir) require.NoError(t, err) require.Equal(t, os.FileMode(0o700), info.Mode().Perm(), "snapshot database directory must not be world-readable") _, err = db.Conn().ExecContext(context.Background(), "CREATE TABLE probe_readonly (x)") require.Error(t, err, "materialized snapshot database must be read-only") } // TestMaterializeSnapshotDBRejectsCompleteEmptyStream proves the written == 0 // guard rejects a genuinely empty but complete metadata object: a real age // header, nonce, and final tag encrypting zero plaintext bytes. The truncation // case is stopped earlier by the reader (io.ErrUnexpectedEOF) and never reaches // this branch, so it needs its own input. This complete stream decrypts to zero // bytes with a clean EOF, passes the reader, and must be refused as empty rather // than accepted as a valid zero-table database. Reverting the guard lets the // empty file open as a fresh schema and the test fails. func TestMaterializeSnapshotDBRejectsCompleteEmptyStream(t *testing.T) { identity, err := age.GenerateX25519Identity() require.NoError(t, err) var stream bytes.Buffer w, err := age.Encrypt(&stream, identity.Recipient()) require.NoError(t, err) require.NoError(t, w.Close()) blobReader, err := blobgen.NewReader(bytes.NewReader(stream.Bytes()), identity) require.NoError(t, err) t.Cleanup(func() { _ = blobReader.Close() }) t.Setenv("TMPDIR", t.TempDir()) v := &Vaultik{ctx: context.Background(), Fs: afero.NewOsFs()} _, _, err = v.materializeSnapshotDB(blobReader) require.ErrorIs(t, err, errEmptySnapshotDB) } // TestMaterializeSnapshotDBRemovesDirOnOpenFailure proves a failed open // leaves no temp directory behind. func TestMaterializeSnapshotDBRemovesDirOnOpenFailure(t *testing.T) { base := t.TempDir() t.Setenv("TMPDIR", base) v := &Vaultik{ctx: context.Background(), Fs: afero.NewOsFs()} _, _, err := v.materializeSnapshotDB( bytes.NewReader([]byte("this is not a sqlite database"))) require.Error(t, err) entries, rerr := os.ReadDir(base) require.NoError(t, rerr) require.Empty(t, entries, "temp directory left behind after open failure") }