Files
tapir/internal/adapters/store/connections_test.go
T
mathiasandClaude Opus 4.8 0c9531a9b8 feat(store): video_connections table + RLS + connection store methods
Add the video_connections table (data-model VIDEO_CONNECTION) with FORCE
row-level security keyed off tapir.current_user_id, identical to migration
003's per-user isolation pattern — connections are user-owned data and must
be isolated at the DB layer, not only by application WHERE clauses.

Store methods (UpsertConnection / ConnectionsForUser / DeleteConnection) all
route through withUser so RLS scopes every access. UpsertConnection is
idempotent on (user_id, provider). The OAuth refresh token never lives here;
token_ref is the opaque SecretStore reference.

Extend the RLS isolation proof to cover video_connections: seeded per user,
included in the deny-all + scoped-read assertions, and added to the
cross-user write-invisibility and survivor checks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 16:12:34 +02:00

112 lines
3.3 KiB
Go

package store_test
import (
"context"
"testing"
"github.com/stretchr/testify/require"
"gitea.d-ma.be/mathias/tapir/internal/adapters/store"
)
// seedUserRow inserts a bare users row (FK target for a connection) as the
// superuser pool, which bypasses RLS.
func seedUserRow(t *testing.T, userID string) {
t.Helper()
_, err := rawPool(t).Exec(context.Background(),
`INSERT INTO users (id) VALUES ($1) ON CONFLICT (id) DO NOTHING`, userID)
require.NoError(t, err)
}
func TestUpsertConnectionInsertsRow(t *testing.T) {
ctx := context.Background()
s := newStore(t)
p := rawPool(t)
resetDB(t, p)
seedUserRow(t, userA)
require.NoError(t, s.UpsertConnection(ctx, userA, store.Connection{
Provider: "youtube",
ProviderAccount: "chan@example.com",
TokenRef: "youtube/" + userA + "/refresh_token",
Status: "active",
}))
conns, err := s.ConnectionsForUser(ctx, userA)
require.NoError(t, err)
require.Len(t, conns, 1)
require.Equal(t, "youtube", conns[0].Provider)
require.Equal(t, "chan@example.com", conns[0].ProviderAccount)
require.Equal(t, "youtube/"+userA+"/refresh_token", conns[0].TokenRef)
require.Equal(t, "active", conns[0].Status)
require.False(t, conns[0].ConnectedAt.IsZero(), "connected_at set by the DB default")
}
func TestUpsertConnectionIsIdempotentOnUserProvider(t *testing.T) {
ctx := context.Background()
s := newStore(t)
p := rawPool(t)
resetDB(t, p)
seedUserRow(t, userA)
require.NoError(t, s.UpsertConnection(ctx, userA, store.Connection{
Provider: "youtube", TokenRef: "ref-1", Status: "active",
}))
// Re-connect the same provider: must update in place, not duplicate.
require.NoError(t, s.UpsertConnection(ctx, userA, store.Connection{
Provider: "youtube", TokenRef: "ref-2", Status: "revoked",
}))
var count int
require.NoError(t, p.QueryRow(ctx,
`SELECT count(*) FROM video_connections WHERE user_id = $1 AND provider = 'youtube'`,
userA).Scan(&count))
require.Equal(t, 1, count, "second connect must update, not duplicate")
conns, err := s.ConnectionsForUser(ctx, userA)
require.NoError(t, err)
require.Len(t, conns, 1)
require.Equal(t, "ref-2", conns[0].TokenRef, "token_ref overwritten")
require.Equal(t, "revoked", conns[0].Status, "status overwritten")
}
func TestDeleteConnection(t *testing.T) {
ctx := context.Background()
s := newStore(t)
resetDB(t, rawPool(t))
seedUserRow(t, userA)
require.NoError(t, s.UpsertConnection(ctx, userA, store.Connection{
Provider: "youtube", TokenRef: "ref", Status: "active",
}))
require.NoError(t, s.DeleteConnection(ctx, userA, "youtube"))
conns, err := s.ConnectionsForUser(ctx, userA)
require.NoError(t, err)
require.Empty(t, conns)
// Deleting an absent connection is a no-op, not an error.
require.NoError(t, s.DeleteConnection(ctx, userA, "youtube"))
}
func TestConnectionsForUserIsScoped(t *testing.T) {
ctx := context.Background()
s := newStore(t)
resetDB(t, rawPool(t))
seedUserRow(t, userA)
seedUserRow(t, userB)
require.NoError(t, s.UpsertConnection(ctx, userA, store.Connection{
Provider: "youtube", TokenRef: "a-ref", Status: "active",
}))
// User B must not see user A's connection.
connsB, err := s.ConnectionsForUser(ctx, userB)
require.NoError(t, err)
require.Empty(t, connsB, "user B must not see user A's connections")
connsA, err := s.ConnectionsForUser(ctx, userA)
require.NoError(t, err)
require.Len(t, connsA, 1)
}