Skip to content

Commit

Permalink
test(rest-adapter): add unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
DerYeger committed Oct 25, 2024
1 parent 6a44fc9 commit 890ad89
Show file tree
Hide file tree
Showing 4 changed files with 302 additions and 15 deletions.
17 changes: 14 additions & 3 deletions packages/framework/plugin-adapter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,20 @@ import { Stream } from '@yeger/streams'

export type SupportedPlugin<In, Out> = Plugin<In, Out, ParameterMetadata>

export type PluginAdapterState = 'not-started' | 'starting' | 'started'

export abstract class PluginAdapter<In, Out> {
protected plugins = new Map<string, Plugin<In, Out, any>>()

private started = false
#state: PluginAdapterState = 'not-started'

protected get state() {
return this.#state
}

private set state(value: PluginAdapterState) {
this.#state = value
}

/**
*
Expand Down Expand Up @@ -37,14 +47,15 @@ export abstract class PluginAdapter<In, Out> {

public start() {
this.requireNotStarted()
this.started = true
this.state = 'starting'
this.onStart()
this.state = 'started'
}

protected abstract onStart(): void

private requireNotStarted() {
if (this.started) {
if (this.state !== 'not-started') {
throw new Error('PluginAdapter has already been started.')
}
}
Expand Down
3 changes: 2 additions & 1 deletion packages/framework/rest-adapter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"check:publish": "publint run --strict",
"check:tsc": "tsc",
"dev": "vite build --watch",
"lint": "yeger-lint"
"lint": "yeger-lint",
"test": "vitest"
},
"dependencies": {
"@cm2ml/plugin": "workspace:*",
Expand Down
38 changes: 27 additions & 11 deletions packages/framework/rest-adapter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { fastify } from 'fastify'
class Server extends PluginAdapter<string[], StructuredOutput<unknown[], unknown>> {
private readonly server = fastify()

private finalized = false

protected onApply<Parameters extends ParameterMetadata>(
plugin: Plugin<string[], StructuredOutput<unknown[], unknown>, Parameters>,
) {
Expand All @@ -19,6 +21,30 @@ class Server extends PluginAdapter<string[], StructuredOutput<unknown[], unknown
}

protected onStart() {
this.finalize()

this.server.listen(
{ port: +(process.env.PORT ?? 8080) },
(err, address) => {
if (err) {
console.error(err)
process.exit(1)
}
// eslint-disable-next-line no-console
console.log(`Server listening at ${address}`)
},
)
}

/**
* Finalizes the server configuration.
* @returns The server instance.
*/
protected finalize() {
if (this.finalized) {
throw new Error('Server already finalized.')
}
this.finalized = true
const plugins = Stream.from(this.plugins.values())
.map((plugin) => ({
name: plugin.name,
Expand All @@ -36,17 +62,7 @@ class Server extends PluginAdapter<string[], StructuredOutput<unknown[], unknown
return { appliedPlugins: plugins.length }
})

this.server.listen(
{ port: +(process.env.PORT ?? 8080) },
(err, address) => {
if (err) {
console.error(err)
process.exit(1)
}
// eslint-disable-next-line no-console
console.log(`Server listening at ${address}`)
},
)
return this.server
}
}

Expand Down
259 changes: 259 additions & 0 deletions packages/framework/rest-adapter/test/rest-adapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
import { defineStructuredPlugin } from '@cm2ml/plugin'
import { describe, expect, it } from 'vitest'

import { createServer } from '../src'

type TestPlugin = Parameters<ReturnType<typeof createServer>['applyAll']>[0][number]

function getTestServer(plugins: TestPlugin[]) {
const server = createServer()
server.applyAll(plugins)
// Call the protected method illegally
return (server as any).finalize()
}

const testPluginA = defineStructuredPlugin({
name: 'a',
parameters: {
value: { type: 'string', description: 'value', defaultValue: 'fix' },
location: { type: 'string', description: 'location', defaultValue: 'prefix', allowedValues: ['prefix', 'postfix'] },
},
invoke(input: string[], parameters) {
return {
data: input.map((entry) => parameters.location === 'prefix' ? `${parameters.value}-${entry}` : `${entry}-${parameters.value}`),
metadata: parameters,
}
},
})

const testPluginB = defineStructuredPlugin({
name: 'b',
parameters: {
flag: { type: 'boolean', description: 'flag', defaultValue: false },
int: { type: 'number', description: 'int', defaultValue: 42 },
float: { type: 'number', description: 'float', defaultValue: 3.14 },
list: { type: 'list<string>', description: 'list', defaultValue: ['a', 'b'] },
set: { type: 'list<string>', description: 'ordered', ordered: false, unique: true, defaultValue: ['a', 'b'], allowedValues: ['a', 'b', 'c'] },
ordered: { type: 'list<string>', description: 'ordered', ordered: true, unique: true, defaultValue: ['a', 'b'], allowedValues: ['a', 'b', 'c'] },
},
invoke(input: string[], parameters) {
return { data: input, metadata: parameters }
},
})

const testPlugins = [testPluginA, testPluginB] as unknown[] as TestPlugin[]

describe('REST adapter', () => {
describe('/health', () => {
describe('GET', () => {
it('includes the number of applied plugins', async () => {
const server = getTestServer(testPlugins)
const response = await server.inject({ method: 'GET', url: '/health' })
expect(response.statusCode).toBe(200)
expect(response.json()).toEqual({ appliedPlugins: 2 })
})
})
})

describe('/plugins', () => {
describe('GET', () => {
it('returns the plugins and their metadata', async () => {
const server = getTestServer(testPlugins)
const response = await server.inject({ method: 'GET', url: '/plugins' })
expect(response.statusCode).toBe(200)
expect(response.json()).toEqual([
{
name: 'a',
parameters: {
...testPluginA.parameters,
},
},
{
name: 'b',
parameters: {
...testPluginB.parameters,
},
},
])
})
})

describe('/plugins/:pluginName', () => {
describe('POST', () => {
it('returns the plugins output', async () => {
const server = getTestServer(testPlugins)
const response = await server.inject({
method: 'POST',
url: `/plugins/${testPluginA.name}`,
body: {
input: ['hello', 'world'],
},
})
expect(response.statusCode).toBe(200)
expect(response.json()).toEqual({
data: [
'fix-hello',
'fix-world',
],
metadata: {
value: 'fix',
location: 'prefix',
},
})
})

it('returns 404 if the plugin does not exist', async () => {
const server = getTestServer(testPlugins)
const response = await server.inject({ method: 'POST', url: '/plugins/c' })
expect(response.statusCode).toBe(404)
})

it('returns 422 if the request body is invalid', async () => {
const server = getTestServer(testPlugins)
const response = await server.inject({
method: 'POST',
url: `/plugins/${testPluginA.name}`,
body: {
input: 'foo',
},
})
expect(response.statusCode).toBe(422)
})

it('returns 422 if the request body is missing', async () => {
const server = getTestServer(testPlugins)
const response = await server.inject({
method: 'POST',
url: `/plugins/${testPluginA.name}`,
})
expect(response.statusCode).toBe(422)
})

describe('with parameters', () => {
it('can configure string parameters', async () => {
const server = getTestServer(testPlugins)
const response = await server.inject({
method: 'POST',
url: `/plugins/${testPluginA.name}`,
body: {
input: ['hello', 'world'],
value: 'foo',
location: 'postfix',
},
})
expect(response.statusCode).toBe(200)
expect(response.json()).toEqual({
data: [
'hello-foo',
'world-foo',
],
metadata: {
value: 'foo',
location: 'postfix',
},
})
})

it('can configure boolean parameters to true', async () => {
const server = getTestServer(testPlugins)
const response = await server.inject({
method: 'POST',
url: `/plugins/${testPluginB.name}`,
body: {
input: ['hello', 'world'],
flag: true,
},
})
expect(response.statusCode).toBe(200)
expect(response.json().metadata.flag).toEqual(true)
})

it('can configure boolean parameters to false', async () => {
const server = getTestServer(testPlugins)
const response = await server.inject({
method: 'POST',
url: `/plugins/${testPluginB.name}`,
body: {
input: ['hello', 'world'],
flag: false,
},
})
expect(response.statusCode).toBe(200)
expect(response.json().metadata.flag).toEqual(false)
})

it('can configure numeric parameters', async () => {
const server = getTestServer(testPlugins)
const response = await server.inject({
method: 'POST',
url: `/plugins/${testPluginB.name}`,
body: {
input: ['hello', 'world'],
int: 23,
float: 2.71,
},
})
expect(response.statusCode).toBe(200)
expect(response.json().metadata.int).toEqual(23)
expect(response.json().metadata.float).toEqual(2.71)
})

it('can configure list parameters', async () => {
const server = getTestServer(testPlugins)
const response = await server.inject({
method: 'POST',
url: `/plugins/${testPluginB.name}`,
body: {
input: ['hello', 'world'],
list: ['foo', 'bar', 'foo'],
},
})
expect(response.statusCode).toBe(200)
expect(response.json().metadata.list).toEqual(['bar', 'foo', 'foo'])
})

it('can configure set parameters', async () => {
const server = getTestServer(testPlugins)
const response = await server.inject({
method: 'POST',
url: `/plugins/${testPluginB.name}`,
body: {
input: ['hello', 'world'],
set: ['c', 'a', 'b', 'a'],
},
})
expect(response.statusCode).toBe(200)
expect(response.json().metadata.set).toEqual(['a', 'b', 'c'])
})

it('can configure ordered parameters', async () => {
const server = getTestServer(testPlugins)
const response = await server.inject({
method: 'POST',
url: `/plugins/${testPluginB.name}`,
body: {
input: ['hello', 'world'],
ordered: ['c', 'a', 'b', 'a'],
},
})
expect(response.statusCode).toBe(200)
expect(response.json().metadata.ordered).toEqual(['c', 'a', 'b'])
})

it('returns 422 if the parameters are invalid', async () => {
const server = getTestServer(testPlugins)
const response = await server.inject({
method: 'POST',
url: `/plugins/${testPluginA.name}`,
body: {
input: ['hello', 'world'],
location: 'invalid',
},
})
expect(response.statusCode).toBe(422)
})
})
})
})
})
})

0 comments on commit 890ad89

Please sign in to comment.