templating api
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
# API service Tests
|
||||
|
||||
## Implementation
|
||||
|
||||
Tests are written in [vitest](https://vitest.dev/guide/)
|
||||
|
||||
Test initialization goes like this:
|
||||
|
||||
1. `api/vitest.config.mts` loads the ts config and finds the appropriate setup functions.
|
||||
|
||||
2. Before running the tests, it runs the `globalSetup` function from `api/tests/global-setup.ts`. This does things like setting up the database and running migrations and base seeds.
|
||||
|
||||
3. Next it loads a specific test file triggers the `setupFiles` files, currently only `api/tests/setup.ts`. These setup files add callbacks that will run before/after _each test file_ runs, so they should be performant. Mostly cleanup functions.
|
||||
|
||||
4. It runs the actual tests in the loaded file.
|
||||
|
||||
5. (Currently) Runs `beforeEach` callback that cleans the database before each test file is run.
|
||||
|
||||
6. Runs the next test file, and repeats from step 3.
|
||||
|
||||
## General Notes About Tests
|
||||
|
||||
1. Tests should map to a specific file in the api/src folder.
|
||||
|
||||
e.g.
|
||||
|
||||
- `api/src/models/funding-submission-line-json.ts` maps to `api/tests/models/funding-submission-line-json.test.ts`
|
||||
- `api/src/services/centre-services.ts` maps to `api/tests/services/centre-services.test.ts`
|
||||
|
||||
2. Tests should follow the naming convention `{filename}.test.{extension}`.
|
||||
3. Test file location should be moved if a given file is moved, and deleted if the file under test is deleted.
|
||||
4. A good general pattern for a test is
|
||||
|
||||
```typescript
|
||||
describe("api/src/services/centre-services.ts", () => { // references file under test
|
||||
describe("CentreServices", () => { // references class or model under test
|
||||
describe(".create", () => { // referneces a specific method on the class or model
|
||||
test("creates a new centre in the database", async () => { // descriptive message about the specific behaviour under test
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
5. I'm using a plugin that lets me switch between the test and non-test file, and creates the test file if it does not exist. It's not great, but it mostly works. See <https://marketplace.visualstudio.com/items?itemName=klondikemarlen.create-test-file>
|
||||
|
||||
It requires this config (in your workspace or `.vscode/settings.json`).
|
||||
|
||||
> Note that if this is in your worspace config must be inside the "settings" entry. i.e. `{ "settings": { // these settings } }`.
|
||||
|
||||
```json
|
||||
{
|
||||
"createTestFile.nameTemplate": "{filename}.test.{extension}",
|
||||
"createTestFile.languages": {
|
||||
"[vue]": {
|
||||
"createTestFile.nameTemplate": "{filename}.test.{extension}.ts"
|
||||
}
|
||||
},
|
||||
"createTestFile.pathMaps": [
|
||||
{
|
||||
// Other examples
|
||||
// "pathPattern": "/?(.*)",
|
||||
// "testFilePathPattern": "spec/$1"
|
||||
"pathPattern": "(api)/src/?(.*)",
|
||||
"testFilePathPattern": "$1/tests/$2"
|
||||
},
|
||||
{
|
||||
"pathPattern": "(web)/src/?(.*)",
|
||||
"testFilePathPattern": "$1/tests/$2"
|
||||
}
|
||||
],
|
||||
"createTestFile.isTestFileMatchers": [
|
||||
"^(?:test|spec)s?/",
|
||||
"/(?:test|spec)s?/",
|
||||
"/?(?:test|spec)s?_",
|
||||
"/?_(?:test|spec)s?",
|
||||
"/?\\.(?:test|spec)s?",
|
||||
"/?(?:test|spec)s?\\."
|
||||
]
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,25 @@
|
||||
import { User } from "@/models"
|
||||
|
||||
import { userFactory } from "@/factories"
|
||||
import { mockCurrentUser, request } from "@/support"
|
||||
|
||||
describe("api/src/controllers/current-user-controller.ts", () => {
|
||||
describe("CurrentUserController", () => {
|
||||
describe("#show", () => {
|
||||
test("it returns the policy alongside the user", async () => {
|
||||
// Arrange
|
||||
const currentUser = await userFactory.create({
|
||||
roles: [User.Roles.SYSTEM_ADMIN],
|
||||
})
|
||||
mockCurrentUser(currentUser)
|
||||
|
||||
// Act
|
||||
const response = await request().get("/api/current-user")
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(200)
|
||||
expect(response.body.policy).toBeDefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,32 @@
|
||||
import { User } from "@/models"
|
||||
|
||||
import { userFactory } from "@/factories"
|
||||
import { mockCurrentUser, request } from "@/support"
|
||||
|
||||
describe("api/src/controllers/users-controller.ts", () => {
|
||||
beforeEach(async () => {
|
||||
const currentUser = await userFactory.create({
|
||||
roles: [User.Roles.SYSTEM_ADMIN],
|
||||
})
|
||||
mockCurrentUser(currentUser)
|
||||
})
|
||||
|
||||
describe("UsersController", () => {
|
||||
describe("#create", () => {
|
||||
test("when creating a new user as a system admin, it creates the user", async () => {
|
||||
// Arrange
|
||||
const attributes = {
|
||||
email: "test_create@example.com",
|
||||
auth0Subject: "test_create@example.com",
|
||||
}
|
||||
|
||||
// Act
|
||||
const response = await request().post("/api/users").send(attributes)
|
||||
|
||||
// Assert
|
||||
expect(response.status).toBe(201)
|
||||
expect(response.body.user.email).toEqual(attributes.email)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
import { BuildOptions, DeepPartial, Factory } from "fishery"
|
||||
import { Model } from "@sequelize/core"
|
||||
|
||||
// Must keep type signature in sync with fishery's Factory type
|
||||
// See api/node_modules/fishery/dist/factory.d.ts
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export class BaseFactory<T extends Model, I = any, C = T> extends Factory<T, I, C> {
|
||||
// See https://thoughtbot.github.io/factory_bot/ref/build-strategies.html#attributes_for
|
||||
attributesFor(params?: DeepPartial<T>, options?: BuildOptions<T, I>): T {
|
||||
const model = this.build(params, options)
|
||||
return model.dataValues
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseFactory
|
||||
@@ -0,0 +1 @@
|
||||
export { nestedSaveAndAssociateIfNew } from "./nested-save-and-associate-if-new"
|
||||
@@ -0,0 +1,162 @@
|
||||
import { Model } from "@sequelize/core"
|
||||
|
||||
/**
|
||||
* Recursively saves a Sequelize model instance along with its associated models if they are new (unsaved).
|
||||
*
|
||||
* It handles `BelongsTo` associations and ensures associated model is saved _before_ the current model,
|
||||
* then propagates the foreign key relationship before saving the current model.
|
||||
*
|
||||
* It handles `HasMany` associations and ensures associated models are saved _after_ the current model,
|
||||
* propagating the foreign key relationship before saving.
|
||||
*
|
||||
* It handles `HasOne` associations and ensures the associated model is saved _after_ the current model,
|
||||
* propagating the foreign key relationship before saving.
|
||||
*
|
||||
* @template M - A Sequelize model type extending `Model`.
|
||||
* @param {M} modelInstance - The Sequelize model instance to be saved along with its associated models.
|
||||
* @returns {Promise<M>} - Returns the saved model instance.
|
||||
*
|
||||
* @example
|
||||
* // Given a non-persisted TravelSegment model instance, being passed via a HasMany association to a TravelAuthorization model.
|
||||
* // The TravelAuthorization is saved first, then the TravelSegment is updated with the travelAuthorizationId before it is saved.
|
||||
*
|
||||
* const travelSegment = travelSegmentFactory.build({
|
||||
* departureOn: faker.date.past(),
|
||||
* })
|
||||
* const travelAuthorization = await travelAuthorizationFactory
|
||||
* .associations({
|
||||
* travelSegments: [travelSegment],
|
||||
* })
|
||||
* .transient({
|
||||
* include: ["user", "travelSegments"],
|
||||
* })
|
||||
* .create({
|
||||
* status: TravelAuthorization.Statuses.SUBMITTED,
|
||||
* })
|
||||
*
|
||||
* @example
|
||||
* // Given a persisted TravelAuthorization passed to a TravelSegment model instance via a BelongsTo association.
|
||||
* // The TravelSegment is updated with the travelAuthorizationId before it is saved.
|
||||
* const travelAuthorization = await travelAuthorizationFactory
|
||||
* .transient({ roundTrip: true })
|
||||
* .create()
|
||||
* const whitehorse = await locationFactory.create({ city: "Whitehorse", province: "YT" })
|
||||
* const vancouver = await locationFactory.create({ city: "Vancouver", province: "BC" })
|
||||
* const travelSegment1 = await travelSegmentFactory
|
||||
* .associations({
|
||||
* travelAuthorization,
|
||||
* departureLocation: whitehorse,
|
||||
* arrivalLocation: vancouver,
|
||||
* })
|
||||
* .create({
|
||||
* segmentNumber: 1,
|
||||
* departureOn: new Date("2022-06-05"),
|
||||
* departureTime: Stop.BEGINNING_OF_DAY,
|
||||
* modeOfTransport: Stop.TravelMethods.AIRCRAFT,
|
||||
* accommodationType: Stop.AccommodationTypes.HOTEL,
|
||||
* })
|
||||
*
|
||||
* @example
|
||||
* // Given a generic TravelAuthorization creation.
|
||||
* // The factory will create the purpose and user associations if they are not provided.
|
||||
* // Then the onCreated hook will call nestedSaveAndAssociateIfNew to save
|
||||
* // the purpose and user associations as they are new.
|
||||
* // Then those associations will propagate their foreign keys to the travelAuthorization before it is saved.
|
||||
* const travelAuthorization = await travelAuthorizationFactory.create()
|
||||
*/
|
||||
export async function nestedSaveAndAssociateIfNew<M extends Model>(modelInstance: M): Promise<M> {
|
||||
const modelClass = modelInstance.constructor as typeof Model
|
||||
const associations = modelClass.associations
|
||||
|
||||
const belongsToAssociationNames: string[] = []
|
||||
const hasManyAssociationNames: string[] = []
|
||||
const hasOneAssociationNames: string[] = []
|
||||
|
||||
for (const [associationName, { associationType }] of Object.entries(associations)) {
|
||||
if (associationType === "BelongsTo") {
|
||||
belongsToAssociationNames.push(associationName)
|
||||
} else if (associationType === "HasMany") {
|
||||
hasManyAssociationNames.push(associationName)
|
||||
} else if (associationType === "HasOne") {
|
||||
hasOneAssociationNames.push(associationName)
|
||||
} else {
|
||||
// no-op - maybe I should warn?
|
||||
}
|
||||
}
|
||||
|
||||
for (const associationName of belongsToAssociationNames) {
|
||||
const associationDefinition = associations[associationName]
|
||||
const foreignKeyName = associationDefinition.foreignKey
|
||||
let foreignKeyValue = modelInstance.get(foreignKeyName)
|
||||
if (foreignKeyValue !== undefined) continue
|
||||
|
||||
// Maybe should be associationAccessor instead of as?
|
||||
const associationAlias = associationDefinition.as as keyof M
|
||||
const associatedInstance = modelInstance[associationAlias] as Model | undefined
|
||||
if (associatedInstance === undefined) continue
|
||||
|
||||
// @ts-expect-error - TS doesn't know that targetKey is a property of associationDefinition
|
||||
const { targetKey }: { targetKey: keyof typeof associatedInstance } = associationDefinition
|
||||
if (associatedInstance.isNewRecord !== true) {
|
||||
foreignKeyValue = associatedInstance.get(targetKey)
|
||||
modelInstance.set(foreignKeyName, foreignKeyValue)
|
||||
continue
|
||||
}
|
||||
|
||||
const updatedAssocationInstance = await nestedSaveAndAssociateIfNew(associatedInstance)
|
||||
foreignKeyValue = updatedAssocationInstance.get(targetKey)
|
||||
modelInstance.set(foreignKeyName, foreignKeyValue)
|
||||
}
|
||||
|
||||
if (modelInstance.isNewRecord === true) {
|
||||
await modelInstance.save()
|
||||
}
|
||||
|
||||
for (const associationName of hasManyAssociationNames) {
|
||||
const associationDefinition = associations[associationName]
|
||||
|
||||
// Maybe should be associationAccessor instead of as?
|
||||
const associationAlias = associationDefinition.as as keyof M
|
||||
const associatedInstances = modelInstance[associationAlias] as Model[] | undefined
|
||||
if (associatedInstances === undefined) continue
|
||||
|
||||
for (const associatedInstance of associatedInstances) {
|
||||
if (associatedInstance.isNewRecord !== true) continue
|
||||
|
||||
// @ts-expect-error - TS doesn't know that sourceKey is a property of associationDefinition
|
||||
const {
|
||||
sourceKey,
|
||||
foreignKey,
|
||||
}: { sourceKey: keyof M; foreignKey: keyof typeof associatedInstance } = associationDefinition
|
||||
const sourceKeyValue = modelInstance.get(sourceKey)
|
||||
|
||||
associatedInstance.set(foreignKey, sourceKeyValue)
|
||||
await nestedSaveAndAssociateIfNew(associatedInstance)
|
||||
}
|
||||
}
|
||||
|
||||
for (const associationName of hasOneAssociationNames) {
|
||||
const associationDefinition = associations[associationName]
|
||||
|
||||
// Maybe should be associationAccessor instead of as?
|
||||
const associationAlias = associationDefinition.as as keyof M
|
||||
const associatedInstance = modelInstance[associationAlias] as Model | undefined
|
||||
if (associatedInstance === undefined) continue
|
||||
|
||||
if (associatedInstance.isNewRecord !== true) continue
|
||||
|
||||
// @ts-expect-error - TS doesn't know that sourceKey is a property of associationDefinition
|
||||
const {
|
||||
sourceKey,
|
||||
foreignKey,
|
||||
}: { sourceKey: keyof M; foreignKey: keyof typeof associatedInstance } = associationDefinition
|
||||
const sourceKeyValue = modelInstance.get(sourceKey)
|
||||
|
||||
associatedInstance.set(foreignKey, sourceKeyValue)
|
||||
await nestedSaveAndAssociateIfNew(associatedInstance)
|
||||
}
|
||||
|
||||
return modelInstance
|
||||
}
|
||||
|
||||
export default nestedSaveAndAssociateIfNew
|
||||
@@ -0,0 +1,2 @@
|
||||
// Factories
|
||||
export { userFactory } from "./user-factory"
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Factory } from "fishery"
|
||||
import { faker } from "@faker-js/faker/locale/en_CA"
|
||||
|
||||
import { User } from "@/models"
|
||||
|
||||
export const userFactory = Factory.define<User>(({ sequence, params, onCreate }) => {
|
||||
onCreate((user) => {
|
||||
try {
|
||||
return user.save()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
throw new Error(
|
||||
`Could not create User with attributes: ${JSON.stringify(user.dataValues, null, 2)}`
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const firstName = params.firstName || `${faker.person.firstName()}-${sequence}`
|
||||
const lastName = params.lastName || faker.person.lastName()
|
||||
const email = params.email || faker.internet.email({ firstName, lastName })
|
||||
|
||||
return User.build({
|
||||
email,
|
||||
auth0Subject: params.auth0Subject || email,
|
||||
firstName,
|
||||
lastName,
|
||||
displayName: `${firstName} ${lastName}`,
|
||||
roles: [User.Roles.USER],
|
||||
})
|
||||
})
|
||||
|
||||
export default userFactory
|
||||
@@ -0,0 +1,11 @@
|
||||
import { execSync } from 'child_process';
|
||||
|
||||
export default async function globalSetup() {
|
||||
try {
|
||||
// Keep in sync with api/bin/boot-app.sh
|
||||
execSync(`npm run initializers`, { stdio: 'inherit' });
|
||||
} catch (error) {
|
||||
console.error('Failed to run importAndExecuteInitializers:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { userFactory } from "@/factories"
|
||||
|
||||
import { UserRoles } from "@/models/user"
|
||||
|
||||
describe("api/src/models/user.ts", () => {
|
||||
describe("User", () => {
|
||||
describe("#isSystemAdmin", () => {
|
||||
test("when user has system_admin role, it returns true", async () => {
|
||||
// Arrange
|
||||
const user = await userFactory.create({ roles: [UserRoles.SYSTEM_ADMIN] })
|
||||
|
||||
// Act & Assert
|
||||
expect(user.isSystemAdmin).toBe(true)
|
||||
})
|
||||
|
||||
test("when user does not have system_admin role, it returns false", async () => {
|
||||
// Arrange
|
||||
const user = await userFactory.create({ roles: [UserRoles.USER] })
|
||||
|
||||
// Act & Assert
|
||||
expect(user.isSystemAdmin).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* See https://vitest.dev/config/#setupfiles
|
||||
*
|
||||
* Run some code before each test file.
|
||||
*
|
||||
* WARNING: Be very careful of imports in this file!!!
|
||||
* Vitest will not mock modules that were imported inside a setup file because they are
|
||||
* cached by the time a test file is running.
|
||||
* You can do
|
||||
* ```ts
|
||||
* vi.hoisted(() => {
|
||||
* vi.resetModules()
|
||||
* })
|
||||
* ```
|
||||
* to clear all module caches before running a test file.
|
||||
* See: https://vitest.dev/api/vi#vi-mock
|
||||
*/
|
||||
|
||||
// Global Mocks
|
||||
import cleanDatabase from "@/support/clean-database"
|
||||
import mockedAxios from "@/support/mock-axios"
|
||||
|
||||
beforeEach(async () => {
|
||||
await cleanDatabase()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
mockedAxios.reset()
|
||||
})
|
||||
@@ -0,0 +1,57 @@
|
||||
import { QueryTypes } from "@sequelize/core"
|
||||
|
||||
import { isNil } from "lodash"
|
||||
|
||||
import db from "@/db/db-client"
|
||||
|
||||
async function getTableNames() {
|
||||
const query = /* sql */ `
|
||||
SELECT
|
||||
table_name as "tableName"
|
||||
FROM
|
||||
information_schema.tables
|
||||
WHERE
|
||||
table_schema = 'public'
|
||||
AND table_type = 'BASE TABLE'
|
||||
AND table_name != 'SequelizeMeta'
|
||||
AND table_name != 'knex_migrations'
|
||||
AND table_name != 'knex_migrations_lock';
|
||||
`
|
||||
|
||||
try {
|
||||
const result = await db.query<{ tableName: string }>(query, { type: QueryTypes.SELECT })
|
||||
const tableNames = result.map((row) => row.tableName)
|
||||
return tableNames
|
||||
} catch (error) {
|
||||
console.error("Error fetching table names:", error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async function buildCleanDatabaseQuery() {
|
||||
const tableNames = await getTableNames()
|
||||
const quotedTableNames = tableNames.map((name) => `"${name}"`)
|
||||
return /* sql */ `
|
||||
TRUNCATE TABLE ${quotedTableNames.join(",\n ")} RESTART IDENTITY CASCADE;
|
||||
`
|
||||
}
|
||||
|
||||
let cleanDatabaseQuery: string | null = null
|
||||
|
||||
export async function cleanDatabase() {
|
||||
if (isNil(cleanDatabaseQuery)) {
|
||||
cleanDatabaseQuery = await buildCleanDatabaseQuery()
|
||||
}
|
||||
|
||||
try {
|
||||
// TODO: once all tables are in Sequelize models, use this instead:
|
||||
// await db.truncate({ cascade: true, restartIdentity: true })
|
||||
await db.query(cleanDatabaseQuery, { raw: true })
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export default cleanDatabase
|
||||
@@ -0,0 +1,6 @@
|
||||
export { cleanDatabase } from "./clean-database"
|
||||
export { loadTestData } from "./load-test-data"
|
||||
export { mockCurrentUser } from "./mock-current-user"
|
||||
export { mockedAxios } from "./mock-axios"
|
||||
export { request } from "./request"
|
||||
export { testWithCustomLogLevel } from "./test-with-custom-log-level"
|
||||
@@ -0,0 +1,39 @@
|
||||
import path from "path"
|
||||
import { readFileSync } from "fs"
|
||||
|
||||
import { APP_ROOT_PATH } from "@/config"
|
||||
import arrayWrap from "@/utils/array-wrap"
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
* - `testDataPath('path/to/my-file.json')`
|
||||
* - `testDataPath('path', 'to', 'my-file.json')`
|
||||
*/
|
||||
export function testDataPath(pathOrPathSegment: string | string[]): string {
|
||||
const pathSegments = arrayWrap(pathOrPathSegment)
|
||||
return path.join(APP_ROOT_PATH, "tests", "data", ...pathSegments)
|
||||
}
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
* - `specData('path/to/my-file.json')`
|
||||
* - `specData('path', 'to', 'my-file.json')`
|
||||
*
|
||||
* Returns:
|
||||
* - JSON parsed object if file is a JSON file
|
||||
* - Raw file content otherwise
|
||||
*/
|
||||
export function loadTestData(pathOrPathSegment: string | string[]): string | object {
|
||||
const filePath = testDataPath(pathOrPathSegment)
|
||||
const rawData = readFileSync(filePath)
|
||||
|
||||
const extension = path.extname(filePath)
|
||||
switch (extension) {
|
||||
case ".json":
|
||||
return JSON.parse(rawData.toString())
|
||||
default:
|
||||
return rawData.toString()
|
||||
}
|
||||
}
|
||||
|
||||
export default loadTestData
|
||||
@@ -0,0 +1,6 @@
|
||||
import axios from "axios"
|
||||
import MockAdapter from "axios-mock-adapter"
|
||||
|
||||
export const mockedAxios = new MockAdapter(axios)
|
||||
|
||||
export default mockedAxios
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Request, Response, NextFunction } from "express"
|
||||
|
||||
import {
|
||||
findAndAuthorizeCurrentUserMiddleware,
|
||||
type AuthorizationRequest,
|
||||
} from "@/middlewares/find-and-authorize-current-user-middleware"
|
||||
|
||||
import { User } from "@/models"
|
||||
|
||||
/**
|
||||
* Usage:
|
||||
* At the top level of a test file import:
|
||||
* import { mockCurrentUser } from "@/support"
|
||||
*
|
||||
* Then where you want to set the current user:
|
||||
* mockCurrentUser(currentUser)
|
||||
*
|
||||
* @param newCurrentUser - The user to set as the current user
|
||||
*/
|
||||
export function mockCurrentUser(newCurrentUser: User) {
|
||||
vi.mock("@/middlewares/jwt-middleware", () => ({
|
||||
default: async (_req: Request, _res: Response, next: NextFunction) => next(),
|
||||
jwtMiddleware: async (_req: Request, _res: Response, next: NextFunction) => next(),
|
||||
}))
|
||||
|
||||
vi.mock("@/middlewares/find-and-authorize-current-user-middleware")
|
||||
const findAndAuthorizeCurrentUserMiddlewareMock = vi.mocked(findAndAuthorizeCurrentUserMiddleware)
|
||||
findAndAuthorizeCurrentUserMiddlewareMock.mockImplementation(
|
||||
async (req: AuthorizationRequest, _res: Response, next: NextFunction) => {
|
||||
const currentUser = await User.withScope(["asCurrentUser"]).findByPk(newCurrentUser.id, {
|
||||
rejectOnEmpty: true,
|
||||
})
|
||||
|
||||
req.currentUser = currentUser
|
||||
next()
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import supertest, { AgentOptions } from "supertest"
|
||||
import { App } from "supertest/types"
|
||||
|
||||
import defaultApp from "@/app"
|
||||
|
||||
export function request(options?: AgentOptions | undefined, app: App = defaultApp) {
|
||||
return supertest(app, options)
|
||||
}
|
||||
|
||||
export default request
|
||||
@@ -0,0 +1,19 @@
|
||||
import logger from "@/utils/logger"
|
||||
|
||||
function setLogLevel(level: string) {
|
||||
logger.level = level
|
||||
}
|
||||
|
||||
export const testWithCustomLogLevel = test.extend<{
|
||||
setLogLevel: (level: string) => void
|
||||
}>({
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
setLogLevel: async ({}, use) => {
|
||||
const originalLogLevel = logger.level
|
||||
try {
|
||||
await use(setLogLevel)
|
||||
} finally {
|
||||
setLogLevel(originalLogLevel)
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "./../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./../",
|
||||
"paths": {
|
||||
"@/*": ["./src/*", "./tests/*"]
|
||||
},
|
||||
"typeRoots": ["../node_modules/@types", "../@types", "./@types"],
|
||||
"types": ["node", "../vitest/globals"]
|
||||
},
|
||||
"include": ["../src/**/*", "../tests/**/*", ""]
|
||||
}
|
||||
Reference in New Issue
Block a user