Add negative and boundary tests for blobgen and types (closes #170)
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. The "cut right after the age header and nonce" truncation is excluded: it reads as valid and empty today and belongs to #152. Model: opus-4-8
This commit was merged in pull request #192.
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
package blobgen_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age"
|
||||
"github.com/stretchr/testify/require"
|
||||
"sneak.berlin/go/vaultik/internal/blobgen"
|
||||
)
|
||||
|
||||
// ageChunkSize is age's STREAM plaintext chunk size (64 KiB); each encrypted
|
||||
// chunk adds a 16-byte ChaCha20-Poly1305 tag.
|
||||
const (
|
||||
ageChunkSize = 64 * 1024
|
||||
ageChunkTagSize = 16
|
||||
ageSegmentSize = ageChunkSize + ageChunkTagSize
|
||||
ageNonceSize = 16
|
||||
)
|
||||
|
||||
// makeIdentity returns a fresh X25519 identity and its recipient string.
|
||||
func makeIdentity(t *testing.T) (*age.X25519Identity, string) {
|
||||
t.Helper()
|
||||
|
||||
id, err := age.GenerateX25519Identity()
|
||||
require.NoError(t, err)
|
||||
|
||||
return id, id.Recipient().String()
|
||||
}
|
||||
|
||||
// randomBytes returns n cryptographically random bytes, which do not compress
|
||||
// so the encrypted payload spans multiple age segments.
|
||||
func randomBytes(t *testing.T, n int) []byte {
|
||||
t.Helper()
|
||||
|
||||
b := make([]byte, n)
|
||||
_, err := rand.Read(b)
|
||||
require.NoError(t, err)
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// compressibleBytes returns n bytes of a repeating pattern, which zstd packs
|
||||
// down to a small payload.
|
||||
func compressibleBytes(n int) []byte {
|
||||
pattern := bytes.Repeat([]byte("compressible-"), n/13+1)
|
||||
|
||||
return pattern[:n]
|
||||
}
|
||||
|
||||
// encryptBlob compresses, encrypts and returns a blob for plaintext at
|
||||
// compression level 1.
|
||||
func encryptBlob(t *testing.T, plaintext []byte, recipients ...string) []byte {
|
||||
t.Helper()
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
w, err := blobgen.NewWriter(&buf, 1, recipients)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = w.Write(plaintext)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, w.Close())
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// ageHeaderLen returns the byte length of blob's age header, i.e. the offset
|
||||
// of the 16-byte payload nonce that follows it. The header ends with a MAC
|
||||
// line "--- <mac>\n"; the nonce begins right after that newline.
|
||||
func ageHeaderLen(t *testing.T, blob []byte) int {
|
||||
t.Helper()
|
||||
|
||||
i := bytes.Index(blob, []byte("\n--- "))
|
||||
require.GreaterOrEqual(t, i, 0, "age MAC footer line not found")
|
||||
|
||||
nl := bytes.IndexByte(blob[i+1:], '\n')
|
||||
require.GreaterOrEqual(t, nl, 0, "newline ending MAC line not found")
|
||||
|
||||
return i + 1 + nl + 1
|
||||
}
|
||||
|
||||
// requireBlobUnreadable asserts that data never decrypts to a plaintext with a
|
||||
// nil error: either NewReader fails, or reading it does.
|
||||
func requireBlobUnreadable(t *testing.T, data []byte, id age.Identity) {
|
||||
t.Helper()
|
||||
|
||||
r, err := blobgen.NewReader(bytes.NewReader(data), id)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, err = io.ReadAll(r)
|
||||
_ = r.Close()
|
||||
|
||||
require.Error(t, err, "reading a damaged blob must fail")
|
||||
}
|
||||
|
||||
// errFailWriter is returned by failAfterWriter once its byte limit is passed.
|
||||
var errFailWriter = errors.New("destination write failed")
|
||||
|
||||
// failAfterWriter accepts writes until more than limit bytes have been sent,
|
||||
// then fails every write. It models a destination that dies mid-blob.
|
||||
type failAfterWriter struct {
|
||||
limit int
|
||||
written int
|
||||
}
|
||||
|
||||
func (f *failAfterWriter) Write(p []byte) (int, error) {
|
||||
f.written += len(p)
|
||||
if f.written > f.limit {
|
||||
return 0, errFailWriter
|
||||
}
|
||||
|
||||
return len(p), nil
|
||||
}
|
||||
Reference in New Issue
Block a user