diff --git a/package.json b/package.json index 64832d8..b2cd392 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ }, "scripts": { "build": "yarn workspaces foreach --all run build", - "test": "yarn workspaces foreach --all --parallel run test", + "test": "yarn workspaces foreach --all run test", "coverage:permissions": "yarn workspaces foreach --all --parallel run coverage:permissions", "check-git": "git status -uno --porcelain && [ -z \"$(git status -uno --porcelain)\" ] || (echo 'Git working directory not clean'; false)", "format": "npx @biomejs/biome format --write packages/**/*.{js,ts,json}", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 6f608cd..fd7d0e3 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -2,3 +2,4 @@ export * from './request.js' export * from './response.js' export * from './service.js' export * from './fixtures.js' +export * from './configuration.js' diff --git a/packages/core/src/service.ts b/packages/core/src/service.ts index deca56a..3094043 100644 --- a/packages/core/src/service.ts +++ b/packages/core/src/service.ts @@ -17,7 +17,7 @@ import { REQUEST_PROPERTIES, requestPropertyMatch } from './properties.js' import type { CoreRequest } from './request.js' import type { CoreResponse } from './response.js' -function createError(status: number, message: string): [number, { message: string }] { +export function createServiceError(status: number, message: string): [number, { message: string }] { return [status, { message: `[FIXTURE SERVER ERROR ${status}]: ${message}` }] } @@ -49,7 +49,7 @@ export function updateServiceConfiguration( const error = validateConfiguration(data) if (error) { - return createError(400, error.message) + return createServiceError(400, error.message) } const { cors, headers, query, cookies } = data @@ -71,13 +71,13 @@ export function createServiceFixture( const [fixture, validationError] = validateFixture(unsafeFixture, configuration) if (!fixture) { - return createError(400, validationError) + return createServiceError(400, validationError) } const { conflictError, fixtureId } = registerFixture(fixtureStorage, fixture, configuration) if (conflictError) { - return createError(409, conflictError) + return createServiceError(409, conflictError) } return [201, { id: fixtureId }] } @@ -100,7 +100,7 @@ export function createServiceFixtures( if (!fixture) { cleanUpOnError() - return createError(400, validationError) + return createServiceError(400, validationError) } const { conflictError, fixtureId } = registerFixture(fixtureStorage, fixture, configuration) @@ -108,7 +108,7 @@ export function createServiceFixtures( if (conflictError) { cleanUpOnError() - return createError(409, conflictError) + return createServiceError(409, conflictError) } fixtureIds.push({ id: fixtureId }) diff --git a/packages/dynamock/src/createServer.ts b/packages/dynamock/src/createServer.ts index 90e5298..d92502d 100644 --- a/packages/dynamock/src/createServer.ts +++ b/packages/dynamock/src/createServer.ts @@ -29,7 +29,7 @@ export function createServer() { const service = createService() app.post('/___fixtures', (req, res) => { - if (typeof req.body?.request === 'object') { + if (typeof req.body?.request === 'object' && req.body?.request !== null) { req.body.request.origin = `${req.protocol}://${req.get('host')}` } const [status, data] = createServiceFixture(service, req.body) @@ -39,7 +39,7 @@ export function createServer() { app.post('/___fixtures/bulk', (req, res) => { if (Array.isArray(req.body)) { for (const fixture of req.body) { - if (typeof fixture?.request === 'object') { + if (typeof fixture?.request === 'object' && fixture?.request !== null) { fixture.request.origin = `${req.protocol}://${req.get('host')}` } } diff --git a/packages/dynamock/test/bin.spec.ts b/packages/dynamock/test/bin.spec.ts index b7074ea..0a96827 100644 --- a/packages/dynamock/test/bin.spec.ts +++ b/packages/dynamock/test/bin.spec.ts @@ -1,17 +1,16 @@ import { afterEach, beforeEach, describe, expect, test } from '@jest/globals' -import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process' -import { getTestFiles } from '@dynamock/test-cases' -import { fetchWithBaseUrl, waitPortFree, waitPortUsed, wrapError } from './config/utils.js' +import { spawn, type ChildProcess } from 'node:child_process' +import { getTestFiles, wrapError } from '@dynamock/test-cases' +import { waitPortFree, waitPortUsed } from './config/utils.js' import { getServerTestCases } from './config/getTestCases.js' describe('bin integration tests', () => { - const allTests = getTestFiles() - const port = 5000 - const fetchDynamock = fetchWithBaseUrl(fetch, `http://localhost:${port}`) - let process: ChildProcessWithoutNullStreams + const allTests = getTestFiles() //.filter(([filePath]) => filePath.endsWith('create-and-delete-bulk.yml')) + const port = 3000 + let process: ChildProcess beforeEach(async () => { - process = spawn('dynamock', [String(port)]) + process = spawn('dynamock', [String(port)] /*, {stdio: 'inherit'}*/) await waitPortUsed(port) }) @@ -27,21 +26,50 @@ describe('bin integration tests', () => { for (let i = 0; i < testData.length; i++) { const { action, expectation } = testData[i] - const { path, ...options } = action + const { path, method, headers, body, bodyJSON, query } = action + const fetchOptions: { + method: string + headers: { [key: string]: string } + body: null | string + } = { + method, + headers: headers ?? {}, + body: null, + } - const result = await fetchDynamock(path, options) + if (bodyJSON !== undefined) { + fetchOptions.body = JSON.stringify(bodyJSON) + fetchOptions.headers = { + ...fetchOptions.headers, + 'Content-Type': 'application/json', + } + } else if (body !== undefined) { + fetchOptions.body = String(body) + } + + const url = new URL(`http://127.0.0.1:${port}${path}`) + + for (const queryKey in query) { + url.searchParams.set(queryKey, query[queryKey]) + } + + const result = await fetch(url.toString(), fetchOptions) if (expectation) { - const { status, body, bodyJSON } = expectation + const { status, headers, body, bodyJSON } = expectation if (status) { await wrapError(i, () => expect(result.status).toBe(status)) } - if (bodyJSON) { + if (headers) { + await wrapError(i, () => expect(result.headers).toMatchObject(headers)) + } + + if (bodyJSON !== undefined) { await wrapError(i, async () => expect(await result.json()).toEqual(bodyJSON)) } else if (body !== undefined) { - await wrapError(i, async () => expect(await result.json()).toEqual(body)) + await wrapError(i, async () => expect(await result.text()).toEqual(body)) } } } diff --git a/packages/dynamock/test/config/getTestCases.ts b/packages/dynamock/test/config/getTestCases.ts index ff9154d..ce40222 100644 --- a/packages/dynamock/test/config/getTestCases.ts +++ b/packages/dynamock/test/config/getTestCases.ts @@ -1,59 +1,42 @@ -import { type AnyYMLTestCase, getYMLTestCases } from '@dynamock/test-cases' +import { + ActionEnum, + type AnyYMLTestCase, + type ApiTest, + getYMLTestCases, + type YMLTestCaseExpectation, +} from '@dynamock/test-cases' type ServerTestCase = { - action: { - path: string - method: string - headers?: [] | object | null - cookies?: [] | object | null - query?: [] | object | null - options?: object | null - } - expectation: { - status?: number - body?: unknown - bodyJSON?: unknown - } + action: ApiTest + expectation: YMLTestCaseExpectation } export function getServerTestCases(absolutePath: string): ServerTestCase[] { const ymlTestCases: AnyYMLTestCase[] = getYMLTestCases(absolutePath) - return ymlTestCases.map(({ action, expectation }) => { + const testCases: (ServerTestCase | null)[] = ymlTestCases.map(({ action, expectation }) => { const { name, data } = action switch (name) { - case 'get_config': + case ActionEnum.get_config: return { action: { - ...data, path: '/___config', method: 'GET', + bodyJSON: data, }, - expectation: { - ...expectation, - body: undefined, - bodyJSON: expectation.body, - }, + expectation, } - case 'put_config': + case ActionEnum.put_config: return { action: { - ...data, path: '/___config', method: 'PUT', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }, - expectation: { - ...expectation, - body: undefined, - bodyJSON: expectation.body, + bodyJSON: data, }, + expectation, } - case 'delete_config': + case ActionEnum.delete_config: return { action: { path: '/___config', @@ -61,19 +44,50 @@ export function getServerTestCases(absolutePath: string): ServerTestCase[] { }, expectation, } - case 'post_fixture': + case ActionEnum.post_fixture: return { action: { - ...data, path: '/___fixtures', method: 'POST', + bodyJSON: data, + }, + expectation, + } + case ActionEnum.post_fixtures_bulk: + return { + action: { + path: '/___fixtures/bulk', + method: 'POST', + bodyJSON: data, + }, + expectation, + } + case ActionEnum.delete_fixture: + return { + action: { + path: '/___fixtures', + method: 'DELETE', + bodyJSON: data, }, - expectation: { - ...expectation, - body: undefined, - bodyJSON: expectation.body, + expectation, + } + case ActionEnum.delete_all_fixtures: + return { + action: { + path: '/___fixtures', + method: 'DELETE', }, + expectation, } + case ActionEnum.test_fixture: + return { + action: data as ApiTest, + expectation, + } + default: + return null } }) + + return testCases.filter((testCase) => testCase !== null) } diff --git a/packages/dynamock/test/config/utils.ts b/packages/dynamock/test/config/utils.ts index b148521..19611ab 100644 --- a/packages/dynamock/test/config/utils.ts +++ b/packages/dynamock/test/config/utils.ts @@ -37,20 +37,3 @@ export function waitPortUsed(port: number, reverse = false, retry = 50) { export function waitPortFree(port: number, retry?: number) { return waitPortUsed(port, true, retry) } - -export function fetchWithBaseUrl(initialFetch: (url: string, options: object) => Promise, baseUrl: string) { - return (url: string, options: object) => { - return initialFetch(baseUrl + url, options) - } -} - -export async function wrapError(testIndex: number, task: () => unknown) { - try { - return await task() - } catch (error: unknown) { - if (error instanceof Error) { - error.message += `\nTest iteration ${testIndex}` - } - throw error - } -} diff --git a/packages/dynamock/test/integrations.spec.ts b/packages/dynamock/test/integrations.spec.ts index 73ceade..a928f78 100644 --- a/packages/dynamock/test/integrations.spec.ts +++ b/packages/dynamock/test/integrations.spec.ts @@ -23,70 +23,9 @@ describe('integrations.js', () => { }) describe('manipulate configuration', () => { - test('default configuration', () => - request.get('/___config').expect(200, { - cors: null, - headers: {}, - query: {}, - cookies: {}, - })) - - test('update configuration', async () => { - await request - .put('/___config') - .send({ - headers: { - commonHeaders: { - 'x-header-1': '1', - 'x-header-2': '2', - }, - }, - }) - .expect(200) - - await request - .put('/___config') - .send({ - cors: '*', - headers: { - clientToken: { - authorization: 'Bearer client-token', - }, - }, - cookies: {}, - }) - .expect(200) - - return request.get('/___config').expect(200, { - cors: '*', - headers: { - commonHeaders: { - 'x-header-1': '1', - 'x-header-2': '2', - }, - clientToken: { - authorization: 'Bearer client-token', - }, - }, - query: {}, - cookies: {}, - }) - }) - - test('reset configuration', async () => { - await request.delete('/___config').expect(204) - }) - test.each([ - // valid - [{}, true], - [{ cors: '*', headers: {}, cookies: {}, query: {} }, true], - [{ cors: null, headers: {}, cookies: {}, query: {} }, true], - // invalid [[], false], - [{ unknown: 'unknown' }, false], - [{ headers: [] }, false], ])('validate config="%o" response="%o" options="%o" isValid=%s', (config, isValid) => { return request .put('/___config') @@ -98,13 +37,6 @@ describe('integrations.js', () => { describe('validation fixture', () => { test.each([ // request path / method - [{}, { body: '' }, false], - [{ unknown: 'unknown' }, { body: '' }, false], - [{ method: 'get' }, { body: '' }, false], - [{ path: '/' }, { body: '' }, false], - [{ path: '/' }, { body: '' }, false], - - [{ path: '/', method: 'unknown' }, { body: '' }, false], [{ path: '/', method: 'head' }, { body: '' }, true], [{ path: '/', method: 'HEAD' }, { body: '' }, true], [{ path: '/', method: 'delete' }, { body: '' }, true], @@ -185,134 +117,6 @@ describe('integrations.js', () => { .expect(413) }) - test('create and remove simple fixture', async () => { - await new Promise((resolve) => server.close(() => resolve(true))) - request = supertest.agent(server) - await new Promise((resolve) => server.listen(1111, () => resolve(true))) - - const products = [{ id: 1 }, { id: 2 }] - - await request - .post('/___fixtures') - .send({ - request: { - path: '/products', - method: 'get', - }, - response: { - status: 418, - body: products, - options: { - lifetime: 2, - }, - }, - }) - .expect(201, { - id: '52c9acef4355fe4844da764e588180318be6afde', - }) - - await request.get('/products').expect(418, products) - - await request.delete('/___fixtures/52c9acef4355fe4844da764e588180318be6afde').expect(204) - - await request.get('/products').expect(404) - }) - - test('create and remove multiple fixtures', async () => { - await new Promise((resolve) => server.close(() => resolve(true))) - request = supertest.agent(server) - await new Promise((resolve) => server.listen(2222, () => resolve(true))) - - const products = [{ id: 1 }, { id: 2 }] - const categories = [{ id: 1 }, { id: 2 }] - - await request - .post('/___fixtures/bulk') - .send([ - { - request: { - path: '/products', - method: 'get', - }, - response: { - body: products, - options: { - lifetime: 2, - }, - }, - }, - { - request: { - path: '/categories', - method: 'get', - }, - response: { - body: categories, - options: { - lifetime: 2, - }, - }, - }, - ]) - .expect(201, [ - { id: 'd1b3524ee44828f7bebafa57d612587f2849be4a' }, - { id: '2651bafc31c6ffd50a7df42019fb60865b044566' }, - ]) - - await Promise.all([ - request.get('/products').expect(200, products), - request.get('/categories').expect(200, categories), - ]) - - await Promise.all([ - request.delete('/___fixtures/d1b3524ee44828f7bebafa57d612587f2849be4a').expect(204), - request.delete('/___fixtures/2651bafc31c6ffd50a7df42019fb60865b044566').expect(204), - ]) - - await Promise.all([request.get('/products').expect(404), request.get('/categories').expect(404)]) - }) - - test('create and remove all fixtures', async () => { - await new Promise((resolve) => server.close(() => resolve(true))) - request = supertest.agent(server) - await new Promise((resolve) => server.listen(3333, () => resolve(true))) - - await request - .post('/___fixtures') - .send({ - request: { - path: '/octopus', - method: 'get', - }, - response: { - body: [], - }, - }) - .expect(201, { - id: 'dd9658b2bcf756ec362b854d823576fbf4e0e221', - }) - await request - .post('/___fixtures') - .send({ - request: { - path: '/giraffes', - method: 'get', - }, - response: { - body: [], - }, - }) - .expect(201, { - id: '8288f08371243d5cfad8522988c0092bed1600c2', - }) - - await request.delete('/___fixtures').expect(204) - - await request.get('/octopus').expect(404, {}) - - await request.get('/giraffes').expect(404, {}) - }) - test('create and remove filepath fixture', async () => { await new Promise((resolve) => server.close(() => resolve(true))) request = supertest.agent(server) diff --git a/packages/puppeteer/src/index.ts b/packages/puppeteer/src/index.ts index 4d06acf..3fa7e55 100644 --- a/packages/puppeteer/src/index.ts +++ b/packages/puppeteer/src/index.ts @@ -7,6 +7,7 @@ import { deleteServiceConfiguration, deleteServiceFixture, deleteServiceFixtures, + createServiceError, type FixtureRequestType, type FixtureResponseType, type FixtureType, @@ -97,6 +98,7 @@ async function initializeInterceptor(page: Page) { // TODO: need to computer cookies with coreResponse.cookies // TODO: body should be string|undefined in CoreResponse, already parsed const contentType = coreResponse.headers['content-type'] ?? '' + request.respond({ status: coreResponse.status, headers: coreResponse.headers, @@ -162,7 +164,15 @@ export async function dynamock( } if (fixtures.length) { - const coreFixtures = fixtures.map(mapToFixtureType) + let coreFixtures: FixtureType[] = [] + + try { + coreFixtures = fixtures.map(mapToFixtureType) + } catch (error) { + const [, { message }] = createServiceError(0, 'Missing or invalid request.url') + throw new Error(message) + } + const [fixturesStatus, fixturesOrError] = createServiceFixtures(service, coreFixtures) if (fixturesStatus !== 200 && fixturesOrError && 'message' in fixturesOrError) { diff --git a/packages/puppeteer/test/config/getTestCases.ts b/packages/puppeteer/test/config/getTestCases.ts index 1345914..67a5e75 100644 --- a/packages/puppeteer/test/config/getTestCases.ts +++ b/packages/puppeteer/test/config/getTestCases.ts @@ -1,43 +1,132 @@ -import { type AnyYMLTestCase, getYMLTestCases } from '@dynamock/test-cases' -import type { DynamockOptions, FixturePuppeteerType } from '../../src/index.js' -import type { ConfigurationType } from '@dynamock/core/dist/configuration.js' - -type PuppeteerTestCase = { - action: DynamockOptions - expectation: { - status?: number - body?: unknown - bodyJSON?: unknown +import { + ActionEnum, + type AnyYMLTestCase, + type ApiTest, + getYMLTestCases, + type YMLTestCaseExpectation, +} from '@dynamock/test-cases' +import type { FixturePuppeteerType } from '../../src/index.js' +import type { ConfigurationType } from '@dynamock/core' + +type PuppeteerTestCase = { + action: { + name: T + data: D } + expectation: YMLTestCaseExpectation } -export function getPuppeteerTestCases(absolutePath: string): PuppeteerTestCase[] { +type AllPuppeteerTestCase = + | PuppeteerTestCase | null }> + | PuppeteerTestCase + | PuppeteerTestCase + | PuppeteerTestCase + | PuppeteerTestCase + | PuppeteerTestCase + | PuppeteerTestCase + +export function getPuppeteerTestCases(absolutePath: string, origin: string): AllPuppeteerTestCase[] { const ymlTestCases: AnyYMLTestCase[] = getYMLTestCases(absolutePath) - const testCases: (PuppeteerTestCase | null)[] = ymlTestCases.map(({ action, expectation }) => { + const testCases: (AllPuppeteerTestCase | null)[] = ymlTestCases.map(({ action, expectation }) => { const { name, data } = action switch (name) { - case 'get_config': + case ActionEnum.get_config: return null - case 'put_config': + case ActionEnum.put_config: + return { + action: { + name: ActionEnum.put_config, + data: { + configuration: data as Partial | null, + }, + }, + expectation, + } + case ActionEnum.delete_config: + return { + action: { + name: ActionEnum.delete_config, + data: { + resetConfiguration: true, + }, + }, + expectation, + } + case ActionEnum.post_fixture: { + const newData: FixturePuppeteerType = data as FixturePuppeteerType + + // @ts-ignore + const path = data?.request?.path ?? '' + + if (path) { + newData.request.url = origin + path + } + + return { + action: { + name: ActionEnum.post_fixture, + data: { + fixtures: [newData] as FixturePuppeteerType[], + }, + }, + expectation, + } + } + case ActionEnum.post_fixtures_bulk: { + const newData = data as FixturePuppeteerType[] + + if (Array.isArray(newData)) { + for (const fixture of newData) { + // @ts-ignore + const path = fixture?.request?.path ?? '' + + if (path) { + fixture.request.url = origin + path + } + } + } + + return { + action: { + name: ActionEnum.post_fixtures_bulk, + data: { + fixtures: newData, + }, + }, + expectation, + } + } + case ActionEnum.delete_fixture: { + // @ts-ignore + const newData = data?.id as string + return { action: { - configuration: data as Partial | null, + name: ActionEnum.delete_fixture, + data: { + deleteFixtures: [newData], + }, }, expectation, } - case 'delete_config': + } + case ActionEnum.delete_all_fixtures: return { action: { - resetConfiguration: true, + name: ActionEnum.delete_all_fixtures, + data: { + resetFixtures: true, + }, }, expectation, } - case 'post_fixture': + case ActionEnum.test_fixture: return { action: { - fixtures: data as FixturePuppeteerType[], + name: ActionEnum.test_fixture, + data: data as ApiTest, }, expectation, } diff --git a/packages/puppeteer/test/config/setupTests.ts b/packages/puppeteer/test/config/setupTests.ts index 148a02d..3e8bc41 100644 --- a/packages/puppeteer/test/config/setupTests.ts +++ b/packages/puppeteer/test/config/setupTests.ts @@ -1,6 +1,12 @@ import puppeteer, { type Browser, type Page } from 'puppeteer' import { afterAll, afterEach, beforeAll, beforeEach } from '@jest/globals' -import { createServer as createHTTPServer, type IncomingMessage, type ServerResponse, type Server } from 'node:http' +import { + createServer as createHTTPServer, + request, + type IncomingMessage, + type ServerResponse, + type Server, +} from 'node:http' let browser: Browser let page: Page @@ -9,8 +15,15 @@ let server: Server beforeAll(async () => { browser = await puppeteer.launch() server = createHTTPServer((req: IncomingMessage, res: ServerResponse) => { - res.writeHead(200, { 'Content-Type': 'text/html' }) - res.end(``) + if (req.method === 'GET' && req.url === '/index.html') { + res.writeHead(200, { 'Content-Type': 'text/html' }) + res.end(``) + } else if (req.method === 'GET' && req.url === '/favicon.ico') { + res.writeHead(200, { 'Content-Type': 'text/plain' }) + res.end('') + } else { + res.writeHead(404).end() + } }) await new Promise((resolve) => server.listen(3000, '127.0.0.1', () => resolve(true))) }) diff --git a/packages/puppeteer/test/puppeteer.spec.ts b/packages/puppeteer/test/puppeteer.spec.ts index 40e81e6..ad6b4a9 100644 --- a/packages/puppeteer/test/puppeteer.spec.ts +++ b/packages/puppeteer/test/puppeteer.spec.ts @@ -1,57 +1,124 @@ -import { beforeEach, describe, expect, test } from '@jest/globals' +import { jest, afterEach, beforeEach, describe, expect, test } from '@jest/globals' import { page } from './config/setupTests.js' -import { getTestFiles } from '@dynamock/test-cases' +import { ActionEnum, getTestFiles, wrapError } from '@dynamock/test-cases' import { getPuppeteerTestCases } from './config/getTestCases.js' describe('puppeteer integration tests', () => { - const allTests = getTestFiles() + const allTests = getTestFiles() //.filter(([filePath]) => filePath.endsWith('create-and-delete-bulk.yml')) - beforeEach(() => page.goto('http://127.0.0.1:3000/')) + beforeEach(() => page.goto('http://127.0.0.1:3000/index.html')) + + afterEach(() => { + jest.resetModules() + }) test.each(allTests)('Test %s', async (absoluteFilePath) => { const { dynamock } = await import('../src/index.js') - const testData = getPuppeteerTestCases(absoluteFilePath) + const testData = getPuppeteerTestCases(absoluteFilePath, 'http://127.0.0.1:3000') for (let i = 0; i < testData.length; i++) { const { action, expectation } = testData[i] - - if ( - action.configuration !== undefined || - action.resetConfiguration !== undefined || - action.fixtures !== undefined - ) { - if (expectation.status === 400) { - try { - await dynamock(page, action) - } catch (error: unknown) { - if (error instanceof Error) { - expect(error.message.substring(0, 21)).toBe('[FIXTURE SERVER ERROR') - } else { - expect('error is not an instanceof Error').toBe(false) + // @ts-ignore + // console.log(action.name, action.data, expectation) + switch (action.name) { + case ActionEnum.put_config: + case ActionEnum.delete_config: + case ActionEnum.post_fixture: + case ActionEnum.post_fixtures_bulk: + case ActionEnum.delete_fixture: + case ActionEnum.delete_all_fixtures: { + if (expectation.status === 400) { + try { + await dynamock(page, action.data) + } catch (error: unknown) { + if (error instanceof Error) { + await wrapError(i, () => expect(error.message.substring(0, 21)).toBe('[FIXTURE SERVER ERROR')) + } else { + await wrapError(i, () => expect('error is not an instanceof Error').toBe(false)) + } } + } else { + await expect(dynamock(page, action.data)).resolves.toBe(undefined) } - } else { - await expect(dynamock(page, action)).resolves.toBe(undefined) + break } - } + case ActionEnum.test_fixture: { + const { path, method, body, bodyJSON, headers, query } = action.data + const safeHeaders = headers ?? {} + const safeQuery = query ?? {} + + const result = await page.evaluate( + async (_path, _method, _body, _bodyJSON, _headers, _query) => { + const fetchOptions: { + method: string + headers: { [key: string]: string } + body: null | string + } = { + method: _method, + headers: _headers, + body: null, + } + + if (_bodyJSON !== undefined) { + fetchOptions.body = JSON.stringify(_bodyJSON) + fetchOptions.headers = { + ...fetchOptions.headers, + 'Content-Type': 'application/json', + } + } else if (_body !== undefined) { + fetchOptions.body = String(_body) + } + + const url = new URL(`http://127.0.0.1:3000${_path}`) + + for (const queryKey in _query) { + url.searchParams.set(queryKey, _query[queryKey]) + } + + const result = await fetch(url.toString(), fetchOptions) + + const bodyText = await result.text() - // const result = await page.evaluate(async () => { - // const result = await fetch('http://127.0.0.1:5000/pandas/1', { - // method: 'POST', - // }) - // - // return { - // status: result.status, - // body: await result.json(), - // } - // }) - // - // expect(result).toEqual({ - // status: 201, - // body: { - // id: 1, - // }, - // }) + let bodyJSON = undefined + try { + bodyJSON = JSON.parse(bodyText) + } catch (error) {} + + return { + status: result.status, + headers: Object.entries(_headers).reduce<{ [key: string]: string }>((acc, [key, value]) => { + acc[key] = value + + return acc + }, {}), + bodyText, + bodyJSON, + } + }, + path, + method, + body, + bodyJSON, + safeHeaders, + safeQuery, + ) + + if (expectation.status) { + await wrapError(i, () => expect(result.status).toBe(expectation.status)) + } + + if (expectation.headers) { + // @ts-ignore + await wrapError(i, () => expect(result.headers).toMatchObject(expectation.headers)) + } + + if (expectation.bodyJSON !== undefined) { + await wrapError(i, async () => expect(result.bodyJSON).toEqual(expectation.bodyJSON)) + } else if (expectation.body !== undefined) { + await wrapError(i, async () => expect(result.bodyText).toEqual(expectation.body)) + } + } + } } }) diff --git a/packages/test-cases/index.ts b/packages/test-cases/index.ts index 715303a..2fa63fb 100644 --- a/packages/test-cases/index.ts +++ b/packages/test-cases/index.ts @@ -2,6 +2,7 @@ import { readdirSync, readFileSync, statSync } from 'node:fs' import { join } from 'node:path' import { URL } from 'node:url' import { load } from 'js-yaml' +import type { HeadersInit } from 'undici-types/fetch.js' function readDirectoryRecursively(directory: string): string[][] { let results: string[][] = [] @@ -27,16 +28,29 @@ export function getTestFiles() { return readDirectoryRecursively(pathname).filter(([fullPath]) => fullPath.endsWith('.yml')) } +export type YMLTestCaseExpectation = { + status?: number + headers?: { [key: string]: string } + body?: null | string + bodyJSON?: unknown +} + type YMLTestCase = { action: { name: A json: boolean - data: object | null - } - expectation: { - status?: number - body?: unknown + data: object } + expectation: YMLTestCaseExpectation +} + +export type ApiTest = { + path: string + method: string + headers?: { [key: string]: string } + query?: { [key: string]: string } + body?: null | string + bodyJSON?: unknown } export enum ActionEnum { @@ -44,11 +58,24 @@ export enum ActionEnum { get_config = 'get_config', delete_config = 'delete_config', post_fixture = 'post_fixture', + post_fixtures_bulk = 'post_fixtures_bulk', + delete_fixture = 'delete_fixture', + delete_all_fixtures = 'delete_all_fixtures', + test_fixture = 'test_fixture', } const actions = Object.values(ActionEnum) -export type AnyYMLTestCase = YMLTestCase<'put_config' | 'get_config' | 'delete_config' | 'post_fixture'> +export type AnyYMLTestCase = YMLTestCase< + | 'put_config' + | 'get_config' + | 'delete_config' + | 'post_fixture' + | 'post_fixtures_bulk' + | 'delete_fixture' + | 'delete_all_fixtures' + | 'test_fixture' +> export function getYMLTestCases(absolutePath: string): AnyYMLTestCase[] { const content = readFileSync(absolutePath).toString() @@ -83,3 +110,22 @@ export function getYMLTestCases(absolutePath: string): AnyYMLTestCase[] { return data } + +export async function wrapError(testIndex: number, task: () => unknown) { + try { + return await task() + } catch (error: unknown) { + if (error instanceof Error) { + error.message += `\nTest iteration ${testIndex}` + } + throw error + } +} + +export function fetchHeadersToObject(headers: HeadersInit) { + return Object.entries(headers).reduce<{ [key: string]: string }>((acc, [key, value]) => { + acc[key] = value + + return acc + }, {}) +} diff --git a/packages/test-cases/test-files/config/manipulate/delete-all.yml b/packages/test-cases/test-files/config/manipulate/delete-all.yml index 3ced981..61a8553 100644 --- a/packages/test-cases/test-files/config/manipulate/delete-all.yml +++ b/packages/test-cases/test-files/config/manipulate/delete-all.yml @@ -13,7 +13,7 @@ cbar: "cbar" expectation: status: 200 - body: + bodyJSON: cors: "*" headers: hfoo: "hfoo" @@ -32,7 +32,7 @@ name: get_config expectation: status: 200 - body: + bodyJSON: cors: null headers: {} query: {} diff --git a/packages/test-cases/test-files/config/manipulate/get-default.yml b/packages/test-cases/test-files/config/manipulate/get-default.yml index 417368c..c0af01e 100644 --- a/packages/test-cases/test-files/config/manipulate/get-default.yml +++ b/packages/test-cases/test-files/config/manipulate/get-default.yml @@ -2,7 +2,7 @@ name: get_config expectation: status: 200 - body: + bodyJSON: cors: null headers: {} query: {} diff --git a/packages/test-cases/test-files/config/manipulate/put-all.yml b/packages/test-cases/test-files/config/manipulate/put-all.yml index beadf10..efb197d 100644 --- a/packages/test-cases/test-files/config/manipulate/put-all.yml +++ b/packages/test-cases/test-files/config/manipulate/put-all.yml @@ -13,7 +13,7 @@ cbar: "cbar" expectation: status: 200 - body: + bodyJSON: cors: "*" headers: hfoo: "hfoo" @@ -28,7 +28,7 @@ name: get_config expectation: status: 200 - body: + bodyJSON: cors: "*" headers: hfoo: "hfoo" diff --git a/packages/test-cases/test-files/config/manipulate/put-cookies.yml b/packages/test-cases/test-files/config/manipulate/put-cookies.yml index 31a6bec..3aa601d 100644 --- a/packages/test-cases/test-files/config/manipulate/put-cookies.yml +++ b/packages/test-cases/test-files/config/manipulate/put-cookies.yml @@ -5,7 +5,7 @@ jwt: "jwt" expectation: status: 200 - body: + bodyJSON: cors: null headers: {} query: {} @@ -19,10 +19,11 @@ jwt3: "jwt3" expectation: status: 200 - cors: null - headers: {} - query: {} - cookies: - jwt: "jwt" - jwt2: "jwt2" - jwt3: "jwt3" + bodyJSON: + cors: null + headers: {} + query: {} + cookies: + jwt: "jwt" + jwt2: "jwt2" + jwt3: "jwt3" diff --git a/packages/test-cases/test-files/config/manipulate/put-cors.yml b/packages/test-cases/test-files/config/manipulate/put-cors.yml index c50f6e6..c9464f3 100644 --- a/packages/test-cases/test-files/config/manipulate/put-cors.yml +++ b/packages/test-cases/test-files/config/manipulate/put-cors.yml @@ -4,7 +4,17 @@ cors: null expectation: status: 200 - body: + bodyJSON: + cors: null + headers: {} + query: {} + cookies: {} +- action: + name: put_config + data: {} + expectation: + status: 200 + bodyJSON: cors: null headers: {} query: {} @@ -12,12 +22,11 @@ - action: name: put_config data: - cors: "*" + cors: null expectation: status: 200 - body: - cors: "*" + bodyJSON: + cors: null headers: {} query: {} cookies: {} - diff --git a/packages/test-cases/test-files/config/manipulate/put-headers.yml b/packages/test-cases/test-files/config/manipulate/put-headers.yml index c82140c..7ba02d7 100644 --- a/packages/test-cases/test-files/config/manipulate/put-headers.yml +++ b/packages/test-cases/test-files/config/manipulate/put-headers.yml @@ -7,7 +7,7 @@ authorization: "Bearer client-token" expectation: status: 200 - data: + bodyJSON: cors: null headers: clientToken: @@ -24,7 +24,7 @@ x-header-2: "2" expectation: status: 200 - data: + bodyJSON: cors: null headers: clientToken: diff --git a/packages/test-cases/test-files/config/manipulate/put-query.yml b/packages/test-cases/test-files/config/manipulate/put-query.yml index 0122d4f..5ee37d0 100644 --- a/packages/test-cases/test-files/config/manipulate/put-query.yml +++ b/packages/test-cases/test-files/config/manipulate/put-query.yml @@ -6,7 +6,7 @@ foo: "foo" expectation: status: 200 - body: + bodyJSON: cors: null headers: { } query: @@ -21,7 +21,7 @@ bar2: "bar2" expectation: status: 200 - body: + bodyJSON: cors: null headers: { } query: diff --git a/packages/test-cases/test-files/fixtures/manipulate/create-and-delete-all.yml b/packages/test-cases/test-files/fixtures/manipulate/create-and-delete-all.yml new file mode 100644 index 0000000..b5ac226 --- /dev/null +++ b/packages/test-cases/test-files/fixtures/manipulate/create-and-delete-all.yml @@ -0,0 +1,43 @@ +- action: + name: post_fixture + data: + request: + path: "/octopus" + method: "GET" + response: + body: [] + expectation: + status: 201 + bodyJSON: + id: 766f863e8ec1a0c30bd1ee6e10856da29e416611 +- action: + name: post_fixture + data: + request: + path: "/giraffes" + method: "GET" + response: + body: [] + expectation: + status: 201 + bodyJSON: + id: e995ff6a68319687e34a14af3babf6d916890681 +- action: + name: delete_all_fixtures + data: + expectation: + status: 204 +- action: + name: test_fixture + data: + path: "/octopus" + method: "GET" + expectation: + status: 404 +- action: + name: test_fixture + data: + path: "/giraffes" + method: "GET" + expectation: + status: 404 diff --git a/packages/test-cases/test-files/fixtures/manipulate/create-and-delete-bulk.yml b/packages/test-cases/test-files/fixtures/manipulate/create-and-delete-bulk.yml new file mode 100644 index 0000000..d916a2a --- /dev/null +++ b/packages/test-cases/test-files/fixtures/manipulate/create-and-delete-bulk.yml @@ -0,0 +1,80 @@ +- action: + name: post_fixtures_bulk + data: + - request: + path: "/products" + method: "GET" + response: + headers: + content-type: "application/json" + body: + products: + - id: 1 + - id: 2 + options: + lifetime: 2 + - request: + path: "/categories" + method: "GET" + response: + headers: + content-type: "application/json" + body: + categories: + - id: 1 + - id: 2 + options: + lifetime: 2 + expectation: + status: 201 + bodyJSON: + - id: e94408a374abb23afc3a6b188dfb98aad791e562 + - id: 02e7486f03fd81badeb74dec75aaf7a88f180137 +- action: + name: test_fixture + data: + path: "/products" + method: "GET" + expectation: + status: 200 + bodyJSON: + products: + - id: 1 + - id: 2 +- action: + name: test_fixture + data: + path: "/categories" + method: "GET" + expectation: + status: 200 + bodyJSON: + categories: + - id: 1 + - id: 2 +- action: + name: delete_fixture + data: + id: e94408a374abb23afc3a6b188dfb98aad791e562 + expectation: + status: 204 +- action: + name: delete_fixture + data: + id: 02e7486f03fd81badeb74dec75aaf7a88f180137 + expectation: + status: 204 +- action: + name: test_fixture + data: + path: "/products" + method: "GET" + expectation: + status: 404 +- action: + name: test_fixture + data: + path: "/categories" + method: "GET" + expectation: + status: 404 diff --git a/packages/test-cases/test-files/fixtures/manipulate/create-and-delete.yml b/packages/test-cases/test-files/fixtures/manipulate/create-and-delete.yml new file mode 100644 index 0000000..04247fc --- /dev/null +++ b/packages/test-cases/test-files/fixtures/manipulate/create-and-delete.yml @@ -0,0 +1,44 @@ +- action: + name: post_fixture + data: + request: + path: "/products" + method: "GET" + response: + status: 418 + headers: + content-type: "application/json" + body: + products: + - id: 1 + - id: 2 + options: + lifetime: 2 + expectation: + status: 201 + bodyJSON: + id: e94408a374abb23afc3a6b188dfb98aad791e562 +- action: + name: test_fixture + data: + path: "/products" + method: "GET" + expectation: + status: 418 + bodyJSON: + products: + - id: 1 + - id: 2 +- action: + name: delete_fixture + data: + id: e94408a374abb23afc3a6b188dfb98aad791e562 + expectation: + status: 204 +- action: + name: test_fixture + data: + path: "/products" + method: "GET" + expectation: + status: 404 \ No newline at end of file diff --git a/packages/test-cases/test-files/fixtures/validation/invalid-request-method.yml b/packages/test-cases/test-files/fixtures/validation/invalid-request-method.yml index 3d57f13..7380ec5 100644 --- a/packages/test-cases/test-files/fixtures/validation/invalid-request-method.yml +++ b/packages/test-cases/test-files/fixtures/validation/invalid-request-method.yml @@ -1,3 +1,4 @@ +# missing method - action: name: post_fixture data: @@ -7,6 +8,7 @@ status: 200 expectation: status: 400 +# empty method - action: name: post_fixture data: @@ -17,6 +19,7 @@ status: 200 expectation: status: 400 +# null method - action: name: post_fixture data: @@ -27,3 +30,14 @@ status: 200 expectation: status: 400 +# unknown method +- action: + name: post_fixture + data: + request: + method: "unknown" + path: "/pandas/1" + response: + status: 200 + expectation: + status: 400 \ No newline at end of file diff --git a/packages/test-cases/test-files/fixtures/validation/invalid-request.yml b/packages/test-cases/test-files/fixtures/validation/invalid-request.yml index b4bae3e..03eb6a4 100644 --- a/packages/test-cases/test-files/fixtures/validation/invalid-request.yml +++ b/packages/test-cases/test-files/fixtures/validation/invalid-request.yml @@ -6,6 +6,22 @@ status: 200 expectation: status: 400 +- action: + name: post_fixture + data: + request: {} + response: + status: 200 + expectation: + status: 400 +- action: + name: post_fixture + data: + request: null + response: + status: 200 + expectation: + status: 400 - action: name: post_fixture data: