Files
geniusrun/backend/internal/store/isolation_test.go

164 lines
5.5 KiB
Go
Raw Normal View History

package store
import (
"context"
"testing"
)
// TestIsolation_ProfileNeverLeaksAcrossUsers confirms GetProfile only ever
// returns the row matching the given userID, and that two users' profiles
// can diverge independently.
func TestIsolation_ProfileNeverLeaksAcrossUsers(t *testing.T) {
db := openTestDB(t)
ctx := context.Background()
userA, err := db.ProvisionUser(ctx, "sub-a", "A")
if err != nil {
t.Fatalf("ProvisionUser(a): %v", err)
}
userB, err := db.ProvisionUser(ctx, "sub-b", "B")
if err != nil {
t.Fatalf("ProvisionUser(b): %v", err)
}
profileA, err := db.GetProfile(ctx, userA)
if err != nil {
t.Fatalf("GetProfile(a): %v", err)
}
profileA.GarminEmail = "a@example.com"
if err := db.UpdateProfile(ctx, userA, profileA); err != nil {
t.Fatalf("UpdateProfile(a): %v", err)
}
profileB, err := db.GetProfile(ctx, userB)
if err != nil {
t.Fatalf("GetProfile(b): %v", err)
}
if profileB.GarminEmail == "a@example.com" {
t.Fatal("userB's profile picked up userA's GarminEmail update")
}
// Attempting to update B's profile "as A" (i.e. calling UpdateProfile
// with userA but a struct that happens to describe B's desired state)
// only ever touches the row WHERE user_id = userA -- confirm B is
// unaffected by any such call.
if err := db.UpdateProfile(ctx, userA, Profile{GarminEmail: "still-a-only@example.com"}); err != nil {
t.Fatalf("UpdateProfile(a) second call: %v", err)
}
profileB2, err := db.GetProfile(ctx, userB)
if err != nil {
t.Fatalf("GetProfile(b) after A's update: %v", err)
}
if profileB2.GarminEmail == "still-a-only@example.com" {
t.Fatal("userA's UpdateProfile call leaked into userB's row")
}
}
// TestIsolation_WorkoutKindsAndPacesNeverLeakAcrossUsers confirms
// GetWorkoutKind/GetWorkoutTypePace return not-found for a kind id that
// exists but belongs to a different user, and that UpdateWorkoutKind /
// UpdateWorkoutTypePace can never mutate another user's row even if handed
// that row's real id.
func TestIsolation_WorkoutKindsAndPacesNeverLeakAcrossUsers(t *testing.T) {
db := openTestDB(t)
ctx := context.Background()
userA, err := db.ProvisionUser(ctx, "sub-a", "A")
if err != nil {
t.Fatalf("ProvisionUser(a): %v", err)
}
userB, err := db.ProvisionUser(ctx, "sub-b", "B")
if err != nil {
t.Fatalf("ProvisionUser(b): %v", err)
}
kindsA, err := db.ListWorkoutKinds(ctx, userA, false)
if err != nil || len(kindsA) == 0 {
t.Fatalf("ListWorkoutKinds(a): len=%d err=%v", len(kindsA), err)
}
targetKindID := kindsA[0].ID
if _, found, err := db.GetWorkoutKind(ctx, userB, targetKindID); err != nil || found {
t.Fatalf("expected userB not to see userA's kind %d, found=%v err=%v", targetKindID, found, err)
}
// Attempt to update A's kind "as B" -- must silently affect zero rows,
// not A's real row.
if err := db.UpdateWorkoutKind(ctx, userB, WorkoutKind{ID: targetKindID, Name: "Hijacked", RuleJSON: "{}"}); err != nil {
t.Fatalf("UpdateWorkoutKind(as b): %v", err)
}
stillA, found, err := db.GetWorkoutKind(ctx, userA, targetKindID)
if err != nil || !found {
t.Fatalf("GetWorkoutKind(a) after B's attempted update: found=%v err=%v", found, err)
}
if stillA.Name == "Hijacked" {
t.Fatal("userB's UpdateWorkoutKind call was able to mutate userA's kind")
}
if _, err := db.GetWorkoutTypePace(ctx, userB, targetKindID); err != nil {
t.Fatalf("GetWorkoutTypePace(as b) should return a zero-value pace, not an error, got: %v", err)
}
minPace := 300.0
if err := db.UpdateWorkoutTypePace(ctx, userB, WorkoutTypePace{WorkoutKindID: targetKindID, PaceMinSecPerKm: &minPace}); err != nil {
t.Fatalf("UpdateWorkoutTypePace(as b): %v", err)
}
paceA, err := db.GetWorkoutTypePace(ctx, userA, targetKindID)
if err != nil {
t.Fatalf("GetWorkoutTypePace(a): %v", err)
}
if paceA.PaceMinSecPerKm != nil {
t.Fatal("userB's UpdateWorkoutTypePace call was able to mutate userA's pace row")
}
}
// TestIsolation_SyncStateAndRunsNeverLeakAcrossUsers confirms each user's
// backfill watermark and sync run history are independent.
func TestIsolation_SyncStateAndRunsNeverLeakAcrossUsers(t *testing.T) {
db := openTestDB(t)
ctx := context.Background()
userA, err := db.ProvisionUser(ctx, "sub-a", "A")
if err != nil {
t.Fatalf("ProvisionUser(a): %v", err)
}
userB, err := db.ProvisionUser(ctx, "sub-b", "B")
if err != nil {
t.Fatalf("ProvisionUser(b): %v", err)
}
if err := db.UpdateSyncState(ctx, userA, "2020-01-01", true); err != nil {
t.Fatalf("UpdateSyncState(a): %v", err)
}
stateB, err := db.GetSyncState(ctx, userB)
if err != nil {
t.Fatalf("GetSyncState(b): %v", err)
}
if stateB.BackfillComplete || stateB.EarliestSyncedDate != nil {
t.Fatalf("userA's UpdateSyncState leaked into userB's sync_state: %+v", stateB)
}
runID, err := db.StartSyncRun(ctx, userA, SyncKindBackfill)
if err != nil {
t.Fatalf("StartSyncRun(a): %v", err)
}
runsB, err := db.ListSyncRuns(ctx, userB, 10)
if err != nil {
t.Fatalf("ListSyncRuns(b): %v", err)
}
if len(runsB) != 0 {
t.Fatalf("expected userB to have 0 sync runs, got %d (userA's run id=%d)", len(runsB), runID)
}
// Finishing A's run "as B" must not succeed against A's row.
if err := db.FinishSyncRun(ctx, userB, runID, 5, nil); err != nil {
t.Fatalf("FinishSyncRun(as b): %v", err)
}
latestA, found, err := db.LatestSyncRun(ctx, userA)
if err != nil || !found {
t.Fatalf("LatestSyncRun(a): found=%v err=%v", found, err)
}
if latestA.Status == SyncStatusSuccess {
t.Fatal("userB's FinishSyncRun call was able to mutate userA's sync run")
}
}