Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,13 +105,15 @@ export function getUserDisplayName(user: User | null, fallback = 'User'): string
export function canUserClaimSpot({
hasUser,
isOrganizer,
isRegistered,
isFirstEmptySlot,
showGuestRegistration,
}: {
hasUser: boolean;
isOrganizer: boolean;
isRegistered: boolean;
isFirstEmptySlot: boolean;
showGuestRegistration: boolean;
}): boolean {
return hasUser && isOrganizer && isFirstEmptySlot && showGuestRegistration;
return hasUser && (isOrganizer || isRegistered) && isFirstEmptySlot && showGuestRegistration;
}
1 change: 1 addition & 0 deletions src/pages/EventDetailPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -971,6 +971,7 @@ export function EventDetailPage() {
canUserClaimSpot({
hasUser: !!user,
isOrganizer,
isRegistered: !!userRegistration,
isFirstEmptySlot,
showGuestRegistration,
});
Expand Down
38 changes: 37 additions & 1 deletion src/pages/__tests__/EventDetailPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,43 @@ describe('EventDetailPage - Claim button visibility', () => {
expect(screen.getAllByRole('button', { name: /claim/i }).length).toBeGreaterThan(0);
});

it('hides Claim button when user is not the event organizer', async () => {
it('shows Claim button when non-organizer user is registered for the event', async () => {
mockUseAuth.mockReturnValue({
user: { id: 'user-456' } as User,
session: null,
loading: false,
signIn: vi.fn(),
signUp: vi.fn(),
signInWithGoogle: vi.fn(),
signInWithGoogleIdToken: vi.fn(),
signOut: vi.fn(),
isAdmin: false,
isImpersonating: false,
impersonate: vi.fn(),
stopImpersonating: vi.fn(),
resetPasswordForEmail: vi.fn(),
updatePassword: vi.fn(),
});
mockParticipantService.getParticipantsByEventId.mockResolvedValue([
{
id: 'p-1',
name: 'User',
user_id: 'user-456',
event_id: 'test-event-id',
payment_status: 'pending',
},
] as any); // eslint-disable-line @typescript-eslint/no-explicit-any

render(<EventDetailPage />);

await waitFor(() => {
expect(screen.getByText('Test Event')).toBeInTheDocument();
});

expect(screen.getAllByRole('button', { name: /claim/i }).length).toBeGreaterThan(0);
});

it('hides Claim button when user is neither organizer nor registered', async () => {
mockUseAuth.mockReturnValue({
user: { id: 'user-456' } as User,
session: null,
Expand Down
13 changes: 9 additions & 4 deletions src/pages/__tests__/canUserClaimSpot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,26 @@ import { canUserClaimSpot } from '@/lib/utils';
describe('canUserClaimSpot', () => {
const baseArgs = {
hasUser: true,
isOrganizer: true,
isOrganizer: false,
isRegistered: true,
isFirstEmptySlot: true,
showGuestRegistration: true,
};

it('returns true when organizer views first empty slot with guest registration enabled', () => {
it('returns true when registered user views first empty slot with guest registration enabled', () => {
expect(canUserClaimSpot(baseArgs)).toBe(true);
});

it('returns true when organizer is not registered but views first empty slot', () => {
expect(canUserClaimSpot({ ...baseArgs, isOrganizer: true, isRegistered: false })).toBe(true);
});

it('returns false when user is not logged in', () => {
expect(canUserClaimSpot({ ...baseArgs, hasUser: false })).toBe(false);
});

it('returns false when user is not the organizer', () => {
expect(canUserClaimSpot({ ...baseArgs, isOrganizer: false })).toBe(false);
it('returns false when user is neither organizer nor registered', () => {
expect(canUserClaimSpot({ ...baseArgs, isOrganizer: false, isRegistered: false })).toBe(false);
});

it('returns false for non-first empty slots', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
-- Allow any registered participant (not just organizers) to claim spots for guests
DROP POLICY "Event organizers can claim spots for others" ON "public"."participants";

CREATE POLICY "Registered participants can claim spots for others"
ON "public"."participants"
FOR INSERT
WITH CHECK (
claimed_by_user_id = auth.uid()
AND (
-- Event organizer can always claim
EXISTS (
SELECT 1 FROM events
WHERE events.id = participants.event_id
AND events.organizer_id = auth.uid()
)
OR
-- Any registered participant can claim
EXISTS (
SELECT 1 FROM participants AS p
WHERE p.event_id = participants.event_id
AND p.user_id = auth.uid()
)
)
);
Comment thread
michaelchu marked this conversation as resolved.
111 changes: 42 additions & 69 deletions tests/e2e/specs/participants.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
generateTestName,
createTestEvent,
getAdminDb,
enableFeatureFlagForUser,
} from '../fixtures/database';
import {
registerForEvent,
Expand Down Expand Up @@ -173,126 +174,98 @@ test.describe('Participant Registration Flow', () => {
});
});

// Skip Claiming Additional Spots tests - guest_registration feature flag is disabled
test.describe.skip('Claiming Additional Spots', () => {
test('user can claim additional spot for guest', async ({ page }) => {
// Setup
test.describe('Claiming Additional Spots', () => {
test('organizer can claim additional spot for guest', async ({ page }) => {
await register(page, {
email: generateTestEmail('claimer'),
password: 'TestPassword123!',
});


const organizerId = await getUserId(page);
await enableFeatureFlagForUser(organizerId!, 'guest_registration');

const event = await createTestEvent(organizerId!, {
name: generateTestName('Multi-Spot Event'),
max_participants: 10, // Need max_participants for claim button to appear
max_participants: 10,
});

// Register self first
await registerForEvent(page, event.id, {
name: 'Main Registrant',
// Register organizer as participant directly in DB
await getAdminDb().from('participants').insert({
event_id: event.id,
name: 'Organizer',
user_id: organizerId!,
});

// Claim additional spot
// Claim additional spot via UI
await claimAdditionalSpot(page, event.id, {
name: 'Guest 1',
});

// Verify both registrations exist
const userId = await getUserId(page);
const { data: participants } = await getAdminDb()
.from('participants')
.select('*')
.eq('event_id', event.id);

expect(participants?.length).toBeGreaterThanOrEqual(2);

// One should have user_id, one should have claimed_by_user_id
const selfRegistration = participants?.find((p) => p.user_id === userId);
const claimedSpot = participants?.find((p) => p.claimed_by_user_id === userId && p.user_id === null);
const selfRegistration = participants?.find((p) => p.user_id === organizerId);
const claimedSpot = participants?.find(
(p) => p.claimed_by_user_id === organizerId && p.user_id === null
);

expect(selfRegistration).toBeDefined();
expect(claimedSpot).toBeDefined();
});

test('claimed spots get auto-generated names', async ({ page }) => {
// Setup
test('registered non-organizer can claim spot for guest', async ({ page }) => {
// Create organizer and event
await register(page, {
email: generateTestEmail('namegen'),
email: generateTestEmail('claimorg'),
password: 'TestPassword123!',
});


const organizerId = await getUserId(page);
const event = await createTestEvent(organizerId!, {
name: generateTestName('Name Generation Event'),
max_participants: 10, // Need max_participants for claim button to appear
name: generateTestName('Non-Org Claim Event'),
max_participants: 10,
});

// Register self
await registerForEvent(page, event.id, {
name: 'John Doe',
});

// Claim spot without providing name
await claimAdditionalSpot(page, event.id, {});

// Check if auto-generated name follows pattern
const { data: participants } = await getAdminDb()
.from('participants')
.select('*')
.eq('event_id', event.id);

const claimedSpot = participants?.find((p) => p.claimed_by_user_id !== null && p.user_id === null);

if (claimedSpot) {
// Auto-generated names should follow pattern like "John Doe - 1"
expect(claimedSpot.name).toMatch(/.*\s*-\s*\d+/);
}
});
await clearAuth(page);

test('participants are ordered by registration time', async ({ page }) => {
test.setTimeout(60000); // Increase timeout for multiple claims
// Setup
// Create participant user and enable the flag for them
await register(page, {
email: generateTestEmail('slots'),
email: generateTestEmail('claimpart'),
password: 'TestPassword123!',
});

const participantId = await getUserId(page);
await enableFeatureFlagForUser(participantId!, 'guest_registration');

const organizerId = await getUserId(page);
const event = await createTestEvent(organizerId!, {
name: generateTestName('Registration Order Event'),
max_participants: 10, // Need max_participants for claim button to appear
// Register participant directly in DB
await getAdminDb().from('participants').insert({
event_id: event.id,
name: 'Participant',
user_id: participantId!,
});

// Register self
await registerForEvent(page, event.id);

// Claim two additional spots
await claimAdditionalSpot(page, event.id);

// Wait a moment between claims to ensure first claim is fully processed
await page.waitForTimeout(2000);

await claimAdditionalSpot(page, event.id);
// Claim a spot for a guest via UI
await claimAdditionalSpot(page, event.id, {
name: 'My Guest',
});

// Verify participants are created and ordered by created_at
// Verify the claimed spot exists in the database
const { data: participants } = await getAdminDb()
.from('participants')
.select('*')
.eq('event_id', event.id)
.order('created_at', { ascending: true });
.eq('event_id', event.id);

expect(participants?.length).toBe(3);
const claimedSpot = participants?.find(
(p) => p.claimed_by_user_id === participantId && p.user_id === null
);

// Participants should be ordered by creation time
if (participants && participants.length === 3) {
const times = participants.map((p) => new Date(p.created_at).getTime());
expect(times[0]).toBeLessThanOrEqual(times[1]);
expect(times[1]).toBeLessThanOrEqual(times[2]);
}
expect(claimedSpot).toBeDefined();
expect(claimedSpot?.name).toBe('My Guest');
});
});

Expand Down