package store_test import ( "context" "testing" "time" "github.com/jackc/pgx/v5/pgxpool" "github.com/stretchr/testify/require" "gitea.d-ma.be/mathias/tapir/internal/adapters/store" ) // resetInvitations clears the invitations table between cases. It is not in the // shared resetDB TRUNCATE list (invitations is not user-owned and has no FK to // users), so the invite tests wipe it themselves. func resetInvitations(t *testing.T, p *pgxpool.Pool) { t.Helper() _, err := p.Exec(context.Background(), `TRUNCATE invitations`) require.NoError(t, err) } func TestCreateInvitationReturnsUsableToken(t *testing.T) { s, p := newStore(t), rawPool(t) resetInvitations(t, p) ctx := context.Background() token, err := s.CreateInvitation(ctx, "new@example.com", time.Hour) require.NoError(t, err) require.Len(t, token, 64, "32 random bytes hex-encoded") // Peek does not consume: the same token previews twice. email, err := s.PeekInvitation(ctx, token) require.NoError(t, err) require.Equal(t, "new@example.com", email) email, err = s.PeekInvitation(ctx, token) require.NoError(t, err) require.Equal(t, "new@example.com", email) } func TestCreateInvitationTokensAreUnique(t *testing.T) { s, p := newStore(t), rawPool(t) resetInvitations(t, p) ctx := context.Background() t1, err := s.CreateInvitation(ctx, "a@example.com", time.Hour) require.NoError(t, err) t2, err := s.CreateInvitation(ctx, "b@example.com", time.Hour) require.NoError(t, err) require.NotEqual(t, t1, t2) } func TestClaimInvitationHappyPath(t *testing.T) { s, p := newStore(t), rawPool(t) resetInvitations(t, p) ctx := context.Background() token, err := s.CreateInvitation(ctx, "claim@example.com", time.Hour) require.NoError(t, err) email, err := s.ClaimInvitation(ctx, token) require.NoError(t, err) require.Equal(t, "claim@example.com", email) } func TestClaimInvitationIsSingleUse(t *testing.T) { s, p := newStore(t), rawPool(t) resetInvitations(t, p) ctx := context.Background() token, err := s.CreateInvitation(ctx, "once@example.com", time.Hour) require.NoError(t, err) _, err = s.ClaimInvitation(ctx, token) require.NoError(t, err) // Second claim fails — already used. _, err = s.ClaimInvitation(ctx, token) require.ErrorIs(t, err, store.ErrNotFound) // And a used token no longer previews. _, err = s.PeekInvitation(ctx, token) require.ErrorIs(t, err, store.ErrNotFound) } func TestClaimInvitationExpired(t *testing.T) { s, p := newStore(t), rawPool(t) resetInvitations(t, p) ctx := context.Background() // Negative ttl => already expired. token, err := s.CreateInvitation(ctx, "old@example.com", -time.Minute) require.NoError(t, err) _, err = s.PeekInvitation(ctx, token) require.ErrorIs(t, err, store.ErrNotFound) _, err = s.ClaimInvitation(ctx, token) require.ErrorIs(t, err, store.ErrNotFound) } func TestClaimInvitationNotFound(t *testing.T) { s, p := newStore(t), rawPool(t) resetInvitations(t, p) ctx := context.Background() _, err := s.ClaimInvitation(ctx, "does-not-exist") require.ErrorIs(t, err, store.ErrNotFound) _, err = s.PeekInvitation(ctx, "does-not-exist") require.ErrorIs(t, err, store.ErrNotFound) }