check / check (pull_request) Successful in 2m20s
Test-only. internal/blobgen and internal/types had no negative or boundary coverage. Adds, in package blobgen_test: Writer-to-Reader round trips at the 64 KiB age-segment edges for random and compressible data, checking plaintext, byte counts and the reader/writer hashes by decrypting; a wrong-identity open; truncation and single-byte corruption of a multi-segment blob at every region; trailing bytes, empty input and garbage; rejected and accepted compression levels; nil, empty and invalid recipients; and a failing destination. In package types_test: Value/Scan round trips, NULL, wrong-type and malformed Scan, Parse and IsZero for FileID and BlobID. internal/crypto and CompressStream/CompressData no longer exist and are skipped. The "cut right after the age header and nonce" truncation is excluded: it reads as valid and empty today and belongs to #152. Reworded two writer_test.go messages that overstated what the double hash prevents. Model: opus-4-8
191 lines
5.1 KiB
Go
191 lines
5.1 KiB
Go
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")
|
|
}
|