diff --git a/app/controllers/tasks_controller.ts b/app/controllers/tasks_controller.ts index 81f8a3d..f48167a 100644 --- a/app/controllers/tasks_controller.ts +++ b/app/controllers/tasks_controller.ts @@ -159,7 +159,7 @@ export default class TasksController { // 2. Check if team belongs to the same event as the task if (team.eventId !== task.eventId) - return response.badRequest({ message: 'Team does not belong to the same event as the task' }) + return response.forbidden({ message: 'Team does not belong to the same event as the task' }) // 4. Check if registration is open for the task const now = DateTime.now() diff --git a/database/seeders/0_user_seeder.ts b/database/seeders/0_user_seeder.ts index 7a4d3a6..7ccec89 100644 --- a/database/seeders/0_user_seeder.ts +++ b/database/seeders/0_user_seeder.ts @@ -37,6 +37,11 @@ export default class extends BaseSeeder { email: 'user@local.host', password: 'userpassword', permissions: UserGuard.build(), + }, { + nickname: 'user2', + email: 'user2@local.host', + password: 'user2password', + permissions: UserGuard.build(), }]) } } diff --git a/database/seeders/1_event_seeder.ts b/database/seeders/1_event_seeder.ts index 03422b6..ac710c6 100644 --- a/database/seeders/1_event_seeder.ts +++ b/database/seeders/1_event_seeder.ts @@ -60,6 +60,15 @@ export default class extends BaseSeeder { minTeamSize: 1, maxTeamSize: 5, }, + { + slug: 'team-size-event', + title: 'Event with team size limits.', + description: '#This is a test event created by EventSeeder. \n This event has team size limits of 2 to 3 members.', + accessCode: null, + status: 'ACTIVE', + minTeamSize: 2, + maxTeamSize: 3, + }, ]) for (const event of createdEvents) diff --git a/database/seeders/2_team_seeder.ts b/database/seeders/2_team_seeder.ts index 748b92e..365cbc2 100644 --- a/database/seeders/2_team_seeder.ts +++ b/database/seeders/2_team_seeder.ts @@ -37,6 +37,10 @@ export default class extends BaseSeeder { if (!user) throw new Error('User not found. Please run UserSeeder first.') + const user2 = await User.findBy('nickname', 'user2') + if (!user2) + throw new Error('Normal user not found. Please run UserSeeder first.') + const hackathonEvent = await Event.findByUuidOrSlug('hackathon-tasks') if (!hackathonEvent) throw new Error('Hackathon event not found. Please run EventSeeder first.') @@ -50,5 +54,10 @@ export default class extends BaseSeeder { userId: user.id, permissions: TeamMemberGuard.allPermissions(), // User is a team admin }) + + await hackathonTeam.related('members').create({ + userId: user2.id, + permissions: TeamMemberGuard.build(), // User is NOT a team admin + }) } } diff --git a/database/seeders/3_task_seeder.ts b/database/seeders/3_task_seeder.ts index fbf3623..0bf7c6e 100644 --- a/database/seeders/3_task_seeder.ts +++ b/database/seeders/3_task_seeder.ts @@ -32,7 +32,10 @@ export default class extends BaseSeeder { const hackathonEvent = await Event.findBy('slug', 'hackathon-tasks') if (!hackathonEvent) throw new Error('Hackathon event not found. Please run EventSeeder first.') - + + const teamSizeEvent = await Event.findBy('slug', 'team-size-event') + if (!teamSizeEvent) + throw new Error('Team size event not found. Please run EventSeeder first.') const tasks = await Task.createMany([ { @@ -68,6 +71,51 @@ export default class extends BaseSeeder { registrationStartAt: DateTime.now(), registrationEndAt: DateTime.now().plus({ days: 7 }), // 1 week from now }, + { + eventId: hackathonEvent.id, + slug: 'registration-not-started', + title: 'Hackathon Task with Registration Not Started', + description: 'This task has not yet opened for registration.', + taskType: 'HACKATHON', + status: 'ACTIVE', + detailsRevealAt: DateTime.now().plus({ days: 2 }), // Details will be revealed in 2 days + registrationStartAt: DateTime.now().plus({ days: 1 }), // Registration will start in 1 day + registrationEndAt: DateTime.now().plus({ days: 8 }), // Registration will end in 8 days + }, + { + eventId: hackathonEvent.id, + slug: 'registration-closed', + title: 'Hackathon Task with Registration Closed', + description: 'This task has closed registration.', + taskType: 'HACKATHON', + status: 'ACTIVE', + detailsRevealAt: DateTime.now().minus({ days: 2 }), // Details were revealed 2 days ago + registrationStartAt: DateTime.now().minus({ days: 8 }), // Registration started 8 days ago + registrationEndAt: DateTime.now().minus({ days: 1 }), // Registration ended 1 day ago + }, + { + eventId: hackathonEvent.id, + slug: 'autoregister-task', + title: 'Hackathon Task with Autoregistration', + description: 'This task automatically registers all teams upon registration opening.', + taskType: 'HACKATHON', + status: 'ACTIVE', + detailsRevealAt: DateTime.now().plus({ days: 1 }), // Details will be revealed in 1 day + registrationStartAt: DateTime.now().plus({ days: 1 }), // Registration will start in 1 day + registrationEndAt: DateTime.now().plus({ days: 7 }), // Registration will end in 7 days + autoregister: true, + }, + { + eventId: teamSizeEvent.id, + slug: 'team-size-task', + title: 'Task with Team Size Limits', + description: 'This task has team size limits defined by the event.', + taskType: 'HACKATHON', + status: 'ACTIVE', + detailsRevealAt: DateTime.now().plus({ days: 1 }), // Details will be revealed in 1 day + registrationStartAt: DateTime.now(), + registrationEndAt: DateTime.now().plus({ days: 7 }), // 1 week from now + }, ]) for (const task of tasks) diff --git a/tests/functional/auth/register.spec.ts b/tests/functional/auth/register.spec.ts index 95d91cb..30eb915 100644 --- a/tests/functional/auth/register.spec.ts +++ b/tests/functional/auth/register.spec.ts @@ -30,8 +30,8 @@ test.group('Auth register', (group) => { test('registers a new user successfully', async ({ client, assert }) => { const response = await client.post('/auth/register').json({ - nickname: 'user2', - email: 'user2@local.host', + nickname: 'newuser', + email: 'newuser@local.host', password: 'password123', password_confirmation: 'password123', }) @@ -39,13 +39,13 @@ test.group('Auth register', (group) => { response.assertStatus(201) assert.equal(response.body().message, 'Registration successful') assert.exists(response.body().user.id) - assert.equal(response.body().user.email, 'user2@local.host') + assert.equal(response.body().user.email, 'newuser@local.host') }) test('fails when password is not strong enough', async ({ client }) => { const response = await client.post('/auth/register').json({ nickname: 'mysecondaccount', - email: 'user2@local.host', + email: 'newuser@local.host', password: 'notsafe', password_confirmation: 'notsafe', }) @@ -74,8 +74,8 @@ test.group('Auth register', (group) => { test('fails when passwords do not match', async ({ client }) => { const response = await client.post('/auth/register').json({ - nickname: 'user2', - email: 'user2@local.host', + nickname: 'newuser', + email: 'newuser@local.host', password: 'password123', password_confirmation: 'password456', }) diff --git a/tests/functional/registrations.spec.ts b/tests/functional/registrations.spec.ts new file mode 100644 index 0000000..91967a2 --- /dev/null +++ b/tests/functional/registrations.spec.ts @@ -0,0 +1,249 @@ +/* + * ______ __ __ + * _ __/ ____/___ ____ / /____ _____/ /_ + * | |/_/ / / __ \/ __ \/ __/ _ \/ ___/ __/ + * _> { + group.each.setup(() => testUtils.db().seed()) + group.each.teardown(() => testUtils.db().truncate()) + + test('Registers a team to a task successfully', async ({ client }) => { + const event = await Event.findByUuidOrSlug('hackathon-tasks') + const user = await User.findByOrFail('nickname', 'user') + const team = await Team.findByOrFail('name', 'User\'s team') + + const task = await Task.query().where('event_id', event.id).firstOrFail() + const response = await client.post(`/tasks/${task.slug}/registrations`).json({ + teamId: team.id, + }).loginAs(user) + + response.assertCreated() + response.assertBodyContains({ + teamId: team.id, + taskId: task.id, + }) + }) + + test('Unregisters a team from a task successfully', async ({ client }) => { + const user = await User.findByOrFail('nickname', 'user') + const team = await Team.findByOrFail('name', 'User\'s team') + const registration = await TaskRegistration.query().where('team_id', team.id).firstOrFail() + + const response = await client.delete(`/registrations/${registration.id}`).loginAs(user) + + response.assertNoContent() + }) + + test('Fails when user is not a member of the team', async ({ client }) => { + const user = await User.findByOrFail('nickname', 'user') + const event = await Event.findByUuidOrSlug('hackathon-tasks') + + const team = await TeamFactory.with('members', 1, (member) => member.with('user')) + .merge({ eventId: event.id }) + .create() + + const task = await Task.query().where('event_id', event.id).firstOrFail() + const response = await client.post(`/tasks/${task.slug}/registrations`).json({ + teamId: team.id, + }).loginAs(user) + + response.assertForbidden() + }) + + test('Fails when team is from a different event', async ({ client }) => { + const event = await Event.findByUuidOrSlug('no-tasks') + const team = await TeamFactory.with('members', 1, (member) => member.with('user')) + .merge({ eventId: event.id }) + .create() + + await team.load('members') + const member = team.members[0] + await member.load('user') + + const taskEvent = await Event.findByUuidOrSlug('hackathon-tasks') + const task = await Task.query().where('event_id', taskEvent.id).firstOrFail() + const response = await client.post(`/tasks/${task.slug}/registrations`).json({ + teamId: team.id, + }).loginAs(member.user) + + response.assertForbidden() + }) + + test('Fails when registration has not started yet', async ({ client }) => { + const user = await User.findByOrFail('nickname', 'user') + const team = await Team.findByOrFail('name', 'User\'s team') + + const task = await Task.findByOrFail('slug', 'registration-not-started') + const response = await client.post(`/tasks/${task.slug}/registrations`).json({ + teamId: team.id, + }).loginAs(user) + + response.assertBadRequest() + }) + + test('Fails when registration has already ended', async ({ client }) => { + const user = await User.findByOrFail('nickname', 'user') + const team = await Team.findByOrFail('name', 'User\'s team') + + const task = await Task.findByOrFail('slug', 'registration-closed') + const response = await client.post(`/tasks/${task.slug}/registrations`).json({ + teamId: team.id, + }).loginAs(user) + + response.assertBadRequest() + }) + + test('Fails when team size is below the minimum', async ({ client }) => { + const event = await Event.findByUuidOrSlug('team-size-event') + + const team = await TeamFactory.with('members', 1, (member) => member.with('user')) + .merge({ eventId: event.id }) + .create() + + await team.load('members') + const member = team.members[0] + await member.load('user') + + const task = await Task.query().where('event_id', event.id).firstOrFail() + const response = await client.post(`/tasks/${task.slug}/registrations`).json({ + teamId: team.id, + }).loginAs(member.user) + + response.assertBadRequest() + }) + + test('Fails when team size is above the maximum', async ({ client }) => { + const event = await Event.findByUuidOrSlug('team-size-event') + + const team = await TeamFactory.with('members', 5, (member) => member.with('user')) + .merge({ eventId: event.id }) + .create() + + await team.load('members', (query) => query.orderBy('created_at', 'asc')) + const member = team.members[0] + await member.load('user') + + const task = await Task.query().where('event_id', event.id).firstOrFail() + const response = await client.post(`/tasks/${task.slug}/registrations`).json({ + teamId: team.id, + }).loginAs(member.user) + + response.assertBadRequest() + }) + + test('Fails when task has autoregister enabled', async ({ client }) => { + const user = await User.findByOrFail('nickname', 'user') + const team = await Team.findByOrFail('name', 'User\'s team') + + const task = await Task.findByOrFail('slug', 'autoregister-task') + const response = await client.post(`/tasks/${task.slug}/registrations`).json({ + teamId: team.id, + }).loginAs(user) + + response.assertBadRequest() + }) + + test('Fails when team is already registered', async ({ client }) => { + const user = await User.findByOrFail('nickname', 'user') + const team = await Team.findByOrFail('name', 'User\'s team') + + const task = await Task.findByUuidOrSlug('visible-task') + const response = await client.post(`/tasks/${task.slug}/registrations`).json({ + teamId: team.id, + }).loginAs(user) + + response.assertConflict() + }) + + test('Fails to register when does not have permission', async ({ client }) => { + const user2 = await User.findByOrFail('nickname', 'user2') + const team = await Team.findByOrFail('name', 'User\'s team') + const task = await Task.query().where('event_id', team.eventId).firstOrFail() + + const response = await client.post(`/tasks/${task.slug}/registrations`).json({ + teamId: team.id, + }).loginAs(user2) + + response.assertForbidden() + }) + + test('Fails when is not authenticated', async ({ client }) => { + const team = await Team.findByOrFail('name', 'User\'s team') + const task = await Task.query().where('event_id', team.eventId).firstOrFail() + + const response = await client.post(`/tasks/${task.slug}/registrations`).json({ + teamId: team.id, + }) + + response.assertUnauthorized() + }) + + test('Fails to unregister when does not have permission', async ({ client }) => { + const user2 = await User.findByOrFail('nickname', 'user2') + const team = await Team.findByOrFail('name', 'User\'s team') + const registration = await TaskRegistration.query().where('team_id', team.id).firstOrFail() + + const response = await client.delete(`/registrations/${registration.id}`).loginAs(user2) + + response.assertForbidden() + }) + + test('Fails to unregister when is not a member of the team', async ({ client }) => { + const team = await Team.findByOrFail('name', 'User\'s team') + const registration = await TaskRegistration.query().where('team_id', team.id).firstOrFail() + + const team2 = await TeamFactory.with('members', 1, (member) => member.with('user')) + .merge({ eventId: team.eventId }) + .create() + + await team2.load('members') + const member = team2.members[0] + await member.load('user') + + const response = await client.delete(`/registrations/${registration.id}`).loginAs(member.user) + response.assertForbidden() + }) + + test('Fails to unregister if the registration does not exist', async ({ client }) => { + const user = await User.findByOrFail('nickname', 'user') + + const response = await client.delete('/registrations/00000000-0000-0000-0000-000000000000').loginAs(user) + response.assertNotFound() + }) + + test('Fails to unregister when is not authenticated', async ({ client }) => { + const team = await Team.findByOrFail('name', 'User\'s team') + const registration = await TaskRegistration.query().where('team_id', team.id).firstOrFail() + + const response = await client.delete(`/registrations/${registration.id}`) + + response.assertUnauthorized() + }) +}) \ No newline at end of file