Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
2 changes: 1 addition & 1 deletion app/controllers/tasks_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
5 changes: 5 additions & 0 deletions database/seeders/0_user_seeder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ export default class extends BaseSeeder {
email: 'user@local.host',
password: 'userpassword',
permissions: UserGuard.build(),
}, {
Comment thread
InfoX1337 marked this conversation as resolved.
nickname: 'normaluser',
email: 'normaluser@local.host',
password: 'normaluserpassword',
permissions: UserGuard.build(),
}])
}
}
9 changes: 9 additions & 0 deletions database/seeders/1_event_seeder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
9 changes: 9 additions & 0 deletions database/seeders/2_team_seeder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ 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)
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.')
Expand All @@ -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: normalUser.id,
permissions: TeamMemberGuard.build('MANAGE_MEMBERS'), // User is NOT a team admin
})
}
}
50 changes: 49 additions & 1 deletion database/seeders/3_task_seeder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
{
Expand Down Expand Up @@ -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)
Expand Down
214 changes: 214 additions & 0 deletions tests/functional/registrations.spec.ts

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add tests to ensure that:
a team member who lacks the REGISTER_TASK permission flag is blocked from registering the team.
a team member who was never part of the team cannot DELETE the team.

Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/*
* ______ __ __
* _ __/ ____/___ ____ / /____ _____/ /_
* | |/_/ / / __ \/ __ \/ __/ _ \/ ___/ __/
* _> </ /___/ /_/ / / / / /_/ __(__ ) /_
* /_/|_|\____/\____/_/ /_/\__/\___/____/\__/
* Copyright (C) 2026 xContest Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import { TeamFactory } from '#database/factories/team_factory'
import Task from '#models/task/task'
import Event from '#models/event/event'
import testUtils from '@adonisjs/core/services/test_utils'
import { test } from '@japa/runner'
import User from '#models/user'
import Team from '#models/team/team'
import TaskRegistration from '#models/task/task_registration'

test.group('Registrations', (group) => {
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 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(normalUser)

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()
})
})