Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: Set up integration testing #66

Merged
merged 1 commit into from
Jul 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions integration/backend/trigger.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import assert from 'node:assert'
import { cleanup, startTestServer } from '../fixtures.js'

describe('/api/trigger', () => {
afterEach(async () => await cleanup())

it('requires authentication', async () => {
const { origin } = await startTestServer()

const response = await fetch(`${origin}/api/trigger`, { method: 'POST' })
assert.strictEqual(response.status, 403)
assert.strictEqual(response.headers.get('Content-Type'), 'application/json; charset=utf-8')
assert.deepStrictEqual(await response.json(), { error: 'Forbidden' })
})
})
73 changes: 73 additions & 0 deletions integration/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import pino, { BaseLogger } from 'pino'
import { KubeConfig } from '@kubernetes/client-node'
import { startServer } from '../src/server.js'
import { createConfig } from '../src/config.js'

const testServerPort = 3333
const testClusterPort = 56443

const cleanupFunctions: Array<() => Promise<void>> = []

/**
* Clean up any resources created during tests. This should be called after each test,
* i.e., in an `afterEach` hook.
*/
export async function cleanup (): Promise<void> {
for (const cleanupFunction of cleanupFunctions) {
await cleanupFunction()
}
cleanupFunctions.splice(0, cleanupFunctions.length)
}

interface TestServerResult {
origin: string
}

/**
* Start a server for integration tests. The server will be closed when `cleanup` is called,
* which should be done after each test.
*
* @returns Information about the server.
*/
export async function startTestServer (): Promise<TestServerResult> {
const closeFn = await startServer({
log: getTestLogger(),
config: createConfig(),
port: testServerPort,
kubeConfig: getTestKubeConfig()
})
cleanupFunctions.push(closeFn)
return {
origin: `http://127.0.0.1:${testServerPort}`
}
}

function getTestLogger (): BaseLogger {
return pino({ level: 'silent' })
}

function getTestKubeConfig (): KubeConfig {
const kubeConfig = new KubeConfig()
kubeConfig.loadFromOptions({
clusters: [
{
name: 'test-cluster',
server: `http://127.0.0.1:${testClusterPort}`
}
],
contexts: [
{
name: 'test-context',
cluster: 'test-cluster',
user: 'test-user'
}
],
users: [
{
name: 'test-user'
}
],
currentContext: 'test-context'
})
return kubeConfig
}
112 changes: 112 additions & 0 deletions integration/frontend.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import assert from 'node:assert'
import { cleanup, startTestServer } from './fixtures.js'

describe('frontend', () => {
afterEach(async () => await cleanup())

it('serves index.html by default', async () => {
const { origin } = await startTestServer()

for (const path of ['/', '/index.html', '/foo', '/foo/bar']) {
const getResponse = await fetch(`${origin}${path}`)
assert.strictEqual(getResponse.status, 200)

const text = await getResponse.text()
assert.ok(text.includes('<title>Foreman</title>'))

// should set proper headers
const { headers } = getResponse
assert.strictEqual(headers.get('Content-Security-Policy'), "default-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'")
assert.strictEqual(headers.get('X-Frame-Options'), 'DENY')
assert.strictEqual(headers.get('X-Content-Type-Options'), 'nosniff')
assert.strictEqual(headers.get('Referrer-Policy'), 'no-referrer')
assert.strictEqual(headers.get('Cache-Control'), 'public, max-age=0')
assert.strictEqual(headers.get('Content-Type'), 'text/html; charset=UTF-8')

// should also respond to HEAD requests
const headResponse = await fetch(`${origin}${path}`, { method: 'HEAD' })
assert.strictEqual(headResponse.status, 200)
assert.strictEqual(headResponse.headers.get('Content-Type'), headers.get('Content-Type'))
assert.strictEqual(headResponse.headers.get('Content-Length'), headers.get('Content-Length'))
}
})

it('has no inline scripts or styles', async () => {
// Inline CSS/JS is a security risk, and is disallowed by the Content Security Policy.

const { origin } = await startTestServer()

const response = await fetch(`${origin}/`)
assert.strictEqual(response.status, 200)

const text = await response.text()

// script tags with content (vs. references to external scripts)
assert.doesNotMatch(text, /<script[^>]*>[^<]+<\/script>/i)
// inline event handlers
assert.doesNotMatch(text, /\bon[a-z]+=/i)
// style tags
assert.doesNotMatch(text, /<style/i)
// style attributes
assert.doesNotMatch(text, /style=/i)
})

it('references external scripts and styles', async () => {
const { origin } = await startTestServer()

const response = await fetch(`${origin}/`)
assert.strictEqual(response.status, 200)

const text = await response.text()

const stylesheet = text.match(/<link[^>]*\shref="([^"]+)"/i)
assert.ok(stylesheet)
assert.match(stylesheet[1], /^\/assets\/index-.+\.css$/)

const script = text.match(/<script[^>]*\ssrc="([^"]+)"/i)
assert.ok(script)
assert.match(script[1], /^\/assets\/index-.+\.js$/)
})

it('serves static files', async () => {
const { origin } = await startTestServer()

const favicon = await fetch(`${origin}/assets/favicon.ico`)
assert.strictEqual(favicon.status, 200)
assert.strictEqual(favicon.headers.get('Content-Type'), 'image/vnd.microsoft.icon')

const robots = await fetch(`${origin}/robots.txt`)
assert.strictEqual(robots.status, 200)
assert.strictEqual(robots.headers.get('Content-Type'), 'text/plain; charset=UTF-8')
assert.strictEqual(await robots.text(), 'User-agent: *\nDisallow: /\n')

const webmanifest = await fetch(`${origin}/assets/manifest.webmanifest`)
assert.strictEqual(webmanifest.status, 200)
assert.strictEqual(webmanifest.headers.get('Content-Type'), 'application/manifest+json')
})

it('responds with 404 for unexpected request methods', async () => {
const { origin } = await startTestServer()

for (const method of ['POST', 'PUT', 'DELETE', 'PATCH']) {
for (const path of ['/', '/index.html', '/foo', '/foo/bar', '/assets/favicon.ico']) {
const response = await fetch(`${origin}${path}`, { method })
assert.strictEqual(response.status, 404)
assert.strictEqual(response.headers.get('Content-Type'), 'application/json; charset=utf-8')
assert.deepStrictEqual(await response.json(), { error: 'Not Found' })
}
}
})

it('responds with 400 for unknown request methods', async () => {
const { origin } = await startTestServer()

const response = await fetch(`${origin}/`, { method: 'MYRANDOMMETHOD' })
assert.strictEqual(response.status, 400)
assert.deepStrictEqual(await response.json(), {
error: 'Bad Request',
message: 'Client Error',
statusCode: 400
})
})
})
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
"scripts": {
"build": "npm run build --workspaces && node -e \"fs.rmSync('./dist',{force:true,recursive:true})\" && tsc",
"lint": "npm run lint --workspaces && tsc --noEmit -p tsconfig.lint.json && eslint --ignore-path .gitignore src",
"test": "npm run test --workspaces --if-present && mocha --require tsx --recursive \"test/**/*.ts\"",
"test": "npm run test:unit && npm run test:integration",
"test:unit": "npm run test --workspaces --if-present && mocha --require tsx --recursive \"test/**/*.ts\"",
"test:integration": "mocha --require tsx --recursive \"integration/**/*.ts\"",
"start": "node --disable-proto=delete dist/main.js"
},
"repository": {
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.lint.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"extends": "./tsconfig.json",
"include": [
"src",
"test"
"test",
"integration"
]
}