From fe179ac002a039f077df946c9f37606ea58c08ea Mon Sep 17 00:00:00 2001 From: InfoX Date: Wed, 25 Feb 2026 11:41:03 +0100 Subject: [PATCH 01/12] feat: Mute database migration completed logs. --- tests/bootstrap.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/bootstrap.ts b/tests/bootstrap.ts index 46b58f7..a4b66bc 100644 --- a/tests/bootstrap.ts +++ b/tests/bootstrap.ts @@ -29,6 +29,7 @@ import { pluginAdonisJS } from '@japa/plugin-adonisjs' import testUtils from '@adonisjs/core/services/test_utils' import { authApiClient } from '@adonisjs/auth/plugins/api_client' import { sessionApiClient } from '@adonisjs/session/plugins/api_client' +import logger from '@adonisjs/core/services/logger' /** * This file is imported by the "bin/test.ts" entrypoint file @@ -54,7 +55,18 @@ export const plugins: Config['plugins'] = [ * The teardown functions are executed after all the tests */ export const runnerHooks: Required> = { - setup: [() => testUtils.db().migrate()], + setup: [() => testUtils.db().migrate(), () => { + // Suppress database migration logs to keep test output clean + logger.level = 'error' + // eslint-disable-next-line no-console + const originalLog = console.log + // eslint-disable-next-line no-console + console.log = (...args: unknown[]) => { + const message = args.join(' ') + if (!message.includes('completed') || (!message.includes('database') && !message.includes('seeders'))) + originalLog(...args) + } + }], teardown: [], } From 53553994ca12213a77e4bf4d9f91c28f703f5338 Mon Sep 17 00:00:00 2001 From: InfoX Date: Wed, 25 Feb 2026 11:41:30 +0100 Subject: [PATCH 02/12] fix: Remove whitespace after pnpm run lint. --- .husky/pre-push | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.husky/pre-push b/.husky/pre-push index 6ca46a7..d11c1e8 100644 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1 +1 @@ -pnpm run lint +pnpm run lint \ No newline at end of file From e1adde96f200c82f5fa57bfc88bb6fe8e5451d53 Mon Sep 17 00:00:00 2001 From: InfoX Date: Wed, 25 Feb 2026 11:46:37 +0100 Subject: [PATCH 03/12] feat: Add basic tasks tests. --- tests/functional/tasks.spec.ts | 85 ++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 tests/functional/tasks.spec.ts diff --git a/tests/functional/tasks.spec.ts b/tests/functional/tasks.spec.ts new file mode 100644 index 0000000..3c29e1e --- /dev/null +++ b/tests/functional/tasks.spec.ts @@ -0,0 +1,85 @@ +/* + * ______ __ __ + * _ __/ ____/___ ____ / /____ _____/ /_ + * | |/_/ / / __ \/ __ \/ __/ _ \/ ___/ __/ + * _> { + group.each.setup(() => testUtils.db().seed()) + group.each.teardown(() => testUtils.db().truncate()) + + test('Lists all tasks', async ({ client }) => { + const response = await client.get('/event/hackathon-tasks/tasks') + + response.assertOk() + response.assertBodyContains([ + { task: { slug: 'visible-task' } }, + { task: { slug: 'visible-task-2' } }, + ]) + }) + + test('Creates a task', async ({ client }) => { + const admin = await User.findByOrFail('nickname', 'admin') + + const response = await client.post('/event/hackathon-tasks/task').json({ + slug: 'new-task', + title: 'New Hackathon Task', + description: 'A new task for testing.', + taskType: 'HACKATHON', + requirementsDocumentUrl: 'http://local.host/requirements/new-task', + }).loginAs(admin) + + response.assertCreated() + response.assertBodyContains({ + slug: 'new-task', + title: 'New Hackathon Task', + }) + }) + + test('Shows task details', async ({ client }) => { + const response = await client.get('/tasks/visible-task') + + response.assertOk() + response.assertBodyContains({ task: { slug: 'visible-task' } }) + }) + + test('Updates a task', async ({ client }) => { + const admin = await User.findByOrFail('nickname', 'admin') + + const response = await client.put('/tasks/visible-task').json({ + title: 'Updated Task Title', + }).loginAs(admin) + + response.assertOk() + response.assertBodyContains({ title: 'Updated Task Title' }) + }) + + test('Deletes a task', async ({ client }) => { + const admin = await User.findByOrFail('nickname', 'admin') + + const response = await client.delete('/tasks/visible-task').loginAs(admin) + + response.assertNoContent() + }) +}) \ No newline at end of file From b72cbb0f37e2c4cbc93beee207f37e001b809827 Mon Sep 17 00:00:00 2001 From: InfoX Date: Wed, 25 Feb 2026 11:53:25 +0100 Subject: [PATCH 04/12] feat: Add draft checks for tasks --- tests/functional/tasks.spec.ts | 42 ++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/functional/tasks.spec.ts b/tests/functional/tasks.spec.ts index 3c29e1e..2e4d95f 100644 --- a/tests/functional/tasks.spec.ts +++ b/tests/functional/tasks.spec.ts @@ -82,4 +82,46 @@ test.group('Tasks', (group) => { response.assertNoContent() }) + + test('Admin can see draft tasks in listing', async ({ client }) => { + const admin = await User.findByOrFail('nickname', 'admin') + + const response = await client.get('/event/hackathon-tasks/tasks').loginAs(admin) + + response.assertOk() + response.assertBodyContains([ + { task: { slug: 'hidden-task' } }, + ]) + }) + + test('Regular user cannot see draft tasks in listing', async ({ client, assert }) => { + const user = await User.findByOrFail('nickname', 'user') + + const response = await client.get('/event/hackathon-tasks/tasks').loginAs(user) + + response.assertOk() + const slugs = response.body().map((t: any) => t.task?.slug) + assert.notInclude(slugs, 'hidden-task') + }) + + test('Listing tasks for a draft event is forbidden to guests', async ({ client }) => { + const response = await client.get('/event/not-visible/tasks') + + response.assertForbidden() + }) + + test('Guest cannot view draft task details', async ({ client }) => { + const response = await client.get('/tasks/hidden-task') + + response.assertForbidden() + }) + + test('Admin can view draft task details', async ({ client }) => { + const admin = await User.findByOrFail('nickname', 'admin') + + const response = await client.get('/tasks/hidden-task').loginAs(admin) + + response.assertOk() + response.assertBodyContains({ task: { slug: 'hidden-task' } }) + }) }) \ No newline at end of file From 9351cad8c01104e4dcbfff69f7151557e67478a8 Mon Sep 17 00:00:00 2001 From: InfoX Date: Wed, 25 Feb 2026 11:55:27 +0100 Subject: [PATCH 05/12] fix: Include truncate logs in filter. --- tests/bootstrap.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/bootstrap.ts b/tests/bootstrap.ts index a4b66bc..95a4f78 100644 --- a/tests/bootstrap.ts +++ b/tests/bootstrap.ts @@ -56,14 +56,14 @@ export const plugins: Config['plugins'] = [ */ export const runnerHooks: Required> = { setup: [() => testUtils.db().migrate(), () => { - // Suppress database migration logs to keep test output clean + // Suppress database migration and truncate logs to keep test output clean logger.level = 'error' // eslint-disable-next-line no-console const originalLog = console.log // eslint-disable-next-line no-console console.log = (...args: unknown[]) => { const message = args.join(' ') - if (!message.includes('completed') || (!message.includes('database') && !message.includes('seeders'))) + if (!message.includes('completed') && !message.includes('successfully') || (!message.includes('database') && !message.includes('seeders') && !message.includes('Truncated'))) originalLog(...args) } }], From 21b2face5e6a97702aa7a23010841cb90d043ccd Mon Sep 17 00:00:00 2001 From: InfoX Date: Wed, 25 Feb 2026 11:59:58 +0100 Subject: [PATCH 06/12] feat: Add auth / permissions checks for tasks --- tests/functional/tasks.spec.ts | 67 ++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/tests/functional/tasks.spec.ts b/tests/functional/tasks.spec.ts index 2e4d95f..07ea7dc 100644 --- a/tests/functional/tasks.spec.ts +++ b/tests/functional/tasks.spec.ts @@ -124,4 +124,71 @@ test.group('Tasks', (group) => { response.assertOk() response.assertBodyContains({ task: { slug: 'hidden-task' } }) }) + + test('Fails to create task without authentication', async ({ client }) => { + const response = await client.post('/event/hackathon-tasks/task').json({ + slug: 'unauth-task', + title: 'Unauth Task', + description: 'Should fail.', + taskType: 'HACKATHON', + requirementsDocumentUrl: 'http://localhost/requirements/unauth-task', + }) + + response.assertUnauthorized() + }) + + test('Regular user without permission cannot create task', async ({ client }) => { + const user = await User.findByOrFail('nickname', 'user') + + const response = await client.post('/event/hackathon-tasks/task').json({ + slug: 'user-task', + title: 'User Task', + description: 'Should fail.', + taskType: 'HACKATHON', + requirementsDocumentUrl: 'http://localhost/requirements/user-task', + }).loginAs(user) + + response.assertForbidden() + }) + + test('Fails to create HACKATHON task without requirementsDocumentUrl', async ({ client }) => { + const admin = await User.findByOrFail('nickname', 'admin') + + const response = await client.post('/event/hackathon-tasks/task').json({ + slug: 'missing-requirements-task', + title: 'Missing Requirements Task', + description: 'Should fail.', + taskType: 'HACKATHON', + }).loginAs(admin) + + response.assertUnprocessableEntity() + }) + + test('Fails to create task with duplicate slug', async ({ client }) => { + const admin = await User.findByOrFail('nickname', 'admin') + + const response = await client.post('/event/hackathon-tasks/task').json({ + slug: 'visible-task', + title: 'Duplicate Slug Task', + description: 'Should fail due to duplicate slug.', + taskType: 'HACKATHON', + requirementsDocumentUrl: 'http://localhost/requirements/visible-task', + }).loginAs(admin) + + response.assertUnprocessableEntity() + }) + + test('Fails to update task without authentication', async ({ client }) => { + const response = await client.put('/tasks/visible-task').json({ + title: 'Updated Without Auth', + }) + + response.assertUnauthorized() + }) + + test('Fails to delete task without authentication', async ({ client }) => { + const response = await client.delete('/tasks/visible-task') + + response.assertUnauthorized() + }) }) \ No newline at end of file From 34d31ca19f4618184f9188cb9335e7827a6c441e Mon Sep 17 00:00:00 2001 From: InfoX Date: Wed, 25 Feb 2026 18:41:19 +0100 Subject: [PATCH 07/12] feat Add basic event tests. --- tests/functional/events.spec.ts | 88 +++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tests/functional/events.spec.ts diff --git a/tests/functional/events.spec.ts b/tests/functional/events.spec.ts new file mode 100644 index 0000000..3d54f2d --- /dev/null +++ b/tests/functional/events.spec.ts @@ -0,0 +1,88 @@ +/* + * ______ __ __ + * _ __/ ____/___ ____ / /____ _____/ /_ + * | |/_/ / / __ \/ __ \/ __/ _ \/ ___/ __/ + * _> { + group.each.setup(() => testUtils.db().seed()) + group.each.teardown(() => testUtils.db().truncate()) + + test('Lists all events', async ({ client }) => { + const response = await client.get('/events') + + response.assertOk() + response.assertBodyContains([ + { slug: 'no-tasks' }, + { slug: 'hackathon-tasks' }, + ]) + }) + + test('Creates an event', async ({ client }) => { + const admin = await User.findByOrFail('nickname', 'admin') + + const response = await client.post('/events').json({ + slug: 'new-event', + title: 'New Test Event', + description: 'A brand new event for testing.', + status: 'DRAFT', + minTeamSize: 1, + maxTeamSize: 5, + }).loginAs(admin) + + response.assertCreated() + response.assertBodyContains({ + slug: 'new-event', + title: 'New Test Event', + }) + }) + + test('Shows event details', async ({ client }) => { + const response = await client.get('/events/no-tasks') + + response.assertOk() + response.assertBodyContains({ slug: 'no-tasks' }) + }) + + test('Updates an event', async ({ client }) => { + const admin = await User.findByOrFail('nickname', 'admin') + + const response = await client.put('/events/no-tasks').json({ + title: 'Updated Event Title', + }).loginAs(admin) + + response.assertOk() + response.assertBodyContains({ title: 'Updated Event Title' }) + }) + + test('Deletes an event', async ({ client }) => { + const admin = await User.findByOrFail('nickname', 'admin') + + const response = await client.delete('/events/no-tasks').json({ + confirmation: 'no-tasks', + }).loginAs(admin) + + response.assertNoContent() + }) +}) \ No newline at end of file From 06907e52866997f53fb3e45ed01cc513ac25272e Mon Sep 17 00:00:00 2001 From: InfoX Date: Wed, 25 Feb 2026 18:50:04 +0100 Subject: [PATCH 08/12] feat: Add event admin tests. --- tests/functional/events.spec.ts | 55 +++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/functional/events.spec.ts b/tests/functional/events.spec.ts index 3d54f2d..fd5e134 100644 --- a/tests/functional/events.spec.ts +++ b/tests/functional/events.spec.ts @@ -85,4 +85,59 @@ test.group('Events', (group) => { response.assertNoContent() }) + + test('Lists event administrators', async ({ client }) => { + const admin = await User.findByOrFail('nickname', 'admin') + + const response = await client.get('/events/no-tasks/administrators').loginAs(admin) + + response.assertOk() + response.assertBodyContains([ + { userId: admin.id }, + ]) + }) + + test('Adds an event administrator', async ({ client }) => { + const admin = await User.findByOrFail('nickname', 'admin') + const user = await User.findByOrFail('nickname', 'user') + + const response = await client.post('/events/no-tasks/administrators').json({ + userId: user.id, + permissions: 0, + }).loginAs(admin) + + response.assertCreated() + response.assertBodyContains({ userId: user.id }) + }) + + test('Updates an event administrator', async ({ client }) => { + const admin = await User.findByOrFail('nickname', 'admin') + const user = await User.findByOrFail('nickname', 'user') + + await client.post('/events/no-tasks/administrators').json({ + userId: user.id, + permissions: 0, + }).loginAs(admin) + + const response = await client.put(`/events/no-tasks/administrators/${user.id}`).json({ + permissions: 1, + }).loginAs(admin) + + response.assertOk() + response.assertBodyContains({ permissions: 1 }) + }) + + test('Deletes an event administrator', async ({ client }) => { + const admin = await User.findByOrFail('nickname', 'admin') + const user = await User.findByOrFail('nickname', 'user') + + await client.post('/events/no-tasks/administrators').json({ + userId: user.id, + permissions: 0, + }).loginAs(admin) + + const response = await client.delete(`/events/no-tasks/administrators/${user.id}`).loginAs(admin) + + response.assertNoContent() + }) }) \ No newline at end of file From a6dd6fe901fd2b668d7396d3c7a770978dd39614 Mon Sep 17 00:00:00 2001 From: InfoX Date: Wed, 25 Feb 2026 18:56:53 +0100 Subject: [PATCH 09/12] feat: Add event draft tests. --- tests/functional/events.spec.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/functional/events.spec.ts b/tests/functional/events.spec.ts index fd5e134..bddcbc9 100644 --- a/tests/functional/events.spec.ts +++ b/tests/functional/events.spec.ts @@ -140,4 +140,27 @@ test.group('Events', (group) => { response.assertNoContent() }) + + test('Does not expose draft events in listing', async ({ client, assert }) => { + const response = await client.get('/events') + + response.assertOk() + const slugs = response.body().map((e: any) => e.slug) + assert.notInclude(slugs, 'not-visible') + }) + + test('Guest cannot view draft event details', async ({ client }) => { + const response = await client.get('/events/not-visible') + + response.assertForbidden() + }) + + test('Admin can view draft event details', async ({ client }) => { + const admin = await User.findByOrFail('nickname', 'admin') + + const response = await client.get('/events/not-visible').loginAs(admin) + + response.assertOk() + response.assertBodyContains({ slug: 'not-visible' }) + }) }) \ No newline at end of file From 4567de7f646c11b0ce7e5ec67667c4a7f846da10 Mon Sep 17 00:00:00 2001 From: InfoX Date: Wed, 25 Feb 2026 19:05:40 +0100 Subject: [PATCH 10/12] feat: Add auth and more event tests. --- tests/functional/events.spec.ts | 105 ++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/tests/functional/events.spec.ts b/tests/functional/events.spec.ts index bddcbc9..e19a100 100644 --- a/tests/functional/events.spec.ts +++ b/tests/functional/events.spec.ts @@ -163,4 +163,109 @@ test.group('Events', (group) => { response.assertOk() response.assertBodyContains({ slug: 'not-visible' }) }) + + test('Fails to create event without authentication', async ({ client }) => { + const response = await client.post('/events').json({ + slug: 'unauth-event', + title: 'Unauth Event', + description: 'Should fail.', + status: 'DRAFT', + minTeamSize: 1, + maxTeamSize: 3, + }) + + response.assertUnauthorized() + }) + + test('Regular user without permission cannot create event', async ({ client }) => { + const user = await User.findByOrFail('nickname', 'user') + + const response = await client.post('/events').json({ + slug: 'user-event', + title: 'User Event', + description: 'Should fail.', + status: 'DRAFT', + minTeamSize: 1, + maxTeamSize: 3, + }).loginAs(user) + + response.assertForbidden() + }) + + test('Fails to create event with duplicate slug', async ({ client }) => { + const admin = await User.findByOrFail('nickname', 'admin') + + const response = await client.post('/events').json({ + slug: 'no-tasks', + title: 'Duplicate Slug Event', + description: 'Should fail due to duplicate slug.', + status: 'DRAFT', + minTeamSize: 1, + maxTeamSize: 3, + }).loginAs(admin) + + response.assertUnprocessableEntity() + }) + + test('Fails to create event with missing required fields', async ({ client }) => { + const admin = await User.findByOrFail('nickname', 'admin') + + const response = await client.post('/events').json({ + slug: 'incomplete-event', + }).loginAs(admin) + + response.assertUnprocessableEntity() + }) + + test('Fails to update event without authentication', async ({ client }) => { + const response = await client.put('/events/no-tasks').json({ + title: 'Updated Without Auth', + }) + + response.assertUnauthorized() + }) + + test('Fails to delete event without authentication', async ({ client }) => { + const response = await client.delete('/events/no-tasks').json({ + confirmation: 'no-tasks', + }) + + response.assertUnauthorized() + }) + + test('Fails to delete event with wrong confirmation', async ({ client }) => { + const admin = await User.findByOrFail('nickname', 'admin') + + const response = await client.delete('/events/no-tasks').json({ + confirmation: 'wrong-slug', + }).loginAs(admin) + + response.assertUnprocessableEntity() + }) + + test('Fails to access event administrators without authentication', async ({ client }) => { + const response = await client.get('/events/no-tasks/administrators') + + response.assertUnauthorized() + }) + + test('Non-admin user cannot manage event administrators', async ({ client }) => { + const user = await User.findByOrFail('nickname', 'user') + + const response = await client.get('/events/no-tasks/administrators').loginAs(user) + + response.assertForbidden() + }) + + test('Fails to add duplicate event administrator', async ({ client }) => { + const admin = await User.findByOrFail('nickname', 'admin') + + // Admin is already an administrator of all seeded events + const response = await client.post('/events/no-tasks/administrators').json({ + userId: admin.id, + permissions: 0, + }).loginAs(admin) + + response.assertStatus(409) + }) }) \ No newline at end of file From 16891d6e618c975dcd086aface54c5033d8120b2 Mon Sep 17 00:00:00 2001 From: InfoX Date: Wed, 25 Feb 2026 19:35:51 +0100 Subject: [PATCH 11/12] feat: Events now cannot be created when minTeamSize > maxTeamSize. --- app/controllers/events_controller.ts | 4 ++++ tests/functional/events.spec.ts | 15 +++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/app/controllers/events_controller.ts b/app/controllers/events_controller.ts index 9fe9197..7f42bb4 100644 --- a/app/controllers/events_controller.ts +++ b/app/controllers/events_controller.ts @@ -47,6 +47,10 @@ export default class EventsController { const payload = await request.validateUsing(createEventValidator) + if (payload.minTeamSize > payload.maxTeamSize) + return response.unprocessableEntity({ message: 'minTeamSize cannot be greater than maxTeamSize.' }) + + const event = await db.transaction(async (trx) => { const newEvent = await Event.create(payload, { client: trx }) diff --git a/tests/functional/events.spec.ts b/tests/functional/events.spec.ts index e19a100..dfe01f1 100644 --- a/tests/functional/events.spec.ts +++ b/tests/functional/events.spec.ts @@ -268,4 +268,19 @@ test.group('Events', (group) => { response.assertStatus(409) }) + + test('Fails when minTeamSize is greater than maxTeamSize', async ({ client }) => { + const admin = await User.findByOrFail('nickname', 'admin') + + const response = await client.post('/events').json({ + slug: 'invalid-team-size', + title: 'Invalid Team Size Event', + description: 'Should fail due to invalid team size.', + status: 'DRAFT', + minTeamSize: 5, + maxTeamSize: 3, + }).loginAs(admin) + + response.assertUnprocessableEntity() + }) }) \ No newline at end of file From 4c193d9e4a55a8144f01bf7713bd8a213a757c9c Mon Sep 17 00:00:00 2001 From: InfoX Date: Wed, 25 Feb 2026 19:50:57 +0100 Subject: [PATCH 12/12] fix: Event cannot contain 0 administrators now. --- app/controllers/events_controller.ts | 5 +++++ tests/functional/events.spec.ts | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/app/controllers/events_controller.ts b/app/controllers/events_controller.ts index 7f42bb4..a28a9ed 100644 --- a/app/controllers/events_controller.ts +++ b/app/controllers/events_controller.ts @@ -184,6 +184,11 @@ export default class EventsController { if (!admin) return response.notFound({ message: 'User is not an administrator of this event.' }) + const admins = await EventAdministrator.query().where('event_id', event.id) + + if (admins.length <= 1) + return response.unprocessableEntity({ message: 'Cannot remove the last administrator from the event.' }) + await admin.delete() return response.noContent() } diff --git a/tests/functional/events.spec.ts b/tests/functional/events.spec.ts index dfe01f1..dd71933 100644 --- a/tests/functional/events.spec.ts +++ b/tests/functional/events.spec.ts @@ -283,4 +283,12 @@ test.group('Events', (group) => { response.assertUnprocessableEntity() }) + + test('Event cannot contain no administrators', async ({ client }) => { + const admin = await User.findByOrFail('nickname', 'admin') + + const response = await client.delete(`/events/no-tasks/administrators/${admin.id}`).loginAs(admin) + + response.assertUnprocessableEntity() + }) }) \ No newline at end of file