feat(store): user_identities map + UserBySubject/RegisterUser (ADR-012)
Add the Dex-subject -> tapir-user_id bridge for multi-user Stage 1. Migration 004 creates user_identities (dex_subject PK, user_id UNIQUE FK ON DELETE CASCADE). It is intentionally NOT RLS-enabled: it holds no user data and must be readable BEFORE a user_id is known (the lookup is what yields the id used to set tapir.current_user_id). RLS here would be a chicken-and-egg deadlock; data isolation stays on the user-owned tables. UserBySubject resolves subject -> user_id as a plain pool query (pre-scope, no withUser). RegisterUser generates the UUID app-side (stdlib crypto/rand, no new dep) so the forced-RLS WITH CHECK (id = GUC) passes, then inserts the users row via withUser(newID) and the identity row in the same transaction. Re-registration of a subject errors with ErrSubjectRegistered. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
package store_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"gitea.d-ma.be/mathias/tapir/internal/adapters/store"
|
||||
)
|
||||
|
||||
const (
|
||||
subjectA = "dex|alice-123"
|
||||
subjectB = "dex|bob-456"
|
||||
)
|
||||
|
||||
func TestUserBySubjectUnknownReturnsNotFound(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newStore(t)
|
||||
resetDB(t, rawPool(t))
|
||||
|
||||
id, found, err := s.UserBySubject(ctx, subjectA)
|
||||
require.NoError(t, err)
|
||||
require.False(t, found)
|
||||
require.Empty(t, id)
|
||||
}
|
||||
|
||||
func TestRegisterUserCreatesUserAndIdentity(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newStore(t)
|
||||
p := rawPool(t)
|
||||
resetDB(t, p)
|
||||
|
||||
id, err := s.RegisterUser(ctx, subjectA, "Alice")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, id)
|
||||
|
||||
// Exactly one users row with the returned id and the given display name.
|
||||
var users int
|
||||
var name string
|
||||
require.NoError(t, p.QueryRow(ctx,
|
||||
`SELECT count(*), coalesce(max(display_name), '') FROM users WHERE id = $1`, id).
|
||||
Scan(&users, &name))
|
||||
require.Equal(t, 1, users)
|
||||
require.Equal(t, "Alice", name)
|
||||
|
||||
// Exactly one identity row mapping the subject to that id.
|
||||
var idents int
|
||||
require.NoError(t, p.QueryRow(ctx,
|
||||
`SELECT count(*) FROM user_identities WHERE dex_subject = $1 AND user_id = $2`,
|
||||
subjectA, id).Scan(&idents))
|
||||
require.Equal(t, 1, idents)
|
||||
|
||||
// And it now resolves straight through.
|
||||
got, found, err := s.UserBySubject(ctx, subjectA)
|
||||
require.NoError(t, err)
|
||||
require.True(t, found)
|
||||
require.Equal(t, id, got)
|
||||
}
|
||||
|
||||
func TestRegisterUserRejectsDuplicateSubject(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newStore(t)
|
||||
p := rawPool(t)
|
||||
resetDB(t, p)
|
||||
|
||||
first, err := s.RegisterUser(ctx, subjectA, "Alice")
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = s.RegisterUser(ctx, subjectA, "Alice Again")
|
||||
require.Error(t, err)
|
||||
require.True(t, errors.Is(err, store.ErrSubjectRegistered))
|
||||
|
||||
// No second user was created; the original mapping is intact.
|
||||
var users, idents int
|
||||
require.NoError(t, p.QueryRow(ctx, `SELECT count(*) FROM users`).Scan(&users))
|
||||
require.NoError(t, p.QueryRow(ctx, `SELECT count(*) FROM user_identities`).Scan(&idents))
|
||||
require.Equal(t, 1, users)
|
||||
require.Equal(t, 1, idents)
|
||||
|
||||
got, found, err := s.UserBySubject(ctx, subjectA)
|
||||
require.NoError(t, err)
|
||||
require.True(t, found)
|
||||
require.Equal(t, first, got)
|
||||
}
|
||||
|
||||
func TestRegisterUserDistinctSubjectsGetDistinctUsers(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
s := newStore(t)
|
||||
p := rawPool(t)
|
||||
resetDB(t, p)
|
||||
|
||||
idA, err := s.RegisterUser(ctx, subjectA, "Alice")
|
||||
require.NoError(t, err)
|
||||
idB, err := s.RegisterUser(ctx, subjectB, "Bob")
|
||||
require.NoError(t, err)
|
||||
require.NotEqual(t, idA, idB)
|
||||
|
||||
var users int
|
||||
require.NoError(t, p.QueryRow(ctx, `SELECT count(*) FROM users`).Scan(&users))
|
||||
require.Equal(t, 2, users)
|
||||
}
|
||||
Reference in New Issue
Block a user