feat: store layer with indexes, optimistic concurrency, GridFS, credential crypto (phase 2)

- mongo-driver v2 connection with bounded startup retry + /readyz gate
- idempotent creation of all §4.9 indexes
- UpdateVersioned: version-filtered updates, conflict vs not-found errors
- AES-256-GCM Seal/Open for ticketing credentials, base64(nonce|ct)
- GridFS file store: MIME sniffing, MAX_UPLOAD_MB cap, sha256 metadata
- HMAC-signed short-lived file URL tokens (§5.1, 1h TTL)
- integration tests against compose Mongo via docker-compose.test.yml overlay

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
etalon
2026-06-12 18:17:26 +02:00
parent 976f5238f8
commit f2c534f636
18 changed files with 1129 additions and 3 deletions
+157
View File
@@ -0,0 +1,157 @@
//go:build integration
package store
import (
"context"
"errors"
"testing"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"bountyboard/internal/testutil"
"bountyboard/internal/ulid"
)
func TestEnsureIndexesIsIdempotent(t *testing.T) {
db := testutil.MongoDB(t)
ctx := context.Background()
if err := EnsureIndexes(ctx, db); err != nil {
t.Fatalf("first run: %v", err)
}
if err := EnsureIndexes(ctx, db); err != nil {
t.Fatalf("second run must be a no-op: %v", err)
}
// Spot-check a few critical indexes actually exist.
checks := map[string]string{
"users": "email_1",
"tasks": "external.system_1_external.key_1_customerId_1",
"sessions": "expiresAt_1",
}
for coll, wantName := range checks {
cur, err := db.Collection(coll).Indexes().List(ctx)
if err != nil {
t.Fatal(err)
}
var specs []bson.M
if err := cur.All(ctx, &specs); err != nil {
t.Fatal(err)
}
found := false
for _, s := range specs {
if s["name"] == wantName {
found = true
// the unique upsert key must really be unique+sparse
if coll == "tasks" {
if s["unique"] != true || s["sparse"] != true {
t.Errorf("%s/%s: want unique sparse, got %v", coll, wantName, s)
}
}
}
}
if !found {
t.Errorf("%s: index %s not found in %v", coll, wantName, specs)
}
}
}
func TestUpdateVersioned(t *testing.T) {
db := testutil.MongoDB(t)
ctx := context.Background()
coll := db.Collection("tasks")
id := ulid.New()
if err := InsertOne(ctx, coll, id, bson.M{"title": "original", "status": "imported"}); err != nil {
t.Fatal(err)
}
// Happy path bumps version and updatedAt.
if err := UpdateVersioned(ctx, coll, id, 1, bson.M{"$set": bson.M{"title": "edited"}}); err != nil {
t.Fatalf("update at correct version: %v", err)
}
var doc struct {
Title string `bson:"title"`
Version int64 `bson:"version"`
UpdatedAt time.Time `bson:"updatedAt"`
CreatedAt time.Time `bson:"createdAt"`
}
if err := coll.FindOne(ctx, bson.M{"_id": id}).Decode(&doc); err != nil {
t.Fatal(err)
}
if doc.Title != "edited" || doc.Version != 2 {
t.Fatalf("after update: title=%q version=%d", doc.Title, doc.Version)
}
if !doc.UpdatedAt.After(doc.CreatedAt) {
t.Error("updatedAt must advance")
}
// Stale version loses.
err := UpdateVersioned(ctx, coll, id, 1, bson.M{"$set": bson.M{"title": "stale write"}})
if !errors.Is(err, ErrVersionConflict) {
t.Fatalf("want ErrVersionConflict, got %v", err)
}
// Missing document is distinguished from a conflict.
err = UpdateVersioned(ctx, coll, ulid.New(), 1, bson.M{"$set": bson.M{"title": "x"}})
if !errors.Is(err, ErrNotFound) {
t.Fatalf("want ErrNotFound, got %v", err)
}
// Non-$set operators pass through; version still bumps.
if err := UpdateVersioned(ctx, coll, id, 2, bson.M{
"$push": bson.M{"timeline": bson.M{"event": "published"}},
}); err != nil {
t.Fatalf("push update: %v", err)
}
var doc2 struct {
Version int64 `bson:"version"`
Timeline []bson.M `bson:"timeline"`
}
if err := coll.FindOne(ctx, bson.M{"_id": id}).Decode(&doc2); err != nil {
t.Fatal(err)
}
if doc2.Version != 3 || len(doc2.Timeline) != 1 {
t.Fatalf("after push: version=%d timeline=%d", doc2.Version, len(doc2.Timeline))
}
// Touching version inside the update is a programming error.
if err := UpdateVersioned(ctx, coll, id, 3, bson.M{"$inc": bson.M{"version": 5}}); err == nil {
t.Fatal("must reject updates that touch version")
}
}
func TestUpdateVersionedConcurrentWriters(t *testing.T) {
db := testutil.MongoDB(t)
ctx := context.Background()
coll := db.Collection("tasks")
id := ulid.New()
if err := InsertOne(ctx, coll, id, bson.M{"n": 0}); err != nil {
t.Fatal(err)
}
// Two writers race at the same version: exactly one wins.
results := make(chan error, 2)
for i := 0; i < 2; i++ {
go func(i int) {
results <- UpdateVersioned(ctx, coll, id, 1, bson.M{"$set": bson.M{"n": i + 1}})
}(i)
}
var conflicts, oks int
for i := 0; i < 2; i++ {
switch err := <-results; {
case err == nil:
oks++
case errors.Is(err, ErrVersionConflict):
conflicts++
default:
t.Fatalf("unexpected error: %v", err)
}
}
if oks != 1 || conflicts != 1 {
t.Fatalf("oks=%d conflicts=%d, want exactly one of each", oks, conflicts)
}
}