Skip to content
Snippets Groups Projects
Commit 60324f6a authored by Thomas SAUVAGE's avatar Thomas SAUVAGE
Browse files

Merge branch 'thomas' into 'main'

Validation & tests

See merge request svg/bp-backend!3
parents 6b9c42ef f0281c59
No related branches found
No related tags found
1 merge request!3Validation & tests
Pipeline #13769 passed
Showing
with 199 additions and 74 deletions
......@@ -8,13 +8,13 @@ image: node:20
# Build stages
stages:
- test
- Code quality
# ==== Code quality ====
# Formatting test
# Formatting
format:
stage: test
stage: Code quality
before_script:
- npm install -g prettier
script:
......@@ -22,7 +22,7 @@ format:
# Lint checks
lint:
stage: test
stage: Code quality
before_script:
- npm ci
script:
......@@ -30,7 +30,7 @@ lint:
# Type check
typecheck:
stage: test
stage: Code quality
before_script:
- npm ci
script:
......
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { createEvent, getAllRoots, getAllSons } from '../events'
import { createEvent, getAllRoots, getChildren } from '../events'
export default class EventController {
public async getAllRoots(ctx: HttpContextContract) {
......@@ -10,7 +10,7 @@ export default class EventController {
return createEvent(ctx)
}
public async getAllSons(ctx: HttpContextContract) {
return getAllSons(ctx)
public async getChildren(ctx: HttpContextContract) {
return getChildren(ctx)
}
}
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Event from 'App/Models/Event'
import { createEventSchema } from 'App/Schemas/events.schema'
/** Get all events */
/** Get all root events */
export const getAllRoots = async ({ response }: HttpContextContract) => {
const events = await Event.findBy('isRoot', true)
const events = await Event.query().whereNull('parentId')
return response.ok(events)
}
/** Recursively gives a list of sons events */
const getSons = async (id: number) => {
const event = await Event.findOrFail(id) // Use this to throw an error automatically when the item is not found
/** Return the level 1 children of an event */
export const getChildren = async ({ params, response }: HttpContextContract) => {
const fatherEvent = await Event.findOrFail(params.eventId)
const children = await fatherEvent.related('children').query()
return event.related('children').query() // Query level 1 children like this (this is not recursive)
}
/** Get all sons of a given event, using params */
export const getAllSons = async ({ params, response }: HttpContextContract) => {
// TODO: Validate id (and remove the following Event query)
await Event.findOrFail(params.id)
try {
const tree = await getSons(params.id)
return response.ok(tree)
} catch {
return response.internalServerError({
error: 'One of the son events was not found, the problem might come from the database',
})
}
return response.ok(children)
}
/** Create an event */
export const createEvent = async ({ request, response }: HttpContextContract) => {
// TODO: Validate
const { fatherId, ...data } = request.only(['name', 'thumbnailId', 'fatherId'])
const newEvent = await request.validate({ schema: createEventSchema })
// If the father is not specified, the event is a root
if (!fatherId) {
await Event.create({ ...data, isRoot: true })
if (!newEvent.parentId) {
await Event.create({ ...newEvent, parentId: null })
return response.created({ message: 'Root event created' })
}
// Searching for the father of the event
const father = await Event.find(fatherId) // XXX : can use FindOrFail, but the handler is too generic...
// If the father is specified, we add the event to the father's sons
if (!father) {
return response.badRequest({ error: 'Father not found' })
}
await Event.create({ ...data, isRoot: false, parentId: father.id })
// Verify that the father exists, checked by the schema ?
await Event.findOrFail(newEvent.parentId)
return response.created({ message: 'Son event created' })
await Event.create(newEvent)
return response.created({ message: 'Child event created' })
}
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Picture from 'App/Models/Picture'
import { createPictureSchema, getPicturesSchema } from 'App/Schemas/pictures.schema'
/** Get all pictures of an event, with param id */
export const getPictures = async ({ response, params }: HttpContextContract) => {
const pictures = await Picture.query().where('eventId', params.id)
export const getPictures = async ({ request, response }: HttpContextContract) => {
const {
params: { eventId },
} = await request.validate({ schema: getPicturesSchema })
const pictures = await Picture.query().where('eventId', eventId)
return response.ok(pictures)
}
/**
* Create a picture
* TODO: Really create a picture, not just metadata
*/
export const createPicture = async ({ request, response, params }: HttpContextContract) => {
const newPicture = await request.validate({
schema: createPictureSchema,
data: { ...request.body(), eventId: params.eventId },
})
await Picture.create(newPicture)
return response.created({ message: 'Picture created' })
}
......@@ -20,9 +20,6 @@ export default class Event extends BaseModel {
})
public children: HasMany<typeof Event>
@column()
public isRoot: boolean // Represents if the event is the root of the tree
@column.dateTime({ autoCreate: true })
public createdAt: DateTime
......
import { rules, schema } from '@ioc:Adonis/Core/Validator'
export const createEventSchema = schema.create({
name: schema.string([rules.minLength(1), rules.maxLength(255)]),
parentId: schema.number.optional([rules.exists({ table: 'events', column: 'id' })]),
thumbnailId: schema.number([rules.exists({ table: 'pictures', column: 'id' })]),
})
import { rules, schema } from '@ioc:Adonis/Core/Validator'
export const getPicturesSchema = schema.create({
params: schema.object().members({
eventId: schema.number([rules.exists({ table: 'events', column: 'id' })]),
}),
})
export const createPictureSchema = schema.create({
fileName: schema.string([rules.minLength(1), rules.maxLength(255)]),
author: schema.string([rules.minLength(1), rules.maxLength(255)]),
eventId: schema.number([rules.exists({ table: 'events', column: 'id' })]),
})
/** Tree structure */
export type Tree<T> = { father: T; sons: Tree<T>[] }
......@@ -13,8 +13,6 @@ export default class extends BaseSchema {
table.integer('parent_id').nullable().references('id').inTable('events').onDelete('CASCADE')
table.index('parent_id')
table.boolean('is_root').notNullable()
/**
* Uses timestamptz for PostgreSQL and DATETIME2 for MSSQL
*/
......
import Factory from '@ioc:Adonis/Lucid/Factory'
import BaseSeeder from '@ioc:Adonis/Lucid/Seeder'
import Event from 'App/Models/Event'
const EventFactory = Factory.define(Event, async ({ faker }) => {
return {
name: `${faker.word.adjective()} ${faker.word.noun()} ${faker.word.adverb()}`,
thumbnailId: faker.number.int({ min: 1, max: 50 }),
parentId: null,
}
}).build()
export default class extends BaseSeeder {
public async run() {
// Create root events
await EventFactory.createMany(6)
// Create children
const events = await Event.all()
for (const event of events) {
await EventFactory.merge({ parentId: event.id }).createMany(5)
}
// Create grandchildren
const moreEvents = await Event.all()
for (const event of moreEvents) {
await EventFactory.merge({ parentId: event.id }).createMany(3)
}
}
}
......@@ -6,7 +6,7 @@ export default class extends BaseSeeder {
await NotSigmaUser.createMany([
{
username: 'admin',
password: 'admin',
password: '0000',
isAdmin: true,
},
{
......
import Factory from '@ioc:Adonis/Lucid/Factory'
import BaseSeeder from '@ioc:Adonis/Lucid/Seeder'
import Picture from 'App/Models/Picture'
const PictureFactory = Factory.define(Picture, async ({ faker }) => {
return {
fileName: `${faker.music.songName()}.jpg`,
author: `${faker.person.firstName()} ${faker.person.lastName()}`,
eventId: faker.number.int({ min: 1, max: 50 }),
}
}).build()
export default class extends BaseSeeder {
public async run() {
// Create root events
await PictureFactory.createMany(300)
}
}
......@@ -21,9 +21,7 @@ import Route from '@ioc:Adonis/Core/Route'
/** ==== Auth ==== */
Route.group(() => {
Route.group(() => {
Route.post('/login', 'AuthController.loginNotSigmaUser')
}).prefix('/notSigmaUser')
Route.post('/notSigmaUser/login', 'AuthController.loginNotSigmaUser')
Route.group(() => {
Route.get('/login', 'AuthController.loginSigmaUser')
......@@ -36,17 +34,19 @@ Route.group(() => {
/** ==== Events ==== */
Route.group(() => {
Route.get('/getAllRoots', 'EventController.getAllRoots')
Route.get('/getAllSons/:id', 'EventController.getAllSons')
Route.post('/create', 'EventController.createEvent')
// A particular event
Route.group(() => {
Route.get('/children', 'EventController.getChildren')
Route.get('/pictures', 'PictureController.getPictures')
Route.post('/createPicture', 'PictureController.createPicture')
})
.prefix('/:eventId')
.where('eventId', Route.matchers.number())
})
.prefix('/event')
.middleware('auth:api')
/** ==== Pictures ==== */
Route.group(() => {
Route.get('/get/:id', 'PictureController.getPictures')
})
.prefix('/picture')
.middleware('auth:api')
/** ==== Admin ==== */
Route.group(() => {
......
......@@ -5,9 +5,9 @@
* file.
*/
import type { Config } from '@japa/runner'
import TestUtils from '@ioc:Adonis/Core/TestUtils'
import { assert, runFailedTests, specReporter, apiClient } from '@japa/preset-adonis'
import { apiClient, assert, runFailedTests, specReporter } from '@japa/preset-adonis'
import type { Config } from '@japa/runner'
/*
|--------------------------------------------------------------------------
......@@ -47,7 +47,11 @@ export const reporters: Required<Config>['reporters'] = [specReporter()]
|
*/
export const runnerHooks: Pick<Required<Config>, 'setup' | 'teardown'> = {
setup: [() => TestUtils.ace().loadCommands()],
setup: [
() => TestUtils.ace().loadCommands(),
() => TestUtils.db().migrate(),
() => TestUtils.db().seed(),
],
teardown: [],
}
......
import { test } from '@japa/runner'
import { getBearerToken } from '../test.utils'
test('Unauthorized', async ({ client }) => {
const response = await client.get('/event/getAllRoots')
response.assertStatus(401)
const response2 = await client.post('/admin/notSigmaUser/create').json({
username: 'test',
})
response2.assertStatus(401)
})
test('Logged in successfuly', async ({ client }) => {
const token = await getBearerToken(client, 'thibaut', '0000')
const response = await client.get('/event/getAllRoots').bearerToken(token)
response.assertStatus(200)
})
test('Invalid credential', async ({ client }) => {
const response = await client.post('/auth/notSigmaUser/login').json({
username: 'thibaut',
password: '1234',
})
response.assertStatus(401)
const response2 = await client.post('/auth/notSigmaUser/login').json({
username: 'tarwgnwe',
password: '0000',
})
response2.assertStatus(401)
})
test('Create user', async ({ client }) => {
const token = await getBearerToken(client, 'admin', '0000')
const response = await client
.post('/admin/notSigmaUser/create')
.bearerToken(token)
.json({
username: `test${Math.random()}`, // To avoid conflicts
})
response.assertStatus(201)
})
test('Create user without being admin', async ({ client }) => {
const token = await getBearerToken(client, 'thibaut', '0000')
const response = await client
.post('/admin/notSigmaUser/create')
.header('Authorization', `Bearer ${token}`)
.json({
username: 'test',
})
response.assertStatus(401)
})
import { test } from '@japa/runner'
test('display welcome page', async ({ client }) => {
const response = await client.get('/')
response.assertStatus(200)
response.assertBodyContains({ hello: 'world' })
})
import { ApiClient } from '@japa/api-client'
/** Helper to login to the api and obtain an object with the token header */
export const getBearerToken = async (client: ApiClient, username: string, password: string) => {
const response = await client.post('/auth/notSigmaUser/login').json({
username,
password,
})
response.assertStatus(200)
return response.body().token as string
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment