diff --git a/backend/internal/store/isolation_test.go b/backend/internal/store/isolation_test.go index 443c668..bd8f1fe 100644 --- a/backend/internal/store/isolation_test.go +++ b/backend/internal/store/isolation_test.go @@ -262,18 +262,80 @@ func TestIsolation_ActivitiesMissingWorkoutNeverLeaksAcrossUsers(t *testing.T) { 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. + // 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. if err := db.SetActivityWorkout(ctx, userA, idA, `{"segments":[]}`); err != nil { t.Fatalf("SetActivityWorkout(a): %v", err) } - pendingB, err := db.ActivitiesMissingWorkout(ctx, userB, 10) + 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) + countB, err = db.CountActivitiesMissingWorkout(ctx, userB) if err != nil { t.Fatalf("CountActivitiesMissingWorkout(b): %v", err) }