//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) } }