From 0f11f4ccb3a679c261d0879047c334ad0b1a24bd Mon Sep 17 00:00:00 2001 From: InfoTCube Date: Thu, 26 Feb 2026 01:05:29 +0100 Subject: [PATCH 1/6] feat: tests for task registrations --- app/controllers/tasks_controller.ts | 2 +- database/seeders/0_user_seeder.ts | 5 + database/seeders/1_event_seeder.ts | 9 ++ database/seeders/2_team_seeder.ts | 9 ++ database/seeders/3_task_seeder.ts | 50 +++++- tests/functional/registrations.spec.ts | 214 +++++++++++++++++++++++++ 6 files changed, 287 insertions(+), 2 deletions(-) create mode 100644 tests/functional/registrations.spec.ts 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..7faa54f 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('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('MANAGE_MEMBERS'), // 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/registrations.spec.ts b/tests/functional/registrations.spec.ts new file mode 100644 index 0000000..4535461 --- /dev/null +++ b/tests/functional/registrations.spec.ts @@ -0,0 +1,214 @@ +/* + * ______ __ __ + * _ __/ ____/___ ____ / /____ _____/ /_ + * | |/_/ / / __ \/ __ \/ __/ _ \/ ___/ __/ + * _> { + 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 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 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 From 2ccc56a807b9f2a85789d9eb3b3dcd970f2440e9 Mon Sep 17 00:00:00 2001 From: InfoTCube Date: Thu, 26 Feb 2026 01:10:39 +0100 Subject: [PATCH 2/6] fix: changed coliding test --- database/seeders/0_user_seeder.ts | 6 +++--- database/seeders/2_team_seeder.ts | 8 ++++---- tests/functional/registrations.spec.ts | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/database/seeders/0_user_seeder.ts b/database/seeders/0_user_seeder.ts index 7ccec89..211b442 100644 --- a/database/seeders/0_user_seeder.ts +++ b/database/seeders/0_user_seeder.ts @@ -38,9 +38,9 @@ export default class extends BaseSeeder { password: 'userpassword', permissions: UserGuard.build(), }, { - nickname: 'user2', - email: 'user2@local.host', - password: 'user2password', + nickname: 'normaluser', + email: 'normaluser@local.host', + password: 'normaluserpassword', permissions: UserGuard.build(), }]) } diff --git a/database/seeders/2_team_seeder.ts b/database/seeders/2_team_seeder.ts index 7faa54f..87232bf 100644 --- a/database/seeders/2_team_seeder.ts +++ b/database/seeders/2_team_seeder.ts @@ -37,9 +37,9 @@ 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('User not found. Please run UserSeeder first.') + const normalUser = await User.findBy('nickname', 'normaluser') + if (!normalUser) + throw new Error('Normal user not found. Please run UserSeeder first.') const hackathonEvent = await Event.findByUuidOrSlug('hackathon-tasks') if (!hackathonEvent) @@ -56,7 +56,7 @@ export default class extends BaseSeeder { }) await hackathonTeam.related('members').create({ - userId: user2.id, + userId: normalUser.id, permissions: TeamMemberGuard.build('MANAGE_MEMBERS'), // User is NOT a team admin }) } diff --git a/tests/functional/registrations.spec.ts b/tests/functional/registrations.spec.ts index 4535461..ccdd4d5 100644 --- a/tests/functional/registrations.spec.ts +++ b/tests/functional/registrations.spec.ts @@ -194,11 +194,11 @@ test.group('Registrations', (group) => { }) test('Fails to unregister when does not have permission', async ({ client }) => { - const user2 = await User.findByOrFail('nickname', 'user2') + const normalUser = await User.findByOrFail('nickname', 'normaluser') 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) + const response = await client.delete(`/registrations/${registration.id}`).loginAs(normalUser) response.assertForbidden() }) From 9c45b8299bc68be44222d3cd2600f9b94ea46a62 Mon Sep 17 00:00:00 2001 From: InfoTCube Date: Thu, 26 Feb 2026 09:40:16 +0100 Subject: [PATCH 3/6] feat: add missing tests to registrations --- database/seeders/0_user_seeder.ts | 6 ++-- database/seeders/2_team_seeder.ts | 6 ++-- tests/functional/auth/register.spec.ts | 12 ++++---- tests/functional/registrations.spec.ts | 40 ++++++++++++++++++++++++-- 4 files changed, 50 insertions(+), 14 deletions(-) diff --git a/database/seeders/0_user_seeder.ts b/database/seeders/0_user_seeder.ts index 211b442..7ccec89 100644 --- a/database/seeders/0_user_seeder.ts +++ b/database/seeders/0_user_seeder.ts @@ -38,9 +38,9 @@ export default class extends BaseSeeder { password: 'userpassword', permissions: UserGuard.build(), }, { - nickname: 'normaluser', - email: 'normaluser@local.host', - password: 'normaluserpassword', + nickname: 'user2', + email: 'user2@local.host', + password: 'user2password', permissions: UserGuard.build(), }]) } diff --git a/database/seeders/2_team_seeder.ts b/database/seeders/2_team_seeder.ts index 87232bf..502f62d 100644 --- a/database/seeders/2_team_seeder.ts +++ b/database/seeders/2_team_seeder.ts @@ -37,8 +37,8 @@ export default class extends BaseSeeder { if (!user) throw new Error('User not found. Please run UserSeeder first.') - const normalUser = await User.findBy('nickname', 'normaluser') - if (!normalUser) + 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') @@ -56,7 +56,7 @@ export default class extends BaseSeeder { }) await hackathonTeam.related('members').create({ - userId: normalUser.id, + userId: user2.id, permissions: TeamMemberGuard.build('MANAGE_MEMBERS'), // User is NOT a team admin }) } diff --git a/tests/functional/auth/register.spec.ts b/tests/functional/auth/register.spec.ts index 95d91cb..ad34413 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: 'n32user', + email: 'newuser@local.host', password: 'password123', password_confirmation: 'password456', }) diff --git a/tests/functional/registrations.spec.ts b/tests/functional/registrations.spec.ts index ccdd4d5..1461573 100644 --- a/tests/functional/registrations.spec.ts +++ b/tests/functional/registrations.spec.ts @@ -29,6 +29,7 @@ import { test } from '@japa/runner' import User from '#models/user' import Team from '#models/team/team' import TaskRegistration from '#models/task/task_registration' +import { UserFactory } from '#database/factories/user_factory' test.group('Registrations', (group) => { group.each.setup(() => testUtils.db().seed()) @@ -182,6 +183,18 @@ test.group('Registrations', (group) => { 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() @@ -194,15 +207,38 @@ test.group('Registrations', (group) => { }) test('Fails to unregister when does not have permission', async ({ client }) => { - const normalUser = await User.findByOrFail('nickname', 'normaluser') + 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(normalUser) + 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() From 21c4486ddd2bdd49e7cc899cd743b063fdee7276 Mon Sep 17 00:00:00 2001 From: InfoTCube Date: Thu, 26 Feb 2026 09:40:53 +0100 Subject: [PATCH 4/6] fix: remove unused import --- tests/functional/registrations.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/functional/registrations.spec.ts b/tests/functional/registrations.spec.ts index 1461573..91967a2 100644 --- a/tests/functional/registrations.spec.ts +++ b/tests/functional/registrations.spec.ts @@ -29,7 +29,6 @@ import { test } from '@japa/runner' import User from '#models/user' import Team from '#models/team/team' import TaskRegistration from '#models/task/task_registration' -import { UserFactory } from '#database/factories/user_factory' test.group('Registrations', (group) => { group.each.setup(() => testUtils.db().seed()) From 70decd50c2d318a843a76ed8852691c8b79a8cb9 Mon Sep 17 00:00:00 2001 From: InfoTCube Date: Thu, 26 Feb 2026 09:47:51 +0100 Subject: [PATCH 5/6] feat: build member guard with no permissions in team seeder --- database/seeders/2_team_seeder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/seeders/2_team_seeder.ts b/database/seeders/2_team_seeder.ts index 502f62d..365cbc2 100644 --- a/database/seeders/2_team_seeder.ts +++ b/database/seeders/2_team_seeder.ts @@ -57,7 +57,7 @@ export default class extends BaseSeeder { await hackathonTeam.related('members').create({ userId: user2.id, - permissions: TeamMemberGuard.build('MANAGE_MEMBERS'), // User is NOT a team admin + permissions: TeamMemberGuard.build(), // User is NOT a team admin }) } } From 68205ffd9c7da3be31ac915107e2c374d11d9a2c Mon Sep 17 00:00:00 2001 From: InfoTCube Date: Thu, 26 Feb 2026 09:58:30 +0100 Subject: [PATCH 6/6] fix: typo in auth tests --- tests/functional/auth/register.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/auth/register.spec.ts b/tests/functional/auth/register.spec.ts index ad34413..30eb915 100644 --- a/tests/functional/auth/register.spec.ts +++ b/tests/functional/auth/register.spec.ts @@ -74,7 +74,7 @@ test.group('Auth register', (group) => { test('fails when passwords do not match', async ({ client }) => { const response = await client.post('/auth/register').json({ - nickname: 'n32user', + nickname: 'newuser', email: 'newuser@local.host', password: 'password123', password_confirmation: 'password456',