You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/yan1h/agent-coding-workflow/internal/infra/crypto"
|
||||
"github.com/yan1h/agent-coding-workflow/internal/jobs"
|
||||
)
|
||||
|
||||
// memRow models one encrypted-column row across versions.
|
||||
type memRow struct {
|
||||
blob []byte
|
||||
keyVersion int
|
||||
plaintext []byte // ground truth for assertions
|
||||
null bool
|
||||
}
|
||||
|
||||
// memStore is an in-memory ReencryptStore over a single logical table, with a
|
||||
// failNext hook to model a crash mid-run for the resumability test.
|
||||
type memStore struct {
|
||||
rows []*memRow
|
||||
failNext bool
|
||||
}
|
||||
|
||||
func (m *memStore) Tables() []string { return []string{"t"} }
|
||||
|
||||
func (m *memStore) StaleCount(_ context.Context, _ string, active int) (int, error) {
|
||||
n := 0
|
||||
for _, r := range m.rows {
|
||||
if !r.null && r.keyVersion < active {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (m *memStore) ReencryptTable(ctx context.Context, enc *crypto.KeyedEncryptor, _ string, active, batch int) (int, error) {
|
||||
done := 0
|
||||
for _, r := range m.rows {
|
||||
if done >= batch {
|
||||
break
|
||||
}
|
||||
if r.null || r.keyVersion >= active {
|
||||
continue
|
||||
}
|
||||
if m.failNext {
|
||||
m.failNext = false
|
||||
return done, context.Canceled // simulate a crash after some rows
|
||||
}
|
||||
plain, err := enc.DecryptVersion(ctx, r.blob, r.keyVersion)
|
||||
if err != nil {
|
||||
return done, err
|
||||
}
|
||||
sealed, err := enc.SealWithVersion(ctx, active, plain)
|
||||
if err != nil {
|
||||
return done, err
|
||||
}
|
||||
r.blob = sealed
|
||||
r.keyVersion = active
|
||||
done++
|
||||
}
|
||||
return done, nil
|
||||
}
|
||||
|
||||
func key(t *testing.T) []byte {
|
||||
t.Helper()
|
||||
k := make([]byte, 32)
|
||||
_, err := rand.Read(k)
|
||||
require.NoError(t, err)
|
||||
return k
|
||||
}
|
||||
|
||||
// twoVersionEnc returns a KeyedEncryptor with v1+v2 keys and a store reporting
|
||||
// active=2, plus a helper to seal under v1.
|
||||
func twoVersionEnc(t *testing.T) *crypto.KeyedEncryptor {
|
||||
prov := crypto.NewEnvProvider(nil)
|
||||
prov.SetKey(1, key(t))
|
||||
prov.SetKey(2, key(t))
|
||||
return crypto.NewKeyedEncryptor(prov, crypto.NewStaticKeyStore(2))
|
||||
}
|
||||
|
||||
func TestSecretReencrypt_AdvancesAllRows(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
enc := twoVersionEnc(t)
|
||||
|
||||
// Seed 3 rows encrypted under v1 + 1 NULL row.
|
||||
store := &memStore{}
|
||||
for i := 0; i < 3; i++ {
|
||||
pt := []byte{byte(i), byte(i + 1)}
|
||||
ct, err := enc.SealWithVersion(ctx, 1, pt)
|
||||
require.NoError(t, err)
|
||||
store.rows = append(store.rows, &memRow{blob: ct, keyVersion: 1, plaintext: pt})
|
||||
}
|
||||
store.rows = append(store.rows, &memRow{null: true, keyVersion: 1})
|
||||
|
||||
h := NewSecretReencryptHandler(store, enc, nil)
|
||||
payload, _ := json.Marshal(SecretReencryptPayload{ActiveVersion: 2, BatchSize: 2})
|
||||
require.NoError(t, h.Handle(ctx, jobs.Job{Payload: payload}))
|
||||
|
||||
// All non-null rows now on v2 and decrypt to identical plaintext.
|
||||
for _, r := range store.rows {
|
||||
if r.null {
|
||||
require.Equal(t, 1, r.keyVersion, "NULL rows are skipped, version unchanged")
|
||||
continue
|
||||
}
|
||||
require.Equal(t, 2, r.keyVersion)
|
||||
got, err := enc.DecryptVersion(ctx, r.blob, 2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, r.plaintext, got)
|
||||
}
|
||||
left, _ := store.StaleCount(ctx, "t", 2)
|
||||
require.Equal(t, 0, left)
|
||||
}
|
||||
|
||||
func TestSecretReencrypt_IdempotentRerun(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
enc := twoVersionEnc(t)
|
||||
store := &memStore{}
|
||||
pt := []byte("hello")
|
||||
ct, err := enc.SealWithVersion(ctx, 1, pt)
|
||||
require.NoError(t, err)
|
||||
store.rows = append(store.rows, &memRow{blob: ct, keyVersion: 1, plaintext: pt})
|
||||
|
||||
h := NewSecretReencryptHandler(store, enc, nil)
|
||||
payload, _ := json.Marshal(SecretReencryptPayload{ActiveVersion: 2})
|
||||
require.NoError(t, h.Handle(ctx, jobs.Job{Payload: payload}))
|
||||
blobAfterFirst := store.rows[0].blob
|
||||
|
||||
// Second run is a no-op: row already on active version, blob unchanged.
|
||||
require.NoError(t, h.Handle(ctx, jobs.Job{Payload: payload}))
|
||||
require.Equal(t, blobAfterFirst, store.rows[0].blob)
|
||||
}
|
||||
|
||||
func TestSecretReencrypt_ResumableAfterCrash(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
enc := twoVersionEnc(t)
|
||||
store := &memStore{failNext: true}
|
||||
for i := 0; i < 5; i++ {
|
||||
pt := []byte{byte(i)}
|
||||
ct, err := enc.SealWithVersion(ctx, 1, pt)
|
||||
require.NoError(t, err)
|
||||
store.rows = append(store.rows, &memRow{blob: ct, keyVersion: 1, plaintext: pt})
|
||||
}
|
||||
|
||||
h := NewSecretReencryptHandler(store, enc, nil)
|
||||
payload, _ := json.Marshal(SecretReencryptPayload{ActiveVersion: 2, BatchSize: 10})
|
||||
// First run crashes (ctx.Canceled bubbles up).
|
||||
require.Error(t, h.Handle(ctx, jobs.Job{Payload: payload}))
|
||||
left, _ := store.StaleCount(ctx, "t", 2)
|
||||
require.Greater(t, left, 0, "crash left some rows on old version")
|
||||
|
||||
// Re-run completes the rest.
|
||||
require.NoError(t, h.Handle(ctx, jobs.Job{Payload: payload}))
|
||||
left, _ = store.StaleCount(ctx, "t", 2)
|
||||
require.Equal(t, 0, left)
|
||||
for _, r := range store.rows {
|
||||
got, err := enc.DecryptVersion(ctx, r.blob, 2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, r.plaintext, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretReencrypt_BadPayloadIsPermanent(t *testing.T) {
|
||||
h := NewSecretReencryptHandler(&memStore{}, twoVersionEnc(t), nil)
|
||||
err := h.Handle(context.Background(), jobs.Job{Payload: []byte("not json")})
|
||||
require.Error(t, err)
|
||||
require.True(t, jobs.IsPermanent(err))
|
||||
}
|
||||
Reference in New Issue
Block a user