store: scope kind assignments, laps, and samples via activity ownership

None of these three tables gained their own user_id column -- they're
always accessed through a specific activity, so ownership is checked via a
join/subquery against activities.user_id instead.
This commit is contained in:
2026-07-25 17:27:51 +02:00
parent f82549524b
commit 47dbf4e446
5 changed files with 163 additions and 75 deletions

View File

@@ -143,7 +143,7 @@ func TestKindAssignment_AppendOnlyHistoryAndCurrentView(t *testing.T) {
}
// First: rule engine says needs_review (ambiguous).
if _, err := db.InsertKindAssignment(ctx, KindAssignment{
if _, err := db.InsertKindAssignment(ctx, userID, KindAssignment{
ActivityID: activityID,
AssignmentSource: AssignmentSourceRuleEngine,
Status: AssignmentStatusNeedsReview,
@@ -152,7 +152,7 @@ func TestKindAssignment_AppendOnlyHistoryAndCurrentView(t *testing.T) {
t.Fatalf("InsertKindAssignment (rule engine): %v", err)
}
queue, err := db.ReviewQueue(ctx)
queue, err := db.ReviewQueue(ctx, userID)
if err != nil {
t.Fatalf("ReviewQueue: %v", err)
}
@@ -161,7 +161,7 @@ func TestKindAssignment_AppendOnlyHistoryAndCurrentView(t *testing.T) {
}
// Then: user manually resolves it.
if _, err := db.InsertKindAssignment(ctx, KindAssignment{
if _, err := db.InsertKindAssignment(ctx, userID, KindAssignment{
ActivityID: activityID,
WorkoutKindID: &kindID,
AssignmentSource: AssignmentSourceManual,
@@ -170,7 +170,7 @@ func TestKindAssignment_AppendOnlyHistoryAndCurrentView(t *testing.T) {
t.Fatalf("InsertKindAssignment (manual): %v", err)
}
queue, err = db.ReviewQueue(ctx)
queue, err = db.ReviewQueue(ctx, userID)
if err != nil {
t.Fatalf("ReviewQueue after resolve: %v", err)
}
@@ -178,7 +178,7 @@ func TestKindAssignment_AppendOnlyHistoryAndCurrentView(t *testing.T) {
t.Fatalf("expected 0 items in review queue after manual resolve, got %d", len(queue))
}
current, ok, err := db.CurrentAssignment(ctx, activityID)
current, ok, err := db.CurrentAssignment(ctx, userID, activityID)
if err != nil || !ok {
t.Fatalf("CurrentAssignment: ok=%v err=%v", ok, err)
}
@@ -195,7 +195,7 @@ func TestKindAssignment_AppendOnlyHistoryAndCurrentView(t *testing.T) {
t.Errorf("expected 2 historical assignment rows (rule engine + manual), got %d", historyCount)
}
forKind, err := db.AssignmentsForKind(ctx, kindID)
forKind, err := db.AssignmentsForKind(ctx, userID, kindID)
if err != nil {
t.Fatalf("AssignmentsForKind: %v", err)
}
@@ -225,15 +225,15 @@ func TestReplaceLapsIsIdempotent(t *testing.T) {
{LapIndex: 1, RawJSON: "{}"},
{LapIndex: 2, RawJSON: "{}"},
}
if err := db.ReplaceLaps(ctx, activityID, laps); err != nil {
if err := db.ReplaceLaps(ctx, userID, activityID, laps); err != nil {
t.Fatalf("ReplaceLaps (first): %v", err)
}
// Re-sync with a different set (e.g. corrected data) should fully replace, not append.
if err := db.ReplaceLaps(ctx, activityID, laps[:1]); err != nil {
if err := db.ReplaceLaps(ctx, userID, activityID, laps[:1]); err != nil {
t.Fatalf("ReplaceLaps (second): %v", err)
}
got, err := db.LapsForActivity(ctx, activityID)
got, err := db.LapsForActivity(ctx, userID, activityID)
if err != nil {
t.Fatalf("LapsForActivity: %v", err)
}
@@ -309,7 +309,7 @@ func TestForeignKeyEnforcementPostMigration(t *testing.T) {
}
// Inserting a kind_assignment with a valid FK reference should succeed.
assignmentID, err := db.InsertKindAssignment(ctx, KindAssignment{
assignmentID, err := db.InsertKindAssignment(ctx, userID, KindAssignment{
ActivityID: activityID,
WorkoutKindID: &seedKindID,
AssignmentSource: AssignmentSourceManual,
@@ -325,7 +325,7 @@ func TestForeignKeyEnforcementPostMigration(t *testing.T) {
// Inserting a kind_assignment with a nonexistent workout_kind_id should
// be rejected by the FK constraint, proving FK enforcement is ON.
nonexistentKindID := int64(999999)
_, err = db.InsertKindAssignment(ctx, KindAssignment{
_, err = db.InsertKindAssignment(ctx, userID, KindAssignment{
ActivityID: activityID,
WorkoutKindID: &nonexistentKindID,
AssignmentSource: AssignmentSourceManual,
@@ -568,6 +568,38 @@ func TestRebuildMigrationsPreserveForeignKeyReferences(t *testing.T) {
}
}
func TestCurrentAssignment_ScopedToOwningUser(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)
}
activityID, err := db.UpsertActivity(ctx, userA, Activity{GarminActivityID: 1, StartTimeUTC: "2026-07-11 05:00:00", RawJSON: "{}"})
if err != nil {
t.Fatalf("UpsertActivity: %v", err)
}
if _, err := db.InsertKindAssignment(ctx, userA, KindAssignment{
ActivityID: activityID, AssignmentSource: AssignmentSourceRuleEngine, Status: AssignmentStatusNeedsReview, CandidateKindsJSON: "[]",
}); err != nil {
t.Fatalf("InsertKindAssignment: %v", err)
}
if _, found, err := db.CurrentAssignment(ctx, userB, activityID); err != nil || found {
t.Fatalf("expected userB to not see userA's activity assignment, found=%v err=%v", found, err)
}
if _, err := db.InsertKindAssignment(ctx, userB, KindAssignment{
ActivityID: activityID, AssignmentSource: AssignmentSourceManual, Status: AssignmentStatusAssigned, CandidateKindsJSON: "[]",
}); err == nil {
t.Fatal("expected InsertKindAssignment to reject an activity that doesn't belong to userB")
}
}
func contains(s string, substrs ...string) bool {
lower := strings.ToLower(s)
for _, substr := range substrs {