test(store): cover SetActivityWorkoutNotFound adversarially for user isolation
SetActivityWorkoutNotFound is a per-user-scoped store method but never got the adversarial cross-user coverage CLAUDE.md's testing conventions require for such features. Extend the existing isolation test with a mismatched-user call (must no-op) and a correct-user call (must exclude the activity from that user's own ActivitiesMissingWorkout/count without touching the other user's pending activity).
This commit is contained in:
@@ -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)
|
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 {
|
if err := db.SetActivityWorkout(ctx, userA, idA, `{"segments":[]}`); err != nil {
|
||||||
t.Fatalf("SetActivityWorkout(a): %v", err)
|
t.Fatalf("SetActivityWorkout(a): %v", err)
|
||||||
}
|
}
|
||||||
pendingB, err := db.ActivitiesMissingWorkout(ctx, userB, 10)
|
pendingB, err = db.ActivitiesMissingWorkout(ctx, userB, 10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("ActivitiesMissingWorkout(b): %v", err)
|
t.Fatalf("ActivitiesMissingWorkout(b): %v", err)
|
||||||
}
|
}
|
||||||
if len(pendingB) != 1 || pendingB[0].ID != idB {
|
if len(pendingB) != 1 || pendingB[0].ID != idB {
|
||||||
t.Fatalf("ActivitiesMissingWorkout(b) = %+v, want userB's activity unaffected by userA's SetActivityWorkout", pendingB)
|
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 {
|
if err != nil {
|
||||||
t.Fatalf("CountActivitiesMissingWorkout(b): %v", err)
|
t.Fatalf("CountActivitiesMissingWorkout(b): %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user