Skip to content

Commit 9a03ce4

Browse files
egor-romanovlaktek
authored andcommitted
feat: add basic test and testing core
1 parent a4a6f4f commit 9a03ce4

File tree

10 files changed

+324
-3
lines changed

10 files changed

+324
-3
lines changed

.gitignore

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,8 @@ dist
105105
.tern-port
106106

107107
# Generated docs
108-
docs/
108+
docs/
109+
110+
# Test reports
111+
allure-results
112+
allure-report

.mocharc.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
async-only: true
2+
bail: false
3+
check-leaks: true
4+
color: true
5+
delay: false
6+
diff: true
7+
exit: false # could be expressed as "no-exit: true"
8+
extension:
9+
- ts
10+
full-trace: false
11+
recursive: true
12+
require:
13+
- 'ts-node/register'
14+
- source-map-support/register
15+
retries: 0
16+
sort: false
17+
spec: test/spec/**/*.spec.ts
18+
timeout: 60000
19+
reporter: mocha-multi-reporters
20+
reporter-options: configFile=config.json
21+
globals: 'allure'

config.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"reporterEnabled": "spec,mocha-allure2-reporter"
3+
}

package.json

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
"build:module": "tsc -p tsconfig.module.json",
1515
"docs": "typedoc src/index.ts src/types.ts --excludePrivate --excludeProtected",
1616
"docs:json": "typedoc --json docs/spec.json --excludeExternals src/index.ts src/types.ts",
17-
"test": "echo \"Error: no test specified\" && exit 1"
17+
"test": "mocha",
18+
"allure:generate": "node_modules/allure-commandline/bin/allure generate",
19+
"allure:serve": "node_modules/allure-commandline/bin/allure serve",
20+
"test:report": "npm run allure:generate && npm run allure:serve"
1821
},
1922
"repository": {
2023
"type": "git",
@@ -38,12 +41,31 @@
3841
"cross-fetch": "^3.1.5"
3942
},
4043
"devDependencies": {
44+
"@types/chai": "^4.3.0",
45+
"@types/jsonwebtoken": "^8.5.8",
46+
"@types/mocha": "^9.1.0",
47+
"@types/node": "^17.0.23",
48+
"allure-commandline": "^2.17.2",
49+
"chai": "^4.3.6",
50+
"genversion": "^3.0.2",
51+
"jsonwebtoken": "^8.5.1",
52+
"mocha": "^8.4.0",
53+
"mocha-allure2-reporter": "^0.0.3",
54+
"mocha-multi-reporters": "^1.5.1",
55+
"nanoid": "^3.3.1",
4156
"npm-run-all": "^4.1.5",
4257
"prettier": "^2.6.0",
4358
"rimraf": "^3.0.2",
4459
"semantic-release": "^19.0.2",
4560
"semantic-release-plugin-update-version-in-files": "^1.1.0",
61+
<<<<<<< HEAD
4662
"typedoc": "^0.22.16",
63+
=======
64+
"testcontainers": "^8.5.1",
65+
"ts-node": "^10.7.0",
66+
"ts-test-decorators": "^0.0.6",
67+
"typedoc": "^0.22.13",
68+
>>>>>>> b3a0615 (feat: add basic test and testing core)
4769
"typescript": "^4.6.2"
4870
},
4971
"publishConfig": {

test/functions/hello.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { serve } from 'https://deno.land/std/http/server.ts'
2+
3+
serve(() => new Response('Hello World'))

test/relay/container.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import * as fs from 'fs'
2+
import { nanoid } from 'nanoid'
3+
import { GenericContainer, Network, StartedTestContainer, Wait } from 'testcontainers'
4+
import { ExecResult } from 'testcontainers/dist/docker/types'
5+
import { ContentType } from 'allure2-js-commons'
6+
import { attach, log } from '../utils/allure'
7+
8+
/**
9+
* A Relay contains a running relay container that has a unique ID and two promises:
10+
* one for the `deno cache` and one for the `deno run` the required function
11+
*/
12+
export class Relay {
13+
container: StartedTestContainer
14+
id: string
15+
execCache: Promise<ExecResult>
16+
execRun: Promise<ExecResult>
17+
constructor(
18+
container: StartedTestContainer,
19+
id: string,
20+
execCache: Promise<ExecResult>,
21+
execRun: Promise<ExecResult>
22+
) {
23+
this.container = container
24+
this.id = id
25+
this.execCache = execCache
26+
this.execRun = execRun
27+
}
28+
}
29+
30+
/**
31+
* It starts a docker container with a deno relay, and waits for it to be ready
32+
* @param {string} slug - the name of the function to deploy
33+
* @param {string} jwtSecret - the JWT secret to access function
34+
* @param {string} [denoOrigin=http://localhost:8000] - the origin of the deno server
35+
* @param {Map<string, string>} env - list of environment variables for deno relay
36+
* @returns {Promise<Relay>} A Relay object.
37+
*/
38+
export async function runRelay(
39+
slug: string,
40+
jwtSecret: string,
41+
denoOrigin: string = 'http://localhost:8000',
42+
env?: Map<string, string>
43+
): Promise<Relay> {
44+
// read function to deploy
45+
log('read function body')
46+
const functionBytes = fs.readFileSync('test/functions/' + slug + '.ts', 'utf8')
47+
attach('function body', functionBytes, ContentType.TEXT)
48+
49+
// random id for parallel execution
50+
const id = nanoid(5)
51+
52+
//create network
53+
log('add network')
54+
const network = await new Network({ name: 'supabase_network_' + id }).start()
55+
56+
// create relay container
57+
log(`create relay ${slug + '-' + id}`)
58+
const relay = await new GenericContainer('supabase/deno-relay')
59+
.withName(slug + '-' + id)
60+
.withCmd([
61+
'sh',
62+
'-c',
63+
`cat <<'EOF' > /home/deno/${slug}.ts && deno run --allow-all index.ts
64+
` +
65+
functionBytes +
66+
`
67+
EOF
68+
`,
69+
])
70+
.withNetworkMode(network.getName())
71+
.withExposedPorts(8081)
72+
.withWaitStrategy(Wait.forLogMessage('Listening on http://0.0.0.0:8081'))
73+
.withStartupTimeout(15000)
74+
.withReuse()
75+
76+
// add envs
77+
env = parseEnv(env, jwtSecret, denoOrigin)
78+
env && env.forEach((value, key) => relay.withEnv(key, value))
79+
80+
// start relay and function
81+
log(`start relay ${slug + '-' + id}`)
82+
const startedRelay = await relay.start()
83+
const execCache = startedRelay.exec(['deno', 'cache', `/home/deno/${slug}.ts`])
84+
const execRun = startedRelay.exec(['deno', 'run', '--allow-all', `/home/deno/${slug}.ts`])
85+
86+
// wait till function is running
87+
log(`check function is healthy: ${slug + '-' + id}`)
88+
for (let ctr = 0; ctr < 30; ctr++) {
89+
const healthCheck = await startedRelay.exec(['nc', '-z', 'localhost', '8000'])
90+
if (healthCheck.exitCode == 0) {
91+
log(`function started to serve: ${slug + '-' + id}`)
92+
return { container: startedRelay, id, execCache, execRun }
93+
}
94+
await new Promise((resolve) => setTimeout(resolve, 500))
95+
}
96+
97+
// if function hasn't started, stop container and throw
98+
log(`function failed to start: ${slug + '-' + id}`)
99+
startedRelay.stop()
100+
throw new Error("function haven'start correctly")
101+
}
102+
103+
/**
104+
* If the JWT_SECRET and DENO_ORIGIN environment is not set, set it
105+
* @param env - The environment variables.
106+
* @param {string} jwtSecret - The JWT secret.
107+
* @param {string} denoOrigin - The origin of the Deno server.
108+
* @returns {Map<string, string>} - `env` variables map.
109+
*/
110+
function parseEnv(
111+
env: Map<string, string> | undefined | null,
112+
jwtSecret: string,
113+
denoOrigin: string
114+
): Map<string, string> {
115+
if (env) {
116+
!env.has('JWT_SECRET') && jwtSecret && env.set('JWT_SECRET', jwtSecret)
117+
!env.has('DENO_ORIGIN') && denoOrigin && env.set('DENO_ORIGIN', denoOrigin)
118+
} else {
119+
env = new Map([
120+
['JWT_SECRET', jwtSecret],
121+
['DENO_ORIGIN', denoOrigin],
122+
])
123+
}
124+
return env
125+
}

test/spec/hello.spec.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import 'mocha'
2+
import { assert } from 'chai'
3+
import { nanoid } from 'nanoid'
4+
import { sign } from 'jsonwebtoken'
5+
6+
import { FunctionsClient } from '../../src/index'
7+
8+
import { Relay, runRelay } from '../relay/container'
9+
import { log } from '../utils/allure'
10+
11+
describe('HelloTest', () => {
12+
let relay: Relay
13+
let fclient: FunctionsClient
14+
const jwtSecret = nanoid(10)
15+
let apiKey = sign({ name: 'anon' }, jwtSecret)
16+
17+
before(async () => {
18+
relay = await runRelay('hello', jwtSecret)
19+
fclient = new FunctionsClient(`http://localhost:${relay.container.getMappedPort(8081)}`, {
20+
Authorization: `Bearer ${apiKey}`,
21+
})
22+
})
23+
24+
after(async () => {
25+
relay && relay.container && (await relay.container.stop())
26+
})
27+
28+
it('set up relay with hello function and invoke it', async () => {
29+
log('invoke hello')
30+
const { data, error } = await fclient.invoke('hello', { responseType: 'text' })
31+
32+
log('assert no error')
33+
assert.isNull(error)
34+
log(`assert ${data} is equal to 'Hello World'`)
35+
assert.equal(data, 'Hello World')
36+
})
37+
})

test/utils/allure.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { Allure } from 'allure-js-commons'
2+
import { ContentType } from 'allure2-js-commons'
3+
4+
type TestDecorator = (
5+
target: object,
6+
property: string,
7+
descriptor: PropertyDescriptor
8+
) => PropertyDescriptor
9+
10+
function getAllure(): Allure {
11+
// @ts-ignore
12+
const allure: Allure = global.allure
13+
if (!allure) {
14+
throw new Error('Unable to find Allure implementation')
15+
}
16+
return allure
17+
}
18+
19+
export function step<T>(nameFn: string | ((arg: T) => string)): TestDecorator {
20+
return (target: object, propertyKey: string, descriptor: PropertyDescriptor) => {
21+
const original: object = descriptor.value
22+
let callable: (args: T) => void = () => {
23+
/* */
24+
}
25+
26+
if (typeof original === 'function') {
27+
descriptor.value = function (...args: [T]) {
28+
try {
29+
const value: string = typeof nameFn === 'function' ? nameFn.apply(this, args) : nameFn
30+
callable = () => getAllure().step(value, () => original.apply(this, args))
31+
// tslint:disable-next-line:no-console
32+
console.info(`Step: ${value || nameFn}`)
33+
} catch (e) {
34+
// tslint:disable-next-line:no-console
35+
console.error(`[ERROR] Failed to apply decorator: ${e}`)
36+
}
37+
return callable.apply(this, args)
38+
}
39+
}
40+
return descriptor
41+
}
42+
}
43+
44+
export function attachment<T>(name: string, type: ContentType) {
45+
return (
46+
_target: object,
47+
_propertyKey: string,
48+
descriptor: PropertyDescriptor
49+
): PropertyDescriptor => {
50+
const original: object = descriptor.value
51+
let callable: (args: T) => void = () => {
52+
/* */
53+
}
54+
55+
if (typeof original === 'function') {
56+
descriptor.value = async function (...args: [T]) {
57+
try {
58+
const content: Buffer | string = await original.apply(this, args)
59+
callable = () =>
60+
getAllure().step(name, () => {
61+
getAllure().attachment(type.toString(), content, type)
62+
})
63+
} catch (e) {
64+
// tslint:disable-next-line:no-console
65+
console.error(`[ERROR] Failed to apply decorator: ${e}`)
66+
}
67+
return callable.apply(this, args)
68+
}
69+
}
70+
return descriptor
71+
}
72+
}
73+
74+
export function attach(name: string, content: string | Buffer, type: ContentType): void {
75+
getAllure().step(name, () => {
76+
getAllure().attachment(type.toString(), content, type)
77+
})
78+
}
79+
80+
export function log(name: string, description?: string): void {
81+
// console.info(description ? `${name}: ${description}` : name);
82+
getAllure().step(name, () => {
83+
if (description) {
84+
getAllure().step(description, () => {
85+
/* */
86+
})
87+
}
88+
})
89+
}

test/utils/timeoutPromise.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export default async function timeoutRequest<T>(request: Promise<T>, timeout: number): Promise<T> {
2+
let timer: NodeJS.Timeout
3+
4+
request.finally(() => clearTimeout(timer))
5+
6+
const timeoutPromise = new Promise<T>((_, reject) => {
7+
timer = setTimeout(() => reject(new Error(`Timeout (${timeout}) for task exceeded`)), timeout)
8+
})
9+
10+
return Promise.race<T>([request, timeoutPromise])
11+
}

tsconfig.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
"declaration": true,
66
"declarationMap": true,
77
"module": "CommonJS",
8+
"emitDecoratorMetadata": true,
9+
"experimentalDecorators": true,
810
"outDir": "dist/main",
911
"rootDir": "src",
1012
"sourceMap": true,
@@ -18,5 +20,9 @@
1820
"forceConsistentCasingInFileNames": true,
1921
"stripInternal": true,
2022
"allowSyntheticDefaultImports": true
21-
}
23+
},
24+
"typeRoots": [
25+
"./node_modules/allure2-js-commons/dist/declarations/**/",
26+
"./src/types"
27+
]
2228
}

0 commit comments

Comments
 (0)