2026-07-25 17:37:40 +02:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-27 06:44:39 +02:00
|
|
|
runID, err := db.StartSyncRun(ctx, userA, SyncKindFull)
|
2026-07-25 17:37:40 +02:00
|
|
|
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")
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-26 10:17:21 +02:00
|
|
|
|
|
|
|
|
// 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)
|
|
|
|
|
}
|
2026-08-04 16:11:23 +02:00
|
|
|
if _, err := db.GetProfile(ctx, userB); err != nil {
|
2026-07-26 10:17:21 +02:00
|
|
|
t.Fatalf("GetProfile(b) after deleting a: %v", err)
|
|
|
|
|
}
|
2026-08-04 16:11:23 +02:00
|
|
|
userBRow, found, err := db.GetUserBySub(ctx, "sub-b")
|
|
|
|
|
if err != nil || !found || userBRow.Name != "B" {
|
|
|
|
|
t.Errorf("userB's account changed after deleting userA: %+v (found=%v err=%v)", userBRow, found, err)
|
2026-07-26 10:17:21 +02:00
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-26 12:21:42 +02:00
|
|
|
|
2026-07-27 09:01:10 +02:00
|
|
|
// 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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-27 19:16:43 +02:00
|
|
|
// Calling SetActivityWorkoutNotFound with the WRONG user's ID against
|
|
|
|
|
// userA's activity (idA) must be a no-op: the WHERE id = ? AND user_id = ?
|
|
|
|
|
// won't match any row, so it should neither error nor mark idA not-found.
|
|
|
|
|
// (This runs before the SetActivityWorkout resolution below, while idA
|
|
|
|
|
// is still pending -- once workout_raw_json is set, idA drops out of
|
|
|
|
|
// ActivitiesMissingWorkout regardless of workout_not_found_at, which
|
|
|
|
|
// would make the "still appears" assertion vacuous.)
|
|
|
|
|
if err := db.SetActivityWorkoutNotFound(ctx, userB, idA); err != nil {
|
|
|
|
|
t.Fatalf("SetActivityWorkoutNotFound(b, idA): %v", err)
|
|
|
|
|
}
|
|
|
|
|
pendingA, err = db.ActivitiesMissingWorkout(ctx, userA, 10)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ActivitiesMissingWorkout(a) after userB's mismatched SetActivityWorkoutNotFound call: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(pendingA) != 1 || pendingA[0].ID != idA {
|
|
|
|
|
t.Fatalf("ActivitiesMissingWorkout(a) = %+v, want idA still pending -- userB's mismatched call must not mark it not-found", pendingA)
|
|
|
|
|
}
|
|
|
|
|
countA, err = db.CountActivitiesMissingWorkout(ctx, userA)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CountActivitiesMissingWorkout(a) after userB's mismatched SetActivityWorkoutNotFound call: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if countA != 1 {
|
|
|
|
|
t.Fatalf("CountActivitiesMissingWorkout(a) = %d, want 1 (unaffected by userB's mismatched-user SetActivityWorkoutNotFound call)", countA)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Now the CORRECT user marks their own activity not-found: idA must
|
|
|
|
|
// disappear from userA's own results, and userB's own idB (still
|
|
|
|
|
// pending) must remain wholly unaffected.
|
|
|
|
|
if err := db.SetActivityWorkoutNotFound(ctx, userA, idA); err != nil {
|
|
|
|
|
t.Fatalf("SetActivityWorkoutNotFound(a, idA): %v", err)
|
|
|
|
|
}
|
|
|
|
|
pendingA, err = db.ActivitiesMissingWorkout(ctx, userA, 10)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ActivitiesMissingWorkout(a) after userA marked idA not-found: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(pendingA) != 0 {
|
|
|
|
|
t.Fatalf("ActivitiesMissingWorkout(a) = %+v, want empty after userA marked their own idA not-found", pendingA)
|
|
|
|
|
}
|
|
|
|
|
countA, err = db.CountActivitiesMissingWorkout(ctx, userA)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CountActivitiesMissingWorkout(a) after userA marked idA not-found: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if countA != 0 {
|
|
|
|
|
t.Fatalf("CountActivitiesMissingWorkout(a) = %d, want 0 after userA marked their own idA not-found", countA)
|
|
|
|
|
}
|
|
|
|
|
pendingB, err := db.ActivitiesMissingWorkout(ctx, userB, 10)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("ActivitiesMissingWorkout(b) after userA marked idA not-found: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if len(pendingB) != 1 || pendingB[0].ID != idB {
|
|
|
|
|
t.Fatalf("ActivitiesMissingWorkout(b) = %+v, want userB's idB still pending, unaffected by userA's SetActivityWorkoutNotFound call", pendingB)
|
|
|
|
|
}
|
|
|
|
|
countB, err := db.CountActivitiesMissingWorkout(ctx, userB)
|
|
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("CountActivitiesMissingWorkout(b) after userA marked idA not-found: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if countB != 1 {
|
|
|
|
|
t.Fatalf("CountActivitiesMissingWorkout(b) = %d, want 1 (unaffected by userA's SetActivityWorkoutNotFound call)", countB)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Resolving userA's workout (now moot for idA specifically, since it was
|
|
|
|
|
// already marked not-found above, but still exercises SetActivityWorkout
|
|
|
|
|
// itself) must not affect userB's pending activity.
|
2026-07-27 09:01:10 +02:00
|
|
|
if err := db.SetActivityWorkout(ctx, userA, idA, `{"segments":[]}`); err != nil {
|
|
|
|
|
t.Fatalf("SetActivityWorkout(a): %v", err)
|
|
|
|
|
}
|
2026-07-27 19:16:43 +02:00
|
|
|
pendingB, err = db.ActivitiesMissingWorkout(ctx, userB, 10)
|
2026-07-27 09:01:10 +02:00
|
|
|
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)
|
|
|
|
|
}
|
2026-07-27 19:16:43 +02:00
|
|
|
countB, err = db.CountActivitiesMissingWorkout(ctx, userB)
|
2026-07-27 09:01:10 +02:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-26 12:21:42 +02:00
|
|
|
// 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")
|
|
|
|
|
}
|
|
|
|
|
}
|