Make the platform list shared across users; keep games private

Platforms no longer have a user_id: there's one global list, unique by
name, that every authenticated user reads from and writes to. Games are
unchanged - still private per user, still referencing platform_id.

- Migration: dropped platform.user_id and its FK, UNIQUE(user_id, name) ->
  UNIQUE(name), index on change_seq alone instead of (user_id, change_seq).
  Amended the one init migration in place rather than stacking a second one,
  since nothing is deployed yet.
- db/query.sql: platform queries no longer take a user_id parameter.
  DeletePlatform's NOT EXISTS guard already checked games from any user (it
  never filtered the subquery by user_id), so deleting a shared platform is
  correctly blocked by any user's games on it, not just the deleter's.
- routes.go: platform handlers still require auth (the list isn't public to
  anonymous requests) but no longer scope by user.
- Tests: updated the mocks for the new signatures, added
  TestListPlatformsIsSharedAcrossUsers.

Verified against a real Postgres: user A creates "PC", user B sees it
immediately, both can add games against it, each only sees their own
games, deleting the platform is blocked while either user has a game on
it, and the name stays globally unique.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
SansanandClaude Sonnet 5 committed 2026-10-03 11:34:47 +02:00
1 parent 5072919b61
commit 7fbd403d30
8 files changed
+93 -86

No files matched your search

+27 -7
View File
@@ -119,10 +119,10 @@ func TestDeletePlatformConflictWhenReferencedByGames(t *testing.T) {
// the 0-rows result itself and turn it into 409, not just pass through
// whatever error (if any) the query returns.
mockDB := authedMock()
mockDB.GetPlatformFunc = func(ctx context.Context, arg db.GetPlatformParams) (db.GetPlatformRow, error) {
return db.GetPlatformRow{ID: arg.ID, Name: "PC"}, nil
mockDB.GetPlatformFunc = func(ctx context.Context, id uuid.UUID) (db.GetPlatformRow, error) {
return db.GetPlatformRow{ID: id, Name: "PC"}, nil
}
mockDB.DeletePlatformFunc = func(ctx context.Context, arg db.DeletePlatformParams) (int64, error) {
mockDB.DeletePlatformFunc = func(ctx context.Context, id uuid.UUID) (int64, error) {
return 0, nil
}
@@ -135,7 +135,7 @@ func TestDeletePlatformConflictWhenReferencedByGames(t *testing.T) {
func TestDeletePlatformNotFound(t *testing.T) {
mockDB := authedMock()
mockDB.GetPlatformFunc = func(ctx context.Context, arg db.GetPlatformParams) (db.GetPlatformRow, error) {
mockDB.GetPlatformFunc = func(ctx context.Context, id uuid.UUID) (db.GetPlatformRow, error) {
return db.GetPlatformRow{}, pgx.ErrNoRows
}
@@ -161,9 +161,8 @@ func TestCreateGameRejectsScoreOutOfRange(t *testing.T) {
func TestCreatePlatform(t *testing.T) {
mockDB := authedMock()
mockDB.CreatePlatformFunc = func(ctx context.Context, arg db.CreatePlatformParams) (db.CreatePlatformRow, error) {
assert.Equal(t, testUserID, arg.UserID)
return db.CreatePlatformRow{ID: uuid.New(), Name: arg.Name}, nil
mockDB.CreatePlatformFunc = func(ctx context.Context, name string) (db.CreatePlatformRow, error) {
return db.CreatePlatformRow{ID: uuid.New(), Name: name}, nil
}
e, _ := newTestAPI(mockDB)
@@ -174,6 +173,27 @@ func TestCreatePlatform(t *testing.T) {
assert.Contains(t, rec.Body.String(), "Sega Genesis")
}
func TestListPlatformsIsSharedAcrossUsers(t *testing.T) {
// Unlike games, the platform list has no user_id to scope by: every
// authenticated user sees the same rows. ListPlatformsFunc taking no
// user id (beyond ctx) is itself part of what's being checked here -
// this test would fail to compile if a user_id parameter crept back in.
calls := 0
mockDB := authedMock()
mockDB.ListPlatformsFunc = func(ctx context.Context) ([]db.ListPlatformsRow, error) {
calls++
return []db.ListPlatformsRow{{ID: uuid.New(), Name: "PC"}}, nil
}
e, _ := newTestAPI(mockDB)
rec := httptest.NewRecorder()
e.ServeHTTP(rec, authedRequest(http.MethodGet, "/api/platforms", ""))
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "PC")
assert.Equal(t, 1, calls)
}
func TestRegisterDuplicateEmailConflicts(t *testing.T) {
mockDB := &MockQuerier{
CreateUserFunc: func(ctx context.Context, arg db.CreateUserParams) (db.CreateUserRow, error) {