From 2faf3438f151d209e92e3f0781c3c26f310849ef Mon Sep 17 00:00:00 2001 From: jean Date: Sat, 21 Sep 2024 12:56:34 +0200 Subject: [PATCH 1/3] chore: upgrade fastify related packages --- package.json | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 85f45e4..65ef7a0 100644 --- a/package.json +++ b/package.json @@ -24,23 +24,23 @@ "author": "Michelet Jean ", "license": "MIT", "dependencies": { - "@fastify/autoload": "^5.10.0", - "@fastify/cors": "^9.0.1", - "@fastify/env": "^4.3.0", - "@fastify/helmet": "^11.1.1", - "@fastify/jwt": "^8.0.1", - "@fastify/mysql": "^4.3.0", - "@fastify/rate-limit": "^9.1.0", - "@fastify/sensible": "^5.0.0", - "@fastify/swagger": "^8.14.0", - "@fastify/swagger-ui": "^4.0.1", - "@fastify/type-provider-typebox": "^4.0.0", - "@fastify/under-pressure": "^8.3.0", + "@fastify/autoload": "^6.0.0", + "@fastify/cors": "^10.0.0", + "@fastify/env": "^5.0.1", + "@fastify/helmet": "^12.0.0", + "@fastify/jwt": "^9.0.0", + "@fastify/mysql": "^5.0.1", + "@fastify/rate-limit": "^10.0.1", + "@fastify/sensible": "^6.0.1", + "@fastify/swagger": "^9.0.0", + "@fastify/swagger-ui": "^5.0.1", + "@fastify/type-provider-typebox": "^5.0.0", + "@fastify/under-pressure": "^9.0.1", "@sinclair/typebox": "^0.33.7", "concurrently": "^8.2.2", - "fastify": "^4.26.1", - "fastify-cli": "^6.1.1", - "fastify-plugin": "^4.0.0", + "fastify": "^5.0.0", + "fastify-cli": "^7.0.0", + "fastify-plugin": "^5.0.1", "postgrator": "^7.2.0" }, "devDependencies": { From b24d63a75aca1d6d66821ed934beff2fc36ef086 Mon Sep 17 00:00:00 2001 From: jean Date: Sat, 21 Sep 2024 13:01:16 +0200 Subject: [PATCH 2/3] chore: update fastify non-related packages --- package.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 65ef7a0..8384ea4 100644 --- a/package.json +++ b/package.json @@ -36,20 +36,20 @@ "@fastify/swagger-ui": "^5.0.1", "@fastify/type-provider-typebox": "^5.0.0", "@fastify/under-pressure": "^9.0.1", - "@sinclair/typebox": "^0.33.7", - "concurrently": "^8.2.2", + "@sinclair/typebox": "^0.33.12", + "concurrently": "^9.0.1", "fastify": "^5.0.0", "fastify-cli": "^7.0.0", "fastify-plugin": "^5.0.1", - "postgrator": "^7.2.0" + "postgrator": "^7.3.0" }, "devDependencies": { - "@types/node": "^22.0.0", - "eslint": "^9.4.0", + "@types/node": "^22.5.5", + "eslint": "^9.11.0", "fastify-tsconfig": "^2.0.0", - "mysql2": "^3.10.1", - "neostandard": "^0.7.0", + "mysql2": "^3.11.3", + "neostandard": "^0.11.5", "tap": "^21.0.1", - "typescript": "^5.4.5" + "typescript": "^5.6.2" } } From 50e0a020634fcd40d4a2e36d72a038fabd31761d Mon Sep 17 00:00:00 2001 From: jean Date: Sat, 21 Sep 2024 13:05:51 +0200 Subject: [PATCH 3/3] fix: eslint errors --- @types/fastify/fastify.d.ts | 10 +- @types/node/environment.d.ts | 2 +- src/app.ts | 81 +++--- src/plugins/custom/repository.ts | 96 +++---- src/plugins/custom/scrypt.ts | 40 ++- src/plugins/external/cors.ts | 6 +- src/plugins/external/env.ts | 42 +-- src/plugins/external/helmet.ts | 6 +- src/plugins/external/jwt.ts | 10 +- src/plugins/external/mysql.ts | 16 +- src/plugins/external/rate-limit.ts | 12 +- src/plugins/external/sensible.ts | 6 +- src/plugins/external/swagger.ts | 18 +- src/plugins/external/under-pressure.ts | 30 +-- src/routes/api/auth/index.ts | 28 +- src/routes/api/autohooks.ts | 11 +- src/routes/api/index.ts | 8 +- src/routes/api/tasks/index.ts | 100 +++---- src/routes/home.ts | 12 +- src/schemas/auth.ts | 4 +- src/schemas/tasks.ts | 17 +- src/server.ts | 48 ++-- test/app/cors.test.ts | 34 +-- test/app/error-handler.test.ts | 38 +-- test/app/not-found-handler.test.ts | 46 ++-- test/app/rate-limit.test.ts | 40 +-- test/helper.ts | 51 ++-- test/plugins/repository.test.ts | 78 +++--- test/plugins/scrypt.test.ts | 36 +-- test/routes/api/api.test.ts | 54 ++-- test/routes/api/auth/auth.test.ts | 64 ++--- test/routes/api/tasks/tasks.test.ts | 344 ++++++++++++------------- test/routes/home.test.ts | 20 +- 33 files changed, 698 insertions(+), 710 deletions(-) diff --git a/@types/fastify/fastify.d.ts b/@types/fastify/fastify.d.ts index f85cbfd..b9d797b 100644 --- a/@types/fastify/fastify.d.ts +++ b/@types/fastify/fastify.d.ts @@ -1,7 +1,7 @@ -import { Auth } from "../../src/schemas/auth.ts"; +import { Auth } from '../../src/schemas/auth.ts' -declare module "fastify" { - export interface FastifyRequest { - user: Auth - } +declare module 'fastify' { + export interface FastifyRequest { + user: Auth + } } diff --git a/@types/node/environment.d.ts b/@types/node/environment.d.ts index 7105b75..0069d75 100644 --- a/@types/node/environment.d.ts +++ b/@types/node/environment.d.ts @@ -13,4 +13,4 @@ declare global { } } -export {}; \ No newline at end of file +export {} diff --git a/src/app.ts b/src/app.ts index eeaff46..b36f698 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,20 +2,20 @@ * If you would like to turn your application into a standalone executable, look at server.js file */ -import path from "node:path"; -import fastifyAutoload from "@fastify/autoload"; -import { FastifyInstance, FastifyPluginOptions } from "fastify"; +import path from 'node:path' +import fastifyAutoload from '@fastify/autoload' +import { FastifyInstance, FastifyPluginOptions } from 'fastify' export const options = { ajv: { customOptions: { - coerceTypes: "array", - removeAdditional: "all" + coerceTypes: 'array', + removeAdditional: 'all' } } -}; +} -export default async function serviceApp( +export default async function serviceApp ( fastify: FastifyInstance, opts: FastifyPluginOptions ) { @@ -23,26 +23,26 @@ export default async function serviceApp( // This loads all external plugins defined in plugins/external // those should be registered first as your custom plugins might depend on them await fastify.register(fastifyAutoload, { - dir: path.join(import.meta.dirname, "plugins/external"), + dir: path.join(import.meta.dirname, 'plugins/external'), options: { ...opts } - }); + }) // This loads all your custom plugins defined in plugins/custom // those should be support plugins that are reused // through your application fastify.register(fastifyAutoload, { - dir: path.join(import.meta.dirname, "plugins/custom"), + dir: path.join(import.meta.dirname, 'plugins/custom'), options: { ...opts } - }); + }) // This loads all plugins defined in routes // define your routes in one of these fastify.register(fastifyAutoload, { - dir: path.join(import.meta.dirname, "routes"), + dir: path.join(import.meta.dirname, 'routes'), autoHooks: true, cascadeHooks: true, options: { ...opts } - }); + }) fastify.setErrorHandler((err, request, reply) => { request.log.error( @@ -55,43 +55,42 @@ export default async function serviceApp( params: request.params } }, - "Unhandled error occurred" - ); + 'Unhandled error occurred' + ) - reply.code(err.statusCode ?? 500); + reply.code(err.statusCode ?? 500) - let message = "Internal Server Error"; + let message = 'Internal Server Error' if (err.statusCode === 401) { - message = err.message; + message = err.message } - return { message }; - }); + return { message } + }) // An attacker could search for valid URLs if your 404 error handling is not rate limited. fastify.setNotFoundHandler( - { - preHandler: fastify.rateLimit({ - max: 3, - timeWindow: 500 - }) - }, + { + preHandler: fastify.rateLimit({ + max: 3, + timeWindow: 500 + }) + }, (request, reply) => { + request.log.warn( + { + request: { + method: request.method, + url: request.url, + query: request.query, + params: request.params + } + }, + 'Resource not found' + ) - request.log.warn( - { - request: { - method: request.method, - url: request.url, - query: request.query, - params: request.params - } - }, - "Resource not found" - ); - - reply.code(404); + reply.code(404) - return { message: "Not Found" }; - }); + return { message: 'Not Found' } + }) } diff --git a/src/plugins/custom/repository.ts b/src/plugins/custom/repository.ts index da3cd51..1ef3d7f 100644 --- a/src/plugins/custom/repository.ts +++ b/src/plugins/custom/repository.ts @@ -1,96 +1,96 @@ -import { MySQLPromisePool } from "@fastify/mysql"; -import { FastifyInstance } from "fastify"; -import fp from "fastify-plugin"; -import { RowDataPacket, ResultSetHeader } from "mysql2"; +import { MySQLPromisePool } from '@fastify/mysql' +import { FastifyInstance } from 'fastify' +import fp from 'fastify-plugin' +import { RowDataPacket, ResultSetHeader } from 'mysql2' -declare module "fastify" { +declare module 'fastify' { export interface FastifyInstance { repository: Repository; } } -export type Repository = MySQLPromisePool & ReturnType; +export type Repository = MySQLPromisePool & ReturnType -type QuerySeparator = 'AND' | ','; +type QuerySeparator = 'AND' | ',' type QueryOptions = { select?: string; where?: Record; -}; +} type WriteOptions = { data: Record; where?: Record; -}; +} -function createRepository(fastify: FastifyInstance) { +function createRepository (fastify: FastifyInstance) { const processAssignmentRecord = (record: Record, separator: QuerySeparator) => { - const keys = Object.keys(record); - const values = Object.values(record); - const clause = keys.map((key) => `${key} = ?`).join(` ${separator} `); + const keys = Object.keys(record) + const values = Object.values(record) + const clause = keys.map((key) => `${key} = ?`).join(` ${separator} `) - return [clause, values] as const; - }; + return [clause, values] as const + } const repository = { ...fastify.mysql, find: async (table: string, opts: QueryOptions): Promise => { - const { select = '*', where = {1:1} } = opts; - const [clause, values] = processAssignmentRecord(where, 'AND'); + const { select = '*', where = { 1: 1 } } = opts + const [clause, values] = processAssignmentRecord(where, 'AND') - const query = `SELECT ${select} FROM ${table} WHERE ${clause} LIMIT 1`; - const [rows] = await fastify.mysql.query(query, values); + const query = `SELECT ${select} FROM ${table} WHERE ${clause} LIMIT 1` + const [rows] = await fastify.mysql.query(query, values) if (rows.length < 1) { - return null; + return null } - return rows[0] as T; + return rows[0] as T }, findMany: async (table: string, opts: QueryOptions = {}): Promise => { - const { select = '*', where = {1:1} } = opts; - const [clause, values] = processAssignmentRecord(where, 'AND'); + const { select = '*', where = { 1: 1 } } = opts + const [clause, values] = processAssignmentRecord(where, 'AND') - const query = `SELECT ${select} FROM ${table} WHERE ${clause}`; - const [rows] = await fastify.mysql.query(query, values); + const query = `SELECT ${select} FROM ${table} WHERE ${clause}` + const [rows] = await fastify.mysql.query(query, values) - return rows as T[]; + return rows as T[] }, create: async (table: string, opts: WriteOptions): Promise => { - const { data } = opts; - const columns = Object.keys(data).join(', '); - const placeholders = Object.keys(data).map(() => '?').join(', '); - const values = Object.values(data); + const { data } = opts + const columns = Object.keys(data).join(', ') + const placeholders = Object.keys(data).map(() => '?').join(', ') + const values = Object.values(data) - const query = `INSERT INTO ${table} (${columns}) VALUES (${placeholders})`; - const [result] = await fastify.mysql.query(query, values); + const query = `INSERT INTO ${table} (${columns}) VALUES (${placeholders})` + const [result] = await fastify.mysql.query(query, values) - return result.insertId; + return result.insertId }, update: async (table: string, opts: WriteOptions): Promise => { - const { data, where = {} } = opts; - const [dataClause, dataValues] = processAssignmentRecord(data, ','); - const [whereClause, whereValues] = processAssignmentRecord(where, 'AND'); + const { data, where = {} } = opts + const [dataClause, dataValues] = processAssignmentRecord(data, ',') + const [whereClause, whereValues] = processAssignmentRecord(where, 'AND') - const query = `UPDATE ${table} SET ${dataClause} WHERE ${whereClause}`; - const [result] = await fastify.mysql.query(query, [...dataValues, ...whereValues]); + const query = `UPDATE ${table} SET ${dataClause} WHERE ${whereClause}` + const [result] = await fastify.mysql.query(query, [...dataValues, ...whereValues]) - return result.affectedRows; + return result.affectedRows }, delete: async (table: string, where: Record): Promise => { - const [clause, values] = processAssignmentRecord(where, 'AND'); + const [clause, values] = processAssignmentRecord(where, 'AND') - const query = `DELETE FROM ${table} WHERE ${clause}`; - const [result] = await fastify.mysql.query(query, values); + const query = `DELETE FROM ${table} WHERE ${clause}` + const [result] = await fastify.mysql.query(query, values) - return result.affectedRows; + return result.affectedRows } - }; + } - return repository; + return repository } /** @@ -101,9 +101,9 @@ function createRepository(fastify: FastifyInstance) { */ export default fp( async function (fastify) { - fastify.decorate("repository", createRepository(fastify)); + fastify.decorate('repository', createRepository(fastify)) // You should name your plugins if you want to avoid name collisions // and/or to perform dependency checks. }, - { name: "repository", dependencies: ['mysql'] } -); + { name: 'repository', dependencies: ['mysql'] } +) diff --git a/src/plugins/custom/scrypt.ts b/src/plugins/custom/scrypt.ts index f643377..1c78b8e 100644 --- a/src/plugins/custom/scrypt.ts +++ b/src/plugins/custom/scrypt.ts @@ -1,12 +1,12 @@ -import fp from 'fastify-plugin'; +import fp from 'fastify-plugin' import { scrypt, timingSafeEqual, randomBytes } from 'crypto' -declare module "fastify" { - export interface FastifyInstance { - hash: typeof scryptHash; - compare: typeof compare - } +declare module 'fastify' { + export interface FastifyInstance { + hash: typeof scryptHash; + compare: typeof compare } +} const SCRYPT_KEYLEN = 32 const SCRYPT_COST = 65536 @@ -14,11 +14,11 @@ const SCRYPT_BLOCK_SIZE = 8 const SCRYPT_PARALLELIZATION = 2 const SCRYPT_MAXMEM = 128 * SCRYPT_COST * SCRYPT_BLOCK_SIZE * 2 -async function scryptHash(value: string): Promise { +async function scryptHash (value: string): Promise { return new Promise((resolve, reject) => { const salt = randomBytes(Math.min(16, SCRYPT_KEYLEN / 2)) - scrypt(value, salt, SCRYPT_KEYLEN, { + scrypt(value, salt, SCRYPT_KEYLEN, { cost: SCRYPT_COST, blockSize: SCRYPT_BLOCK_SIZE, parallelization: SCRYPT_PARALLELIZATION, @@ -27,22 +27,20 @@ async function scryptHash(value: string): Promise { /* c8 ignore start - Requires extreme or impractical configuration values */ if (error !== null) { reject(error) - } - /* c8 ignore end */ - else { + } /* c8 ignore end */ else { resolve(`${salt.toString('hex')}.${key.toString('hex')}`) } }) }) } -async function compare(value: string, hash: string): Promise { +async function compare (value: string, hash: string): Promise { const [salt, hashed] = hash.split('.') - const saltBuffer = Buffer.from(salt, 'hex'); + const saltBuffer = Buffer.from(salt, 'hex') const hashedBuffer = Buffer.from(hashed, 'hex') return new Promise((resolve) => { - scrypt(value, saltBuffer, SCRYPT_KEYLEN, { + scrypt(value, saltBuffer, SCRYPT_KEYLEN, { cost: SCRYPT_COST, blockSize: SCRYPT_BLOCK_SIZE, parallelization: SCRYPT_PARALLELIZATION, @@ -52,18 +50,16 @@ async function compare(value: string, hash: string): Promise { if (error !== null) { timingSafeEqual(hashedBuffer, hashedBuffer) resolve(false) - } - /* c8 ignore end */ - else { - resolve(timingSafeEqual(key, hashedBuffer)) - } + } /* c8 ignore end */ else { + resolve(timingSafeEqual(key, hashedBuffer)) + } }) }) } export default fp(async (fastify) => { - fastify.decorate('hash', scryptHash); - fastify.decorate('compare', compare); + fastify.decorate('hash', scryptHash) + fastify.decorate('compare', compare) }, { name: 'scrypt' -}); +}) diff --git a/src/plugins/external/cors.ts b/src/plugins/external/cors.ts index 1b6906a..7eaaa6b 100644 --- a/src/plugins/external/cors.ts +++ b/src/plugins/external/cors.ts @@ -1,12 +1,12 @@ -import cors, { FastifyCorsOptions } from "@fastify/cors"; +import cors, { FastifyCorsOptions } from '@fastify/cors' export const autoConfig: FastifyCorsOptions = { methods: ['GET', 'POST', 'PUT', 'DELETE'] -}; +} /** * This plugins enables the use of CORS. * * @see {@link https://github.com/fastify/fastify-cors} */ -export default cors; +export default cors diff --git a/src/plugins/external/env.ts b/src/plugins/external/env.ts index 94613ba..68a7f53 100644 --- a/src/plugins/external/env.ts +++ b/src/plugins/external/env.ts @@ -1,6 +1,6 @@ -import env from "@fastify/env"; +import env from '@fastify/env' -declare module "fastify" { +declare module 'fastify' { export interface FastifyInstance { config: { PORT: number; @@ -16,50 +16,50 @@ declare module "fastify" { } const schema = { - type: "object", + type: 'object', required: [ - "MYSQL_HOST", - "MYSQL_PORT", - "MYSQL_USER", - "MYSQL_PASSWORD", - "MYSQL_DATABASE", - "JWT_SECRET" + 'MYSQL_HOST', + 'MYSQL_PORT', + 'MYSQL_USER', + 'MYSQL_PASSWORD', + 'MYSQL_DATABASE', + 'JWT_SECRET' ], properties: { // Database MYSQL_HOST: { - type: "string", - default: "localhost" + type: 'string', + default: 'localhost' }, MYSQL_PORT: { - type: "number", + type: 'number', default: 3306 }, MYSQL_USER: { - type: "string" + type: 'string' }, MYSQL_PASSWORD: { - type: "string" + type: 'string' }, MYSQL_DATABASE: { - type: "string" + type: 'string' }, // Security JWT_SECRET: { - type: "string" + type: 'string' }, RATE_LIMIT_MAX: { - type: "number", + type: 'number', default: 100 } } -}; +} export const autoConfig = { // Decorate Fastify instance with `config` key // Optional, default: 'config' - confKey: "config", + confKey: 'config', // Schema to validate schema, @@ -75,11 +75,11 @@ export const autoConfig = { // Source for the configuration data // Optional, default: process.env data: process.env -}; +} /** * This plugins helps to check environment variables. * * @see {@link https://github.com/fastify/fastify-env} */ -export default env; +export default env diff --git a/src/plugins/external/helmet.ts b/src/plugins/external/helmet.ts index 86f7e5e..77d6112 100644 --- a/src/plugins/external/helmet.ts +++ b/src/plugins/external/helmet.ts @@ -1,12 +1,12 @@ -import helmet from "@fastify/helmet"; +import helmet from '@fastify/helmet' export const autoConfig = { // Set plugin options here -}; +} /** * This plugins sets the basic security headers. * * @see {@link https://github.com/fastify/fastify-helmet} */ -export default helmet; +export default helmet diff --git a/src/plugins/external/jwt.ts b/src/plugins/external/jwt.ts index 60f4e34..f4213ec 100644 --- a/src/plugins/external/jwt.ts +++ b/src/plugins/external/jwt.ts @@ -1,10 +1,10 @@ -import fastifyJwt from "@fastify/jwt"; -import { FastifyInstance } from "fastify"; +import fastifyJwt from '@fastify/jwt' +import { FastifyInstance } from 'fastify' export const autoConfig = (fastify: FastifyInstance) => { return { secret: fastify.config.JWT_SECRET - }; -}; + } +} -export default fastifyJwt; +export default fastifyJwt diff --git a/src/plugins/external/mysql.ts b/src/plugins/external/mysql.ts index 73051bf..0d40193 100644 --- a/src/plugins/external/mysql.ts +++ b/src/plugins/external/mysql.ts @@ -1,8 +1,8 @@ -import fp from "fastify-plugin"; -import fastifyMysql, { MySQLPromisePool } from "@fastify/mysql"; -import { FastifyInstance } from "fastify"; +import fp from 'fastify-plugin' +import fastifyMysql, { MySQLPromisePool } from '@fastify/mysql' +import { FastifyInstance } from 'fastify' -declare module "fastify" { +declare module 'fastify' { export interface FastifyInstance { mysql: MySQLPromisePool; } @@ -16,9 +16,9 @@ export const autoConfig = (fastify: FastifyInstance) => { password: fastify.config.MYSQL_PASSWORD, database: fastify.config.MYSQL_DATABASE, port: Number(fastify.config.MYSQL_PORT) - }; -}; + } +} export default fp(fastifyMysql, { - name: "mysql" -}); + name: 'mysql' +}) diff --git a/src/plugins/external/rate-limit.ts b/src/plugins/external/rate-limit.ts index d366bd9..2e5821e 100644 --- a/src/plugins/external/rate-limit.ts +++ b/src/plugins/external/rate-limit.ts @@ -1,11 +1,11 @@ -import fastifyRateLimit from "@fastify/rate-limit"; -import { FastifyInstance } from "fastify"; +import fastifyRateLimit from '@fastify/rate-limit' +import { FastifyInstance } from 'fastify' export const autoConfig = (fastify: FastifyInstance) => { - return { - max: fastify.config.RATE_LIMIT_MAX, - timeWindow: "1 minute" - } + return { + max: fastify.config.RATE_LIMIT_MAX, + timeWindow: '1 minute' + } } /** diff --git a/src/plugins/external/sensible.ts b/src/plugins/external/sensible.ts index fba1393..213e09d 100644 --- a/src/plugins/external/sensible.ts +++ b/src/plugins/external/sensible.ts @@ -1,12 +1,12 @@ -import sensible from "@fastify/sensible"; +import sensible from '@fastify/sensible' export const autoConfig = { // Set plugin options here -}; +} /** * This plugin adds some utilities to handle http errors * * @see {@link https://github.com/fastify/fastify-sensible} */ -export default sensible; +export default sensible diff --git a/src/plugins/external/swagger.ts b/src/plugins/external/swagger.ts index 5b8ccfe..3a56961 100644 --- a/src/plugins/external/swagger.ts +++ b/src/plugins/external/swagger.ts @@ -1,6 +1,6 @@ -import fp from "fastify-plugin"; -import fastifySwaggerUi from "@fastify/swagger-ui"; -import fastifySwagger from "@fastify/swagger"; +import fp from 'fastify-plugin' +import fastifySwaggerUi from '@fastify/swagger-ui' +import fastifySwagger from '@fastify/swagger' export default fp(async function (fastify) { /** @@ -12,12 +12,12 @@ export default fp(async function (fastify) { hideUntagged: true, openapi: { info: { - title: "Fastify demo API", - description: "The official Fastify demo API", - version: "0.0.0" + title: 'Fastify demo API', + description: 'The official Fastify demo API', + version: '0.0.0' } } - }); + }) /** * A Fastify plugin for serving Swagger UI. @@ -26,5 +26,5 @@ export default fp(async function (fastify) { */ await fastify.register(fastifySwaggerUi, { routePrefix: '/api/docs' - }); -}); + }) +}) diff --git a/src/plugins/external/under-pressure.ts b/src/plugins/external/under-pressure.ts index e9efd06..103a613 100644 --- a/src/plugins/external/under-pressure.ts +++ b/src/plugins/external/under-pressure.ts @@ -1,6 +1,6 @@ -import { FastifyInstance } from "fastify"; -import fastifyUnderPressure from "@fastify/under-pressure"; -import fp from "fastify-plugin"; +import { FastifyInstance } from 'fastify' +import fastifyUnderPressure from '@fastify/under-pressure' +import fp from 'fastify-plugin' export const autoConfig = (fastify: FastifyInstance) => { return { @@ -8,26 +8,26 @@ export const autoConfig = (fastify: FastifyInstance) => { maxHeapUsedBytes: 100_000_000, maxRssBytes: 1_000_000_000, maxEventLoopUtilization: 0.98, - message: "The server is under pressure, retry later!", + message: 'The server is under pressure, retry later!', retryAfter: 50, healthCheck: async () => { - let connection; + let connection try { - connection = await fastify.mysql.getConnection(); - await connection.query("SELECT 1;"); - return true; + connection = await fastify.mysql.getConnection() + await connection.query('SELECT 1;') + return true /* c8 ignore start */ } catch (err) { - fastify.log.error(err, "healthCheck has failed"); - throw new Error("Database connection is not available"); + fastify.log.error(err, 'healthCheck has failed') + throw new Error('Database connection is not available') } finally { - connection?.release(); + connection?.release() } /* c8 ignore stop */ }, healthCheckInterval: 5000 - }; -}; + } +} /** * A Fastify plugin for mesuring process load and automatically @@ -39,5 +39,5 @@ export const autoConfig = (fastify: FastifyInstance) => { * @see {@link https://www.youtube.com/watch?v=VI29mUA8n9w} */ export default fp(fastifyUnderPressure, { - dependencies: ["mysql"] -}); + dependencies: ['mysql'] +}) diff --git a/src/routes/api/auth/index.ts b/src/routes/api/auth/index.ts index 174a982..292f719 100644 --- a/src/routes/api/auth/index.ts +++ b/src/routes/api/auth/index.ts @@ -1,12 +1,12 @@ import { FastifyPluginAsyncTypebox, Type -} from "@fastify/type-provider-typebox"; -import { CredentialsSchema, Auth } from "../../../schemas/auth.js"; +} from '@fastify/type-provider-typebox' +import { CredentialsSchema, Auth } from '../../../schemas/auth.js' const plugin: FastifyPluginAsyncTypebox = async (fastify) => { fastify.post( - "/login", + '/login', { schema: { body: CredentialsSchema, @@ -18,11 +18,11 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { message: Type.String() }) }, - tags: ["Authentication"] + tags: ['Authentication'] } }, async function (request, reply) { - const { username, password } = request.body; + const { username, password } = request.body const user = await fastify.repository.find('users', { select: 'username, password', @@ -30,19 +30,19 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { }) if (user) { - const isPasswordValid = await fastify.compare(password, user.password); + const isPasswordValid = await fastify.compare(password, user.password) if (isPasswordValid) { - const token = fastify.jwt.sign({ username: user.username }); + const token = fastify.jwt.sign({ username: user.username }) - return { token }; + return { token } } } - reply.status(401); - - return { message: "Invalid username or password." }; + reply.status(401) + + return { message: 'Invalid username or password.' } } - ); -}; + ) +} -export default plugin; +export default plugin diff --git a/src/routes/api/autohooks.ts b/src/routes/api/autohooks.ts index 3a554d4..08d7034 100644 --- a/src/routes/api/autohooks.ts +++ b/src/routes/api/autohooks.ts @@ -1,10 +1,9 @@ -import { FastifyInstance } from "fastify"; - +import { FastifyInstance } from 'fastify' export default async function (fastify: FastifyInstance) { - fastify.addHook("onRequest", async (request) => { - if (!request.url.startsWith("/api/auth/login")) { - await request.jwtVerify(); + fastify.addHook('onRequest', async (request) => { + if (!request.url.startsWith('/api/auth/login')) { + await request.jwtVerify() } - }); + }) } diff --git a/src/routes/api/index.ts b/src/routes/api/index.ts index 71cc046..d897d91 100644 --- a/src/routes/api/index.ts +++ b/src/routes/api/index.ts @@ -1,10 +1,10 @@ -import { FastifyInstance } from "fastify"; +import { FastifyInstance } from 'fastify' export default async function (fastify: FastifyInstance) { - fastify.get("/", ({ user, protocol, hostname }) => { + fastify.get('/', ({ user, protocol, hostname }) => { return { message: `Hello ${user.username}! See documentation at ${protocol}://${hostname}/documentation` - }; - }); + } + }) } diff --git a/src/routes/api/tasks/index.ts b/src/routes/api/tasks/index.ts index 8db0749..4ad70d9 100644 --- a/src/routes/api/tasks/index.ts +++ b/src/routes/api/tasks/index.ts @@ -1,36 +1,36 @@ import { FastifyPluginAsyncTypebox, Type -} from "@fastify/type-provider-typebox"; +} from '@fastify/type-provider-typebox' import { TaskSchema, Task, CreateTaskSchema, UpdateTaskSchema, TaskStatus -} from "../../../schemas/tasks.js"; -import { FastifyReply } from "fastify"; +} from '../../../schemas/tasks.js' +import { FastifyReply } from 'fastify' const plugin: FastifyPluginAsyncTypebox = async (fastify) => { fastify.get( - "/", + '/', { schema: { response: { 200: Type.Array(TaskSchema) }, - tags: ["Tasks"] + tags: ['Tasks'] } }, async function () { - const tasks = await fastify.repository.findMany("tasks"); + const tasks = await fastify.repository.findMany('tasks') - return tasks; + return tasks } - ); + ) fastify.get( - "/:id", + '/:id', { schema: { params: Type.Object({ @@ -40,23 +40,23 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { 200: TaskSchema, 404: Type.Object({ message: Type.String() }) }, - tags: ["Tasks"] + tags: ['Tasks'] } }, async function (request, reply) { - const { id } = request.params; - const task = await fastify.repository.find("tasks", { where: { id } }); + const { id } = request.params + const task = await fastify.repository.find('tasks', { where: { id } }) if (!task) { - return notFound(reply); + return notFound(reply) } - return task; + return task } - ); + ) fastify.post( - "/", + '/', { schema: { body: CreateTaskSchema, @@ -65,21 +65,21 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { id: Type.Number() } }, - tags: ["Tasks"] + tags: ['Tasks'] } }, async function (request, reply) { - const id = await fastify.repository.create("tasks", { data: {...request.body, status: TaskStatus.New} }); - reply.code(201); + const id = await fastify.repository.create('tasks', { data: { ...request.body, status: TaskStatus.New } }) + reply.code(201) return { id - }; + } } - ); + ) fastify.patch( - "/:id", + '/:id', { schema: { params: Type.Object({ @@ -90,28 +90,28 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { 200: TaskSchema, 404: Type.Object({ message: Type.String() }) }, - tags: ["Tasks"] + tags: ['Tasks'] } }, async function (request, reply) { - const { id } = request.params; - const affectedRows = await fastify.repository.update("tasks", { + const { id } = request.params + const affectedRows = await fastify.repository.update('tasks', { data: request.body, where: { id } - }); + }) if (affectedRows === 0) { return notFound(reply) } - const task = await fastify.repository.find("tasks", { where: { id } }); + const task = await fastify.repository.find('tasks', { where: { id } }) - return task as Task; + return task as Task } - ); + ) fastify.delete( - "/:id", + '/:id', { schema: { params: Type.Object({ @@ -121,23 +121,23 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { 204: Type.Null(), 404: Type.Object({ message: Type.String() }) }, - tags: ["Tasks"] + tags: ['Tasks'] } }, async function (request, reply) { - const { id } = request.params; - const affectedRows = await fastify.repository.delete("tasks", { id }); + const { id } = request.params + const affectedRows = await fastify.repository.delete('tasks', { id }) if (affectedRows === 0) { return notFound(reply) } - reply.code(204).send(null); + reply.code(204).send(null) } - ); + ) fastify.post( - "/:id/assign", + '/:id/assign', { schema: { params: Type.Object({ @@ -150,33 +150,33 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { 200: TaskSchema, 404: Type.Object({ message: Type.String() }) }, - tags: ["Tasks"] + tags: ['Tasks'] } }, async function (request, reply) { - const { id } = request.params; - const { userId } = request.body; - - const task = await fastify.repository.find("tasks", { where: { id } }); + const { id } = request.params + const { userId } = request.body + + const task = await fastify.repository.find('tasks', { where: { id } }) if (!task) { - return notFound(reply); + return notFound(reply) } - - await fastify.repository.update("tasks", { + + await fastify.repository.update('tasks', { data: { assigned_user_id: userId }, where: { id } - }); + }) task.assigned_user_id = userId - return task; + return task } ) -}; +} -function notFound(reply: FastifyReply) { +function notFound (reply: FastifyReply) { reply.code(404) - return { message: "Task not found" } + return { message: 'Task not found' } } -export default plugin; +export default plugin diff --git a/src/routes/home.ts b/src/routes/home.ts index eb2cfd7..a3c2951 100644 --- a/src/routes/home.ts +++ b/src/routes/home.ts @@ -1,11 +1,11 @@ import { FastifyPluginAsyncTypebox, Type -} from "@fastify/type-provider-typebox"; +} from '@fastify/type-provider-typebox' const plugin: FastifyPluginAsyncTypebox = async (fastify) => { fastify.get( - "/", + '/', { schema: { response: { @@ -16,9 +16,9 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { } }, async function () { - return { message: "Welcome to the official fastify demo!" }; + return { message: 'Welcome to the official fastify demo!' } } - ); -}; + ) +} -export default plugin; +export default plugin diff --git a/src/schemas/auth.ts b/src/schemas/auth.ts index 83ba0b0..4acdb0d 100644 --- a/src/schemas/auth.ts +++ b/src/schemas/auth.ts @@ -1,8 +1,8 @@ -import { Static, Type } from "@sinclair/typebox"; +import { Static, Type } from '@sinclair/typebox' export const CredentialsSchema = Type.Object({ username: Type.String(), password: Type.String() -}); +}) export interface Auth extends Static {} diff --git a/src/schemas/tasks.ts b/src/schemas/tasks.ts index 3ab9673..3315f8f 100644 --- a/src/schemas/tasks.ts +++ b/src/schemas/tasks.ts @@ -1,4 +1,4 @@ -import { Static, Type } from "@sinclair/typebox"; +import { Static, Type } from '@sinclair/typebox' export const TaskStatus = { New: 'new', @@ -7,9 +7,9 @@ export const TaskStatus = { Completed: 'completed', Canceled: 'canceled', Archived: 'archived' -} as const; +} as const -export type TaskStatusType = typeof TaskStatus[keyof typeof TaskStatus]; +export type TaskStatusType = typeof TaskStatus[keyof typeof TaskStatus] export const TaskSchema = Type.Object({ id: Type.Number(), @@ -17,9 +17,9 @@ export const TaskSchema = Type.Object({ author_id: Type.Number(), assigned_user_id: Type.Optional(Type.Number()), status: Type.String(), - created_at: Type.String({ format: "date-time" }), - updated_at: Type.String({ format: "date-time" }) -}); + created_at: Type.String({ format: 'date-time' }), + updated_at: Type.String({ format: 'date-time' }) +}) export interface Task extends Static {} @@ -27,10 +27,9 @@ export const CreateTaskSchema = Type.Object({ name: Type.String(), author_id: Type.Number(), assigned_user_id: Type.Optional(Type.Number()) -}); +}) export const UpdateTaskSchema = Type.Object({ name: Type.Optional(Type.String()), assigned_user_id: Type.Optional(Type.Number()) -}); - +}) diff --git a/src/server.ts b/src/server.ts index 21ed663..0d80e01 100644 --- a/src/server.ts +++ b/src/server.ts @@ -5,73 +5,73 @@ * You can launch it with the command `npm run standalone` */ -import Fastify from "fastify"; -import fp from "fastify-plugin"; +import Fastify from 'fastify' +import fp from 'fastify-plugin' // Import library to exit fastify process, gracefully (if possible) -import closeWithGrace from "close-with-grace"; +import closeWithGrace from 'close-with-grace' // Import your application as a normal plugin. -import serviceApp from "./app.js"; +import serviceApp from './app.js' /** * Do not use NODE_ENV to determine what logger (or any env related feature) to use * @see {@link https://www.youtube.com/watch?v=HMM7GJC5E2o} */ -function getLoggerOptions() { +function getLoggerOptions () { // Only if the program is running in an interactive terminal if (process.stdout.isTTY) { return { - level: "info", + level: 'info', transport: { - target: "pino-pretty", + target: 'pino-pretty', options: { - translateTime: "HH:MM:ss Z", - ignore: "pid,hostname" + translateTime: 'HH:MM:ss Z', + ignore: 'pid,hostname' } } - }; + } } - return { level: process.env.LOG_LEVEL ?? "silent" }; + return { level: process.env.LOG_LEVEL ?? 'silent' } } const app = Fastify({ logger: getLoggerOptions(), ajv: { customOptions: { - coerceTypes: "array", // change type of data to match type keyword - removeAdditional: "all" // Remove additional body properties + coerceTypes: 'array', // change type of data to match type keyword + removeAdditional: 'all' // Remove additional body properties } } -}); +}) -async function init() { +async function init () { // Register your application as a normal plugin. // fp must be used to override default error handler - app.register(fp(serviceApp)); + app.register(fp(serviceApp)) // Delay is the number of milliseconds for the graceful close to finish closeWithGrace( { delay: process.env.FASTIFY_CLOSE_GRACE_DELAY ?? 500 }, async ({ err }) => { if (err != null) { - app.log.error(err); + app.log.error(err) } - await app.close(); + await app.close() } - ); + ) - await app.ready(); + await app.ready() try { // Start listening. - await app.listen({ port: process.env.PORT ?? 3000 }); + await app.listen({ port: process.env.PORT ?? 3000 }) } catch (err) { - app.log.error(err); - process.exit(1); + app.log.error(err) + process.exit(1) } } -init(); +init() diff --git a/test/app/cors.test.ts b/test/app/cors.test.ts index 9893ab8..024f7cd 100644 --- a/test/app/cors.test.ts +++ b/test/app/cors.test.ts @@ -1,20 +1,20 @@ -import { it } from "node:test"; -import { build } from "../helper.js"; -import assert from "node:assert"; +import { it } from 'node:test' +import { build } from '../helper.js' +import assert from 'node:assert' -it("should correctly handle CORS preflight requests", async (t) => { - const app = await build(t); +it('should correctly handle CORS preflight requests', async (t) => { + const app = await build(t) - const res = await app.inject({ - method: "OPTIONS", - url: "/", - headers: { - "Origin": "http://example.com", - "Access-Control-Request-Method": "GET", - "Access-Control-Request-Headers": "Content-Type" - } - }); + const res = await app.inject({ + method: 'OPTIONS', + url: '/', + headers: { + Origin: 'http://example.com', + 'Access-Control-Request-Method': 'GET', + 'Access-Control-Request-Headers': 'Content-Type' + } + }) - assert.strictEqual(res.statusCode, 204); - assert.strictEqual(res.headers['access-control-allow-methods'], 'GET, POST, PUT, DELETE'); -}); + assert.strictEqual(res.statusCode, 204) + assert.strictEqual(res.headers['access-control-allow-methods'], 'GET, POST, PUT, DELETE') +}) diff --git a/test/app/error-handler.test.ts b/test/app/error-handler.test.ts index deed749..df3c9c9 100644 --- a/test/app/error-handler.test.ts +++ b/test/app/error-handler.test.ts @@ -1,27 +1,27 @@ -import { it } from "node:test"; -import assert from "node:assert"; -import fastify from "fastify"; -import serviceApp from "../../src/app.ts"; -import fp from "fastify-plugin"; +import { it } from 'node:test' +import assert from 'node:assert' +import fastify from 'fastify' +import serviceApp from '../../src/app.ts' +import fp from 'fastify-plugin' -it("should call errorHandler", async (t) => { - const app = fastify(); - await app.register(fp(serviceApp)); +it('should call errorHandler', async (t) => { + const app = fastify() + await app.register(fp(serviceApp)) - app.get("/error", () => { - throw new Error("Kaboom!"); - }); + app.get('/error', () => { + throw new Error('Kaboom!') + }) - await app.ready(); + await app.ready() - t.after(() => app.close()); + t.after(() => app.close()) const res = await app.inject({ - method: "GET", - url: "/error" - }); + method: 'GET', + url: '/error' + }) assert.deepStrictEqual(JSON.parse(res.payload), { - message: "Internal Server Error" - }); -}); + message: 'Internal Server Error' + }) +}) diff --git a/test/app/not-found-handler.test.ts b/test/app/not-found-handler.test.ts index 4abfa12..497c5f5 100644 --- a/test/app/not-found-handler.test.ts +++ b/test/app/not-found-handler.test.ts @@ -1,35 +1,35 @@ -import { it } from "node:test"; -import assert from "node:assert"; -import { build } from "../helper.js"; +import { it } from 'node:test' +import assert from 'node:assert' +import { build } from '../helper.js' -it("should call notFoundHandler", async (t) => { - const app = await build(t); +it('should call notFoundHandler', async (t) => { + const app = await build(t) const res = await app.inject({ - method: "GET", - url: "/this-route-does-not-exist" - }); + method: 'GET', + url: '/this-route-does-not-exist' + }) - assert.strictEqual(res.statusCode, 404); - assert.deepStrictEqual(JSON.parse(res.payload), { message: "Not Found" }); -}); + assert.strictEqual(res.statusCode, 404) + assert.deepStrictEqual(JSON.parse(res.payload), { message: 'Not Found' }) +}) -it("should be rate limited", async (t) => { - const app = await build(t); +it('should be rate limited', async (t) => { + const app = await build(t) for (let i = 0; i < 3; i++) { const res = await app.inject({ - method: "GET", - url: "/this-route-does-not-exist" - }); - - assert.strictEqual(res.statusCode, 404); + method: 'GET', + url: '/this-route-does-not-exist' + }) + + assert.strictEqual(res.statusCode, 404) } const res = await app.inject({ - method: "GET", - url: "/this-route-does-not-exist" - }); + method: 'GET', + url: '/this-route-does-not-exist' + }) - assert.strictEqual(res.statusCode, 429); -}); + assert.strictEqual(res.statusCode, 429) +}) diff --git a/test/app/rate-limit.test.ts b/test/app/rate-limit.test.ts index 998fcd6..fd5fc41 100644 --- a/test/app/rate-limit.test.ts +++ b/test/app/rate-limit.test.ts @@ -1,23 +1,23 @@ -import { it } from "node:test"; -import { build } from "../helper.js"; -import assert from "node:assert"; +import { it } from 'node:test' +import { build } from '../helper.js' +import assert from 'node:assert' -it("should be rate limited", async (t) => { - const app = await build(t); - - for (let i = 0; i < 4; i++) { - const res = await app.inject({ - method: "GET", - url: "/" - }); +it('should be rate limited', async (t) => { + const app = await build(t) - assert.strictEqual(res.statusCode, 200); - } - + for (let i = 0; i < 4; i++) { const res = await app.inject({ - method: "GET", - url: "/" - }); - - assert.strictEqual(res.statusCode, 429); -}); + method: 'GET', + url: '/' + }) + + assert.strictEqual(res.statusCode, 200) + } + + const res = await app.inject({ + method: 'GET', + url: '/' + }) + + assert.strictEqual(res.statusCode, 429) +}) diff --git a/test/helper.ts b/test/helper.ts index dad5f98..bc935c5 100644 --- a/test/helper.ts +++ b/test/helper.ts @@ -1,73 +1,72 @@ +import { FastifyInstance, InjectOptions } from 'fastify' +import { build as buildApplication } from 'fastify-cli/helper.js' +import path from 'node:path' +import { TestContext } from 'node:test' +import { options as serverOptions } from '../src/app.js' -import { FastifyInstance, InjectOptions } from "fastify"; -import { build as buildApplication } from "fastify-cli/helper.js"; -import path from "node:path"; -import { TestContext } from "node:test"; -import { options as serverOptions } from "../src/app.js"; - -declare module "fastify" { +declare module 'fastify' { interface FastifyInstance { login: typeof login; injectWithLogin: typeof injectWithLogin } } -const AppPath = path.join(import.meta.dirname, "../src/app.ts"); +const AppPath = path.join(import.meta.dirname, '../src/app.ts') // Fill in this config with all the configurations // needed for testing the application -export function config() { +export function config () { return { - skipOverride: "true" // Register our application with fastify-plugin - }; + skipOverride: 'true' // Register our application with fastify-plugin + } } const tokens: Record = {} // We will create different users with different roles -async function login(this: FastifyInstance, username: string) { +async function login (this: FastifyInstance, username: string) { if (tokens[username]) { return tokens[username] } const res = await this.inject({ - method: "POST", - url: "/api/auth/login", + method: 'POST', + url: '/api/auth/login', payload: { username, - password: "password" + password: 'password' } - }); + }) - tokens[username] = JSON.parse(res.payload).token; + tokens[username] = JSON.parse(res.payload).token return tokens[username] } -async function injectWithLogin(this: FastifyInstance, username: string, opts: InjectOptions) { +async function injectWithLogin (this: FastifyInstance, username: string, opts: InjectOptions) { opts.headers = { ...opts.headers, Authorization: `Bearer ${await this.login(username)}` - }; + } - return this.inject(opts); + return this.inject(opts) }; // automatically build and tear down our instance -export async function build(t: TestContext) { +export async function build (t: TestContext) { // you can set all the options supported by the fastify CLI command - const argv = [AppPath]; + const argv = [AppPath] // fastify-plugin ensures that all decorators // are exposed for testing purposes, this is // different from the production setup - const app = await buildApplication(argv, config(), serverOptions) as FastifyInstance; + const app = await buildApplication(argv, config(), serverOptions) as FastifyInstance - app.login = login; + app.login = login app.injectWithLogin = injectWithLogin // close the app after we are done - t.after(() => app.close()); + t.after(() => app.close()) - return app; + return app } diff --git a/test/plugins/repository.test.ts b/test/plugins/repository.test.ts index 4244f63..9d534af 100644 --- a/test/plugins/repository.test.ts +++ b/test/plugins/repository.test.ts @@ -1,65 +1,65 @@ -import { test } from "tap"; -import assert from "node:assert"; -import { execSync } from "child_process"; -import Fastify from "fastify"; -import repository from "../../src/plugins/custom/repository.js"; -import * as envPlugin from "../../src/plugins/external/env.js"; -import * as mysqlPlugin from "../../src/plugins/external/mysql.js"; -import { Auth } from '../../src/schemas/auth.js'; +import { test } from 'tap' +import assert from 'node:assert' +import { execSync } from 'child_process' +import Fastify from 'fastify' +import repository from '../../src/plugins/custom/repository.js' +import * as envPlugin from '../../src/plugins/external/env.js' +import * as mysqlPlugin from '../../src/plugins/external/mysql.js' +import { Auth } from '../../src/schemas/auth.js' -test("repository works standalone", async (t) => { - const app = Fastify(); +test('repository works standalone', async (t) => { + const app = Fastify() t.after(() => { - app.close(); + app.close() // Run the seed script again to clean up after tests - execSync('npm run db:seed'); - }); + execSync('npm run db:seed') + }) - app.register(envPlugin.default, envPlugin.autoConfig); - app.register(mysqlPlugin.default, mysqlPlugin.autoConfig); - app.register(repository); + app.register(envPlugin.default, envPlugin.autoConfig) + app.register(mysqlPlugin.default, mysqlPlugin.autoConfig) + app.register(repository) - await app.ready(); + await app.ready() // Test find method - const user = await app.repository.find('users', { select: 'username', where: { username: 'basic' } }); - assert.deepStrictEqual(user, { username: 'basic' }); + const user = await app.repository.find('users', { select: 'username', where: { username: 'basic' } }) + assert.deepStrictEqual(user, { username: 'basic' }) - const firstUser = await app.repository.find('users', { select: 'username' }); - assert.deepStrictEqual(firstUser, { username: 'basic' }); + const firstUser = await app.repository.find('users', { select: 'username' }) + assert.deepStrictEqual(firstUser, { username: 'basic' }) - const nullUser = await app.repository.find('users', { select: 'username', where: { username: 'unknown' } }); - assert.equal(nullUser, null); + const nullUser = await app.repository.find('users', { select: 'username', where: { username: 'unknown' } }) + assert.equal(nullUser, null) // Test findMany method - const users = await app.repository.findMany('users', { select: 'username', where: { username: 'basic' } }); + const users = await app.repository.findMany('users', { select: 'username', where: { username: 'basic' } }) assert.deepStrictEqual(users, [ { username: 'basic' } - ]); + ]) // Test findMany method - const allUsers = await app.repository.findMany('users', { select: 'username' }); + const allUsers = await app.repository.findMany('users', { select: 'username' }) assert.deepStrictEqual(allUsers, [ { username: 'basic' }, { username: 'moderator' }, { username: 'admin' } - ]); + ]) // Test create method - const newUserId = await app.repository.create('users', { data: { username: 'new_user', password: 'new_password' } }); - const newUser = await app.repository.find('users', { select: 'username', where: { id: newUserId } }); - assert.deepStrictEqual(newUser, { username: 'new_user' }); + const newUserId = await app.repository.create('users', { data: { username: 'new_user', password: 'new_password' } }) + const newUser = await app.repository.find('users', { select: 'username', where: { id: newUserId } }) + assert.deepStrictEqual(newUser, { username: 'new_user' }) // Test update method - const updateCount = await app.repository.update('users', { data: { password: 'updated_password' }, where: { username: 'new_user' } }); - assert.equal(updateCount, 1); - const updatedUser = await app.repository.find('users', { select: 'password', where: { username: 'new_user' } }); - assert.deepStrictEqual(updatedUser, { password: 'updated_password' }); + const updateCount = await app.repository.update('users', { data: { password: 'updated_password' }, where: { username: 'new_user' } }) + assert.equal(updateCount, 1) + const updatedUser = await app.repository.find('users', { select: 'password', where: { username: 'new_user' } }) + assert.deepStrictEqual(updatedUser, { password: 'updated_password' }) // Test delete method - const deleteCount = await app.repository.delete('users', { username: 'new_user' }); - assert.equal(deleteCount, 1); - const deletedUser = await app.repository.find('users', { select: 'username', where: { username: 'new_user' } }); - assert.equal(deletedUser, null); -}); + const deleteCount = await app.repository.delete('users', { username: 'new_user' }) + assert.equal(deleteCount, 1) + const deletedUser = await app.repository.find('users', { select: 'username', where: { username: 'new_user' } }) + assert.equal(deletedUser, null) +}) diff --git a/test/plugins/scrypt.test.ts b/test/plugins/scrypt.test.ts index 2d2f6d6..ea513d8 100644 --- a/test/plugins/scrypt.test.ts +++ b/test/plugins/scrypt.test.ts @@ -1,28 +1,28 @@ -import { test } from "tap"; -import Fastify from "fastify"; -import scryptPlugin from "../../src/plugins/custom/scrypt.js"; +import { test } from 'tap' +import Fastify from 'fastify' +import scryptPlugin from '../../src/plugins/custom/scrypt.js' -test("scrypt works standalone", async t => { - const app = Fastify(); +test('scrypt works standalone', async t => { + const app = Fastify() - t.teardown(() => app.close()); + t.teardown(() => app.close()) - app.register(scryptPlugin); + app.register(scryptPlugin) - await app.ready(); + await app.ready() - const password = "test_password"; - const hash = await app.hash(password); - t.type(hash, 'string'); + const password = 'test_password' + const hash = await app.hash(password) + t.type(hash, 'string') - const isValid = await app.compare(password, hash); - t.ok(isValid, 'compare should return true for correct password'); + const isValid = await app.compare(password, hash) + t.ok(isValid, 'compare should return true for correct password') - const isInvalid = await app.compare("wrong_password", hash); - t.notOk(isInvalid, 'compare should return false for incorrect password'); + const isInvalid = await app.compare('wrong_password', hash) + t.notOk(isInvalid, 'compare should return false for incorrect password') await t.rejects( - () => app.compare(password, "malformed_hash"), + () => app.compare(password, 'malformed_hash'), 'compare should throw an error for malformed hash' - ); -}); + ) +}) diff --git a/test/routes/api/api.test.ts b/test/routes/api/api.test.ts index b41ac46..a4341d3 100644 --- a/test/routes/api/api.test.ts +++ b/test/routes/api/api.test.ts @@ -1,42 +1,42 @@ -import { test } from "node:test"; -import assert from "node:assert"; -import { build } from "../../helper.js"; +import { test } from 'node:test' +import assert from 'node:assert' +import { build } from '../../helper.js' -test("GET /api without authorization header", async (t) => { - const app = await build(t); +test('GET /api without authorization header', async (t) => { + const app = await build(t) const res = await app.inject({ - url: "/api" - }); + url: '/api' + }) assert.deepStrictEqual(JSON.parse(res.payload), { - message: "No Authorization was found in request.headers" - }); -}); + message: 'No Authorization was found in request.headers' + }) +}) -test("GET /api without JWT Token", async (t) => { - const app = await build(t); +test('GET /api without JWT Token', async (t) => { + const app = await build(t) const res = await app.inject({ - method: "GET", - url: "/api", + method: 'GET', + url: '/api', headers: { - Authorization: "Bearer invalidtoken" + Authorization: 'Bearer invalidtoken' } - }); + }) assert.deepStrictEqual(JSON.parse(res.payload), { - message: "Authorization token is invalid: The token is malformed." - }); -}); + message: 'Authorization token is invalid: The token is malformed.' + }) +}) -test("GET /api with JWT Token", async (t) => { - const app = await build(t); +test('GET /api with JWT Token', async (t) => { + const app = await build(t) - const res = await app.injectWithLogin("basic", { - url: "/api" - }); + const res = await app.injectWithLogin('basic', { + url: '/api' + }) - assert.equal(res.statusCode, 200); - assert.ok(JSON.parse(res.payload).message.startsWith("Hello basic!")); -}); + assert.equal(res.statusCode, 200) + assert.ok(JSON.parse(res.payload).message.startsWith('Hello basic!')) +}) diff --git a/test/routes/api/auth/auth.test.ts b/test/routes/api/auth/auth.test.ts index fbb2f72..5ae6652 100644 --- a/test/routes/api/auth/auth.test.ts +++ b/test/routes/api/auth/auth.test.ts @@ -1,62 +1,62 @@ -import { test } from "node:test"; -import assert from "node:assert"; -import { build } from "../../../helper.js"; +import { test } from 'node:test' +import assert from 'node:assert' +import { build } from '../../../helper.js' -test("POST /api/auth/login with valid credentials", async (t) => { - const app = await build(t); +test('POST /api/auth/login with valid credentials', async (t) => { + const app = await build(t) const res = await app.inject({ - method: "POST", - url: "/api/auth/login", + method: 'POST', + url: '/api/auth/login', payload: { - username: "basic", - password: "password" + username: 'basic', + password: 'password' } - }); + }) - assert.strictEqual(res.statusCode, 200); - assert.ok(JSON.parse(res.payload).token); -}); + assert.strictEqual(res.statusCode, 200) + assert.ok(JSON.parse(res.payload).token) +}) -test("POST /api/auth/login with invalid credentials", async (t) => { - const app = await build(t); +test('POST /api/auth/login with invalid credentials', async (t) => { + const app = await build(t) const testCases = [ { - username: "invalid_user", - password: "password", - description: "invalid username" + username: 'invalid_user', + password: 'password', + description: 'invalid username' }, { - username: "basic", - password: "wrong_password", - description: "invalid password" + username: 'basic', + password: 'wrong_password', + description: 'invalid password' }, { - username: "invalid_user", - password: "wrong_password", - description: "both invalid" + username: 'invalid_user', + password: 'wrong_password', + description: 'both invalid' } - ]; + ] for (const testCase of testCases) { const res = await app.inject({ - method: "POST", - url: "/api/auth/login", + method: 'POST', + url: '/api/auth/login', payload: { username: testCase.username, password: testCase.password } - }); + }) assert.strictEqual( res.statusCode, 401, `Failed for case: ${testCase.description}` - ); + ) assert.deepStrictEqual(JSON.parse(res.payload), { - message: "Invalid username or password." - }); + message: 'Invalid username or password.' + }) } -}); +}) diff --git a/test/routes/api/tasks/tasks.test.ts b/test/routes/api/tasks/tasks.test.ts index ae82067..d4d9987 100644 --- a/test/routes/api/tasks/tasks.test.ts +++ b/test/routes/api/tasks/tasks.test.ts @@ -1,248 +1,244 @@ -import { describe, it } from "node:test"; -import assert from "node:assert"; -import { build } from "../../../helper.js"; -import { Task, TaskStatus } from "../../../../src/schemas/tasks.js"; -import { FastifyInstance } from "fastify"; - - - - -async function createTask(app: FastifyInstance, taskData: Partial) { - return await app.repository.create("tasks", { data: taskData }); +import { describe, it } from 'node:test' +import assert from 'node:assert' +import { build } from '../../../helper.js' +import { Task, TaskStatus } from '../../../../src/schemas/tasks.js' +import { FastifyInstance } from 'fastify' + +async function createTask (app: FastifyInstance, taskData: Partial) { + return await app.repository.create('tasks', { data: taskData }) } describe('Tasks api (logged user only)', () => { describe('GET /api/tasks', () => { - it("should return a list of tasks", async (t) => { - const app = await build(t); - + it('should return a list of tasks', async (t) => { + const app = await build(t) + const taskData = { - name: "New Task", + name: 'New Task', author_id: 1, status: TaskStatus.New - }; - - const newTaskId = await app.repository.create("tasks", { data: taskData }); - - const res = await app.injectWithLogin("basic", { - method: "GET", - url: "/api/tasks" - }); - - assert.strictEqual(res.statusCode, 200); - const tasks = JSON.parse(res.payload) as Task[]; - const createdTask = tasks.find((task) => task.id === newTaskId); - assert.ok(createdTask, "Created task should be in the response"); - - assert.deepStrictEqual(taskData.name, createdTask.name); - assert.strictEqual(taskData.author_id, createdTask.author_id); - assert.strictEqual(taskData.status, createdTask.status); - }); + } + + const newTaskId = await app.repository.create('tasks', { data: taskData }) + + const res = await app.injectWithLogin('basic', { + method: 'GET', + url: '/api/tasks' + }) + + assert.strictEqual(res.statusCode, 200) + const tasks = JSON.parse(res.payload) as Task[] + const createdTask = tasks.find((task) => task.id === newTaskId) + assert.ok(createdTask, 'Created task should be in the response') + + assert.deepStrictEqual(taskData.name, createdTask.name) + assert.strictEqual(taskData.author_id, createdTask.author_id) + assert.strictEqual(taskData.status, createdTask.status) + }) }) describe('GET /api/tasks/:id', () => { - it("should return a task", async (t) => { - const app = await build(t); - + it('should return a task', async (t) => { + const app = await build(t) + const taskData = { - name: "Single Task", + name: 'Single Task', author_id: 1, status: TaskStatus.New - }; - - const newTaskId = await createTask(app, taskData); - - const res = await app.injectWithLogin("basic", { - method: "GET", + } + + const newTaskId = await createTask(app, taskData) + + const res = await app.injectWithLogin('basic', { + method: 'GET', url: `/api/tasks/${newTaskId}` - }); - - assert.strictEqual(res.statusCode, 200); - const task = JSON.parse(res.payload) as Task; - assert.equal(task.id, newTaskId); - }); - - it("should return 404 if task is not found", async (t) => { - const app = await build(t); - - const res = await app.injectWithLogin("basic", { - method: "GET", - url: "/api/tasks/9999" - }); - - assert.strictEqual(res.statusCode, 404); - const payload = JSON.parse(res.payload); - assert.strictEqual(payload.message, "Task not found"); - }); - }); + }) + + assert.strictEqual(res.statusCode, 200) + const task = JSON.parse(res.payload) as Task + assert.equal(task.id, newTaskId) + }) + + it('should return 404 if task is not found', async (t) => { + const app = await build(t) + + const res = await app.injectWithLogin('basic', { + method: 'GET', + url: '/api/tasks/9999' + }) + + assert.strictEqual(res.statusCode, 404) + const payload = JSON.parse(res.payload) + assert.strictEqual(payload.message, 'Task not found') + }) + }) describe('POST /api/tasks', () => { - it("should create a new task", async (t) => { - const app = await build(t); + it('should create a new task', async (t) => { + const app = await build(t) const taskData = { - name: "New Task", + name: 'New Task', author_id: 1 - }; + } - const res = await app.injectWithLogin("basic", { - method: "POST", - url: "/api/tasks", + const res = await app.injectWithLogin('basic', { + method: 'POST', + url: '/api/tasks', payload: taskData - }); + }) - assert.strictEqual(res.statusCode, 201); - const { id } = JSON.parse(res.payload); + assert.strictEqual(res.statusCode, 201) + const { id } = JSON.parse(res.payload) - const createdTask = await app.repository.find("tasks", { select: 'name', where: { id } }) as Task; - assert.equal(createdTask.name, taskData.name); - }); - }); + const createdTask = await app.repository.find('tasks', { select: 'name', where: { id } }) as Task + assert.equal(createdTask.name, taskData.name) + }) + }) describe('PATCH /api/tasks/:id', () => { - it("should update an existing task", async (t) => { - const app = await build(t); + it('should update an existing task', async (t) => { + const app = await build(t) const taskData = { - name: "Task to Update", + name: 'Task to Update', author_id: 1, status: TaskStatus.New - }; - const newTaskId = await createTask(app, taskData); + } + const newTaskId = await createTask(app, taskData) const updatedData = { - name: "Updated Task" - }; + name: 'Updated Task' + } - const res = await app.injectWithLogin("basic", { - method: "PATCH", + const res = await app.injectWithLogin('basic', { + method: 'PATCH', url: `/api/tasks/${newTaskId}`, payload: updatedData - }); + }) - assert.strictEqual(res.statusCode, 200); - const updatedTask = await app.repository.find("tasks", { where: { id: newTaskId } }) as Task; - assert.equal(updatedTask.name, updatedData.name); - }); + assert.strictEqual(res.statusCode, 200) + const updatedTask = await app.repository.find('tasks', { where: { id: newTaskId } }) as Task + assert.equal(updatedTask.name, updatedData.name) + }) - it("should return 404 if task is not found for update", async (t) => { - const app = await build(t); + it('should return 404 if task is not found for update', async (t) => { + const app = await build(t) const updatedData = { - name: "Updated Task" - }; + name: 'Updated Task' + } - const res = await app.injectWithLogin("basic", { - method: "PATCH", - url: "/api/tasks/9999", + const res = await app.injectWithLogin('basic', { + method: 'PATCH', + url: '/api/tasks/9999', payload: updatedData - }); + }) - assert.strictEqual(res.statusCode, 404); - const payload = JSON.parse(res.payload); - assert.strictEqual(payload.message, "Task not found"); - }); - }); + assert.strictEqual(res.statusCode, 404) + const payload = JSON.parse(res.payload) + assert.strictEqual(payload.message, 'Task not found') + }) + }) describe('DELETE /api/tasks/:id', () => { - it("should delete an existing task", async (t) => { - const app = await build(t); + it('should delete an existing task', async (t) => { + const app = await build(t) const taskData = { - name: "Task to Delete", + name: 'Task to Delete', author_id: 1, status: TaskStatus.New - }; - const newTaskId = await createTask(app, taskData); + } + const newTaskId = await createTask(app, taskData) - const res = await app.injectWithLogin("basic", { - method: "DELETE", + const res = await app.injectWithLogin('basic', { + method: 'DELETE', url: `/api/tasks/${newTaskId}` - }); + }) - assert.strictEqual(res.statusCode, 204); + assert.strictEqual(res.statusCode, 204) - const deletedTask = await app.repository.find("tasks", { where: { id: newTaskId } }); - assert.strictEqual(deletedTask, null); - }); + const deletedTask = await app.repository.find('tasks', { where: { id: newTaskId } }) + assert.strictEqual(deletedTask, null) + }) - it("should return 404 if task is not found for deletion", async (t) => { - const app = await build(t); + it('should return 404 if task is not found for deletion', async (t) => { + const app = await build(t) - const res = await app.injectWithLogin("basic", { - method: "DELETE", - url: "/api/tasks/9999" - }); + const res = await app.injectWithLogin('basic', { + method: 'DELETE', + url: '/api/tasks/9999' + }) - assert.strictEqual(res.statusCode, 404); - const payload = JSON.parse(res.payload); - assert.strictEqual(payload.message, "Task not found"); - }); - }); + assert.strictEqual(res.statusCode, 404) + const payload = JSON.parse(res.payload) + assert.strictEqual(payload.message, 'Task not found') + }) + }) describe('POST /api/tasks/:id/assign', () => { + it('should assign a task to a user and persist the changes', async (t) => { + const app = await build(t) - it("should assign a task to a user and persist the changes", async (t) => { - const app = await build(t); - const taskData = { - name: "Task to Assign", + name: 'Task to Assign', author_id: 1, status: TaskStatus.New - }; - const newTaskId = await createTask(app, taskData); - - const res = await app.injectWithLogin("basic", { - method: "POST", + } + const newTaskId = await createTask(app, taskData) + + const res = await app.injectWithLogin('basic', { + method: 'POST', url: `/api/tasks/${newTaskId}/assign`, payload: { userId: 2 } - }); - - assert.strictEqual(res.statusCode, 200); - - const updatedTask = await app.repository.find("tasks", { where: { id: newTaskId } }) as Task - assert.strictEqual(updatedTask.assigned_user_id, 2); - }); - - it("should unassign a task from a user and persist the changes", async (t) => { - const app = await build(t); - + }) + + assert.strictEqual(res.statusCode, 200) + + const updatedTask = await app.repository.find('tasks', { where: { id: newTaskId } }) as Task + assert.strictEqual(updatedTask.assigned_user_id, 2) + }) + + it('should unassign a task from a user and persist the changes', async (t) => { + const app = await build(t) + const taskData = { - name: "Task to Unassign", + name: 'Task to Unassign', author_id: 1, assigned_user_id: 2, status: TaskStatus.New - }; - const newTaskId = await createTask(app, taskData); - - const res = await app.injectWithLogin("basic", { - method: "POST", + } + const newTaskId = await createTask(app, taskData) + + const res = await app.injectWithLogin('basic', { + method: 'POST', url: `/api/tasks/${newTaskId}/assign`, payload: {} - }); - - assert.strictEqual(res.statusCode, 200); - - const updatedTask = await app.repository.find("tasks", { where: { id: newTaskId } }) as Task; - assert.strictEqual(updatedTask.assigned_user_id, null); - }); - - it("should return 404 if task is not found", async (t) => { - const app = await build(t); - - const res = await app.injectWithLogin("basic", { - method: "POST", - url: "/api/tasks/9999/assign", + }) + + assert.strictEqual(res.statusCode, 200) + + const updatedTask = await app.repository.find('tasks', { where: { id: newTaskId } }) as Task + assert.strictEqual(updatedTask.assigned_user_id, null) + }) + + it('should return 404 if task is not found', async (t) => { + const app = await build(t) + + const res = await app.injectWithLogin('basic', { + method: 'POST', + url: '/api/tasks/9999/assign', payload: { userId: 2 } - }); - - assert.strictEqual(res.statusCode, 404); - const payload = JSON.parse(res.payload); - assert.strictEqual(payload.message, "Task not found"); - }); - }); + }) + + assert.strictEqual(res.statusCode, 404) + const payload = JSON.parse(res.payload) + assert.strictEqual(payload.message, 'Task not found') + }) + }) }) diff --git a/test/routes/home.test.ts b/test/routes/home.test.ts index 6069230..41fc3a1 100644 --- a/test/routes/home.test.ts +++ b/test/routes/home.test.ts @@ -1,14 +1,14 @@ -import { test } from "node:test"; -import assert from "node:assert"; -import { build } from "../helper.js"; +import { test } from 'node:test' +import assert from 'node:assert' +import { build } from '../helper.js' -test("GET /", async (t) => { - const app = await build(t); +test('GET /', async (t) => { + const app = await build(t) const res = await app.inject({ - url: "/" - }); + url: '/' + }) assert.deepStrictEqual(JSON.parse(res.payload), { - message: "Welcome to the official fastify demo!" - }); -}); + message: 'Welcome to the official fastify demo!' + }) +})