package blobgen_test import ( "bytes" "fmt" "testing" "filippo.io/age" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sneak.berlin/go/vaultik/internal/blobgen" ) // TestNewReaderWrongIdentity covers issue case 4: opening a blob with an // identity other than the recipient reports no matching identity. func TestNewReaderWrongIdentity(t *testing.T) { t.Parallel() _, recipient := makeIdentity(t) other, _ := makeIdentity(t) blob := encryptBlob(t, []byte("secret payload"), recipient) _, err := blobgen.NewReader(bytes.NewReader(blob), other) require.Error(t, err) var noMatch *age.NoIdentityMatchError assert.ErrorAs(t, err, &noMatch) } // TestNewReaderTruncated covers issue case 6: a multi-segment blob cut at // several points must never read back as valid data. The point immediately // after the header and nonce is intentionally excluded: it reads as a valid // empty blob today and is the regression case for // https://git.eeqj.de/sneak/vaultik/issues/152. func TestNewReaderTruncated(t *testing.T) { t.Parallel() id, recipient := makeIdentity(t) blob := encryptBlob(t, randomBytes(t, 4*65536+123), recipient) h := ageHeaderLen(t, blob) require.Greater(t, len(blob), h+ageNonceSize+ageSegmentSize, "test needs a blob of at least two age segments") cases := []struct { name string size int }{ {"inside header", h / 2}, {"inside nonce", h + 8}, {"inside first segment", h + ageNonceSize + 100}, {"end of first full segment", h + ageNonceSize + ageSegmentSize}, {"last byte removed", len(blob) - 1}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() requireBlobUnreadable(t, blob[:tc.size], id) }) } } // TestNewReaderCorrupted covers issue case 7: one flipped byte in each region // of a multi-segment blob makes it unreadable. func TestNewReaderCorrupted(t *testing.T) { t.Parallel() id, recipient := makeIdentity(t) blob := encryptBlob(t, randomBytes(t, 4*65536+123), recipient) h := ageHeaderLen(t, blob) firstNL := bytes.IndexByte(blob, '\n') require.Positive(t, firstNL, "header must have a version line") cases := []struct { name string pos int }{ {"header stanza", firstNL + 5}, {"header MAC line", h - 2}, {"nonce", h + 4}, {"body segment", h + ageNonceSize + 50}, {"final tag", len(blob) - 1}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() corrupt := append([]byte(nil), blob...) corrupt[tc.pos] ^= 0xff requireBlobUnreadable(t, corrupt, id) }) } } // TestNewReaderTrailingAndGarbage covers issue case 8: bytes appended after a // valid blob, empty input, and random garbage each fail to read. func TestNewReaderTrailingAndGarbage(t *testing.T) { t.Parallel() id, recipient := makeIdentity(t) valid := encryptBlob(t, []byte("small payload"), recipient) appended := append(append([]byte(nil), valid...), []byte("trailing junk")...) t.Run("appended bytes", func(t *testing.T) { t.Parallel() requireBlobUnreadable(t, appended, id) }) t.Run("empty input", func(t *testing.T) { t.Parallel() requireBlobUnreadable(t, []byte{}, id) }) t.Run("random garbage", func(t *testing.T) { t.Parallel() requireBlobUnreadable(t, randomBytes(t, 512), id) }) } // TestNewWriterInvalidLevel covers the rejected end of issue case 9: an // out-of-range compression level errors and writes nothing to the destination. func TestNewWriterInvalidLevel(t *testing.T) { t.Parallel() _, recipient := makeIdentity(t) for _, level := range []int{0, -1, 20} { t.Run(fmt.Sprintf("level%d", level), func(t *testing.T) { t.Parallel() var buf bytes.Buffer w, err := blobgen.NewWriter(&buf, level, []string{recipient}) require.ErrorIs(t, err, blobgen.ErrInvalidCompressionLevel) assert.Nil(t, w) assert.Zero(t, buf.Len(), "nothing written on an invalid level") }) } } // TestNewWriterInvalidRecipients covers issue case 10: nil and empty recipient // lists and an unparsable recipient string each error. func TestNewWriterInvalidRecipients(t *testing.T) { t.Parallel() cases := []struct { name string recipients []string }{ {"nil list", nil}, {"empty list", []string{}}, {"invalid recipient string", []string{"not-a-recipient"}}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() var buf bytes.Buffer w, err := blobgen.NewWriter(&buf, 1, tc.recipients) require.Error(t, err) assert.Nil(t, w) }) } } // TestNewWriterFailingDestination covers issue case 11: a destination that // fails mid-blob surfaces its error from Write or Close. func TestNewWriterFailingDestination(t *testing.T) { t.Parallel() _, recipient := makeIdentity(t) // The limit clears the age header and nonce so NewWriter succeeds, then // trips once the compressed body starts flowing. dst := &failAfterWriter{limit: 512} w, err := blobgen.NewWriter(dst, 1, []string{recipient}) require.NoError(t, err) _, writeErr := w.Write(randomBytes(t, 256*1024)) closeErr := w.Close() assert.True(t, writeErr != nil || closeErr != nil, "destination failure must surface from Write or Close") }