f2c534f636
- 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>
158 lines
4.2 KiB
Go
158 lines
4.2 KiB
Go
//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)
|
|
}
|
|
}
|