package waitlist_test import ( "context" "os" "testing" pbaero "github.com/flowy-live/llink/genproto/aero" "github.com/flowy-live/llink/internal/testhelper" "github.com/flowy-live/llink/internal/waitlist" "github.com/flowy-live/llink/internal/waitlist/mocks" "github.com/jackc/pgx/v5/pgxpool" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" ) var dbPool *pgxpool.Pool func TestMain(m *testing.M) { dbPool = testhelper.SetupTestDB() defer testhelper.TeardownTestDB() ret := m.Run() os.Exit(ret) } func newTestService(t *testing.T) waitlist.Service { ctrl := gomock.NewController(t) mockAero := mocks.NewMockPrimaryClient(ctrl) mockAero.EXPECT(). ShootEmail(gomock.Any(), gomock.Any()). Return(&pbaero.ShootEmailResponse{}, nil). AnyTimes() return waitlist.NewService(dbPool, mockAero) } func TestWaitlistService_AddAndGet(t *testing.T) { ctx := context.Background() svc := newTestService(t) email := "newuser@example.com" metadata := map[string]string{"source": "landing-page", "plan": "pro"} // Add to waitlist err := svc.AddToWaitlist(ctx, email, metadata) assert.NoError(t, err) // Get entry by email entry, err := svc.GetWaitlistEntryByEmail(ctx, email) assert.NoError(t, err) assert.Equal(t, email, entry.Email) assert.Equal(t, "landing-page", entry.Metadata["source"]) assert.Equal(t, "pro", entry.Metadata["plan"]) assert.NotZero(t, entry.CreatedAt) assert.Nil(t, entry.InvitedAt) // Clean up _, err = dbPool.Exec(ctx, `DELETE FROM waitlist_entries WHERE email = $1`, email) assert.NoError(t, err) } func TestWaitlistService_AddDuplicate(t *testing.T) { ctx := context.Background() svc := newTestService(t) email := "duplicate@example.com" err := svc.AddToWaitlist(ctx, email, nil) assert.NoError(t, err) // Adding the same email again should return AlreadyInWaitlistError err = svc.AddToWaitlist(ctx, email, nil) assert.ErrorIs(t, err, waitlist.AlreadyInWaitlistError) // Clean up _, err = dbPool.Exec(ctx, `DELETE FROM waitlist_entries WHERE email = $1`, email) assert.NoError(t, err) } func TestWaitlistService_AddNilMetadata(t *testing.T) { ctx := context.Background() svc := newTestService(t) email := "nilmeta@example.com" err := svc.AddToWaitlist(ctx, email, nil) assert.NoError(t, err) entry, err := svc.GetWaitlistEntryByEmail(ctx, email) assert.NoError(t, err) assert.NotNil(t, entry.Metadata) // Clean up _, err = dbPool.Exec(ctx, `DELETE FROM waitlist_entries WHERE email = $1`, email) assert.NoError(t, err) } func TestWaitlistService_GetEntryNotFound(t *testing.T) { ctx := context.Background() svc := newTestService(t) _, err := svc.GetWaitlistEntryByEmail(ctx, "nonexistent@example.com") assert.ErrorIs(t, err, waitlist.EntryNotFoundError) } func TestWaitlistService_MarkInvited(t *testing.T) { ctx := context.Background() svc := newTestService(t) email := "invite-me@example.com" err := svc.AddToWaitlist(ctx, email, nil) assert.NoError(t, err) // Mark as invited err = svc.MarkWaitlistEntryInvited(ctx, email) assert.NoError(t, err) // Verify invited_at is set entry, err := svc.GetWaitlistEntryByEmail(ctx, email) assert.NoError(t, err) assert.NotNil(t, entry.InvitedAt) // Clean up _, err = dbPool.Exec(ctx, `DELETE FROM waitlist_entries WHERE email = $1`, email) assert.NoError(t, err) } func TestWaitlistService_MarkInvitedNotFound(t *testing.T) { ctx := context.Background() svc := newTestService(t) err := svc.MarkWaitlistEntryInvited(ctx, "ghost@example.com") assert.ErrorIs(t, err, waitlist.EntryNotFoundError) } func TestWaitlistService_GetWaitlistFilters(t *testing.T) { ctx := context.Background() svc := newTestService(t) uninvitedEmail := "uninvited-filter@example.com" invitedEmail := "invited-filter@example.com" // Add two entries err := svc.AddToWaitlist(ctx, uninvitedEmail, nil) assert.NoError(t, err) err = svc.AddToWaitlist(ctx, invitedEmail, nil) assert.NoError(t, err) // Mark one as invited err = svc.MarkWaitlistEntryInvited(ctx, invitedEmail) assert.NoError(t, err) // Filter: all entries, err := svc.GetWaitlist(ctx, waitlist.GetWaitlistFilterAll) assert.NoError(t, err) emails := extractEmails(entries) assert.Contains(t, emails, uninvitedEmail) assert.Contains(t, emails, invitedEmail) // Filter: invited only entries, err = svc.GetWaitlist(ctx, waitlist.GetWaitlistFilterInvitedOnly) assert.NoError(t, err) emails = extractEmails(entries) assert.Contains(t, emails, invitedEmail) assert.NotContains(t, emails, uninvitedEmail) // Filter: uninvited only entries, err = svc.GetWaitlist(ctx, waitlist.GetWaitlistFilterUninvitedOnly) assert.NoError(t, err) emails = extractEmails(entries) assert.Contains(t, emails, uninvitedEmail) assert.NotContains(t, emails, invitedEmail) // Clean up _, err = dbPool.Exec(ctx, `DELETE FROM waitlist_entries WHERE email IN ($1, $2)`, uninvitedEmail, invitedEmail) assert.NoError(t, err) } func TestWaitlistService_EmailNormalization(t *testing.T) { ctx := context.Background() svc := newTestService(t) // Add with uppercase email err := svc.AddToWaitlist(ctx, "UPPER@EXAMPLE.COM", nil) assert.NoError(t, err) // Should find with lowercase entry, err := svc.GetWaitlistEntryByEmail(ctx, "upper@example.com") assert.NoError(t, err) assert.Equal(t, "upper@example.com", entry.Email) // Adding with different case should be a duplicate err = svc.AddToWaitlist(ctx, "Upper@Example.Com", nil) assert.ErrorIs(t, err, waitlist.AlreadyInWaitlistError) // Clean up _, err = dbPool.Exec(ctx, `DELETE FROM waitlist_entries WHERE email = $1`, "upper@example.com") assert.NoError(t, err) } func TestWaitlistService_InvalidEmail(t *testing.T) { ctx := context.Background() svc := newTestService(t) err := svc.AddToWaitlist(ctx, "not-an-email", nil) assert.Error(t, err) _, err = svc.GetWaitlistEntryByEmail(ctx, "not-an-email") assert.Error(t, err) err = svc.MarkWaitlistEntryInvited(ctx, "not-an-email") assert.Error(t, err) } func extractEmails(entries []*waitlist.WaitlistEntry) []string { emails := make([]string, len(entries)) for i, e := range entries { emails[i] = e.Email } return emails }