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, SyncKindFull) 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") } } // TestIsolation_DeleteUserLeavesOtherUsersDataIntact confirms deleting one // user's account never touches another user's profile, taxonomy, or // activities, even though DeleteUser is a single blunt DELETE FROM users. func TestIsolation_DeleteUserLeavesOtherUsersDataIntact(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.UpsertActivity(ctx, userB, Activity{GarminActivityID: 1, StartTimeUTC: "2026-07-11 06:00:00", RawJSON: "{}"}); err != nil { t.Fatalf("UpsertActivity(b): %v", err) } if err := db.DeleteUser(ctx, userA); err != nil { t.Fatalf("DeleteUser(a): %v", err) } if _, found, err := db.GetUserBySub(ctx, "sub-b"); err != nil || !found { t.Fatalf("expected userB to survive userA's deletion, found=%v err=%v", found, err) } profileB, err := db.GetProfile(ctx, userB) if err != nil { t.Fatalf("GetProfile(b) after deleting a: %v", err) } if profileB.Name != "B" { t.Errorf("userB's profile changed after deleting userA: %+v", profileB) } kindsB, err := db.ListWorkoutKinds(ctx, userB, false) if err != nil || len(kindsB) != 8 { t.Fatalf("userB's taxonomy affected by deleting userA: len=%d err=%v", len(kindsB), err) } activitiesB, err := db.ListActivities(ctx, userB, ActivityFilter{}) if err != nil || len(activitiesB) != 1 { t.Fatalf("userB's activities affected by deleting userA: len=%d err=%v", len(activitiesB), err) } } // TestIsolation_ActivitiesMissingWorkoutNeverLeaksAcrossUsers confirms // ActivitiesMissingWorkout/CountActivitiesMissingWorkout only ever surface // userID's own pending-workout activities, never another user's, even // though both users have activities in the exact shape (workout_id set, // details fetched, workout_raw_json still NULL) the query selects for. func TestIsolation_ActivitiesMissingWorkoutNeverLeaksAcrossUsers(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) } workoutID := int64(999) idA, err := db.UpsertActivity(ctx, userA, Activity{ GarminActivityID: 1, WorkoutID: &workoutID, StartTimeUTC: "2026-07-01 06:00:00", RawJSON: "{}", }) if err != nil { t.Fatalf("UpsertActivity(a): %v", err) } if err := db.SetActivityDetails(ctx, userA, idA, "{}"); err != nil { t.Fatalf("SetActivityDetails(a): %v", err) } idB, err := db.UpsertActivity(ctx, userB, Activity{ GarminActivityID: 1, WorkoutID: &workoutID, StartTimeUTC: "2026-07-01 06:00:00", RawJSON: "{}", }) if err != nil { t.Fatalf("UpsertActivity(b): %v", err) } if err := db.SetActivityDetails(ctx, userB, idB, "{}"); err != nil { t.Fatalf("SetActivityDetails(b): %v", err) } pendingA, err := db.ActivitiesMissingWorkout(ctx, userA, 10) if err != nil { t.Fatalf("ActivitiesMissingWorkout(a): %v", err) } if len(pendingA) != 1 || pendingA[0].ID != idA { t.Fatalf("ActivitiesMissingWorkout(a) = %+v, want only userA's own activity (id %d)", pendingA, idA) } countA, err := db.CountActivitiesMissingWorkout(ctx, userA) if err != nil { t.Fatalf("CountActivitiesMissingWorkout(a): %v", err) } if countA != 1 { t.Fatalf("CountActivitiesMissingWorkout(a) = %d, want 1 (userB's identical-looking activity must not be counted)", countA) } // Resolving userA's workout must not affect userB's pending activity. if err := db.SetActivityWorkout(ctx, userA, idA, `{"segments":[]}`); err != nil { t.Fatalf("SetActivityWorkout(a): %v", err) } pendingB, err := db.ActivitiesMissingWorkout(ctx, userB, 10) if err != nil { t.Fatalf("ActivitiesMissingWorkout(b): %v", err) } if len(pendingB) != 1 || pendingB[0].ID != idB { t.Fatalf("ActivitiesMissingWorkout(b) = %+v, want userB's activity unaffected by userA's SetActivityWorkout", pendingB) } countB, err := db.CountActivitiesMissingWorkout(ctx, userB) if err != nil { t.Fatalf("CountActivitiesMissingWorkout(b): %v", err) } if countB != 1 { t.Fatalf("CountActivitiesMissingWorkout(b) = %d, want 1 (unaffected by userA's SetActivityWorkout call)", countB) } } // TestIsolation_MarkGarminConnectedNeverLeaksAcrossUsers confirms marking // one user's Garmin connection never sets another user's flag. func TestIsolation_MarkGarminConnectedNeverLeaksAcrossUsers(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.MarkGarminConnected(ctx, userA); err != nil { t.Fatalf("MarkGarminConnected(a): %v", err) } profileB, err := db.GetProfile(ctx, userB) if err != nil { t.Fatalf("GetProfile(b): %v", err) } if profileB.GarminConnectedAt != nil { t.Fatal("userA's MarkGarminConnected call leaked into userB's profile") } }