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:
2026-07-27 19:16:43 +02:00
parent 6f6d1ea5e0
commit c40f6634f2

View File

@@ -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)
} }