Skip to content

Commit

Permalink
test: Add integration tests for local login (#68)
Browse files Browse the repository at this point in the history
  • Loading branch information
meyfa authored Jul 20, 2024
1 parent 1bee339 commit 855c0a9
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 3 deletions.
120 changes: 120 additions & 0 deletions integration/backend/auth/local.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import assert from 'node:assert'
import { cleanup, startTestServer } from '../../fixtures.js'

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

it('returns 404 if local login not configured', async () => {
const { origin } = await startTestServer()

const response = await fetch(`${origin}/api/auth/local`, { method: 'POST' })
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('returns 400 for invalid body schema', async () => {
const { origin } = await startTestServer({
config (input) {
return {
...input,
auth: {
...input.auth,
local: {
enabled: true,
username: 'test-user',
password: 'test-password'
}
}
}
}
})

const bodies = [
'{}',
'{"username":"test-user"}',
'{"password":"test-password"}',
'{"username":"user-test","password":null}'
]

for (const body of bodies) {
const response = await fetch(`${origin}/api/auth/local`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body
})
assert.strictEqual(response.status, 400)
// TODO respond with JSON instead of plain text
assert.strictEqual(response.headers.get('Content-Type'), 'text/plain; charset=utf-8')
assert.strictEqual(await response.text(), 'Bad Request')
}
})

it('returns 401 for invalid credentials', async () => {
const { origin } = await startTestServer({
config (input) {
return {
...input,
auth: {
...input.auth,
local: {
enabled: true,
username: 'test-user',
password: 'test-password'
}
}
}
}
})

const bodies = [
'{"username":"user-test","password":"password-test"}',
'{"username":"test-user","password":"password-test"}',
'{"username":"test-user ","password":"test-password"}',
'{"username":"test-user","password":"test-password "}'
]

for (const body of bodies) {
const response = await fetch(`${origin}/api/auth/local`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body
})
assert.strictEqual(response.status, 401)
// TODO respond with JSON instead of plain text
assert.strictEqual(response.headers.get('Content-Type'), 'text/plain; charset=utf-8')
assert.deepStrictEqual(await response.text(), 'Unauthorized')
}
})

it('creates a session for valid credentials', async () => {
const { origin } = await startTestServer({
config (input) {
return {
...input,
auth: {
...input.auth,
local: {
enabled: true,
username: 'test-user',
password: 'test-password'
}
}
}
}
})

const response = await fetch(`${origin}/api/auth/local`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{"username":"test-user","password":"test-password"}'
})
assert.strictEqual(response.status, 200)
assert.strictEqual(response.headers.get('Content-Type'), 'application/json; charset=utf-8')
// TODO set SameSite=Strict
const setCookie = response.headers.getSetCookie()
assert.ok(setCookie.length === 1)
assert.match(setCookie[0], /^session=[^;]+; Max-Age=86400; Path=\/api; HttpOnly; SameSite=Lax$/)
assert.deepStrictEqual(await response.json(), {})
})
})
83 changes: 83 additions & 0 deletions integration/backend/auth/logout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import assert from 'node:assert'
import { cleanup, startTestServer } from '../../fixtures.js'

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

it('returns 200 if called without a session', async () => {
const { origin } = await startTestServer()

// Note: This sets a cookie, but its contents indicate a missing session.
const response = await fetch(`${origin}/api/auth/logout`, { method: 'POST' })
assert.strictEqual(response.status, 200)
assert.strictEqual(response.headers.get('Content-Type'), 'application/json; charset=utf-8')
// TODO set SameSite=Strict
const setCookie = response.headers.getSetCookie()
assert.ok(setCookie.length === 1)
assert.match(setCookie[0], /^session=[^;]+; Max-Age=86400; Path=\/api; HttpOnly; SameSite=Lax$/)
assert.deepStrictEqual(await response.json(), {})

// user should not have a session
const meResponse = await fetch(`${origin}/api/auth/me`, { method: 'GET' })
assert.strictEqual(meResponse.status, 403)
})

it('invalidates an existing session', async () => {
const { origin } = await startTestServer({
config (input) {
return {
...input,
auth: {
...input.auth,
local: {
enabled: true,
username: 'test-user',
password: 'test-password'
}
}
}
}
})

const loginResponse = await fetch(`${origin}/api/auth/local`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{"username":"test-user","password":"test-password"}'
})
assert.strictEqual(loginResponse.status, 200)

const sessionCookie = loginResponse.headers.getSetCookie().at(0)
assert.ok(sessionCookie?.startsWith('session='))
const session = sessionCookie?.split(';').at(0)?.split('=').at(1)
assert.ok(session != null)

// validate session
const meResponse = await fetch(`${origin}/api/auth/me`, {
headers: { Cookie: `session=${session}` }
})
assert.strictEqual(meResponse.status, 200)

// logout
const logoutResponse = await fetch(`${origin}/api/auth/logout`, {
method: 'POST',
headers: { Cookie: `session=${session}` }
})
assert.strictEqual(logoutResponse.status, 200)
assert.strictEqual(logoutResponse.headers.get('Content-Type'), 'application/json; charset=utf-8')
// TODO set SameSite=Strict
const setCookie = logoutResponse.headers.getSetCookie()
assert.ok(setCookie.length === 1)
assert.match(setCookie[0], /^session=[^;]+; Max-Age=86400; Path=\/api; HttpOnly; SameSite=Lax$/)
assert.deepStrictEqual(await logoutResponse.json(), {})

const sessionCookie2 = setCookie.at(0)?.split(';').at(0)?.split('=').at(1)
assert.ok(sessionCookie2 != null)
assert.notStrictEqual(sessionCookie2, session)

// session should be invalidated
const meResponse2 = await fetch(`${origin}/api/auth/me`, {
headers: { Cookie: `session=${sessionCookie2}` }
})
assert.strictEqual(meResponse2.status, 403)
})
})
15 changes: 15 additions & 0 deletions integration/backend/auth/me.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/auth/me', () => {
afterEach(async () => await cleanup())

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

const response = await fetch(`${origin}/api/auth/me`, { method: 'GET' })
assert.strictEqual(response.status, 403)
assert.strictEqual(response.headers.get('Content-Type'), 'application/json; charset=utf-8')
assert.deepStrictEqual(await response.json(), { error: 'Forbidden' })
})
})
42 changes: 42 additions & 0 deletions integration/backend/auth/strategies.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import assert from 'node:assert'
import { cleanup, startTestServer } from '../../fixtures.js'

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

it('returns empty array if not strategies are enabled', async () => {
const { origin } = await startTestServer()

const response = await fetch(`${origin}/api/auth/strategies`, { method: 'GET' })
assert.strictEqual(response.status, 200)
assert.strictEqual(response.headers.get('Content-Type'), 'application/json; charset=utf-8')

const strategies = await response.json()
assert.deepStrictEqual(strategies, [])
})

it('includes local login if enabled', async () => {
const { origin } = await startTestServer({
config (input) {
return {
...input,
auth: {
...input.auth,
local: {
enabled: true,
username: 'test',
password: 'test'
}
}
}
}
})

const response = await fetch(`${origin}/api/auth/strategies`, { method: 'GET' })
assert.strictEqual(response.status, 200)
assert.strictEqual(response.headers.get('Content-Type'), 'application/json; charset=utf-8')

const strategies = await response.json()
assert.deepStrictEqual(strategies, ['local'])
})
})
12 changes: 9 additions & 3 deletions integration/fixtures.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pino, { BaseLogger } from 'pino'
import { KubeConfig } from '@kubernetes/client-node'
import { startServer } from '../src/server.js'
import { createConfig } from '../src/config.js'
import { Config, createConfig } from '../src/config.js'

const testServerPort = 3333
const testClusterPort = 56443
Expand Down Expand Up @@ -29,10 +29,16 @@ interface TestServerResult {
*
* @returns Information about the server.
*/
export async function startTestServer (): Promise<TestServerResult> {
export async function startTestServer (options?: {
config?: (input: Config) => Config
}): Promise<TestServerResult> {
let config = createConfig()
if (options?.config != null) {
config = options.config(config)
}
const closeFn = await startServer({
log: getTestLogger(),
config: createConfig(),
config,
port: testServerPort,
kubeConfig: getTestKubeConfig()
})
Expand Down

0 comments on commit 855c0a9

Please sign in to comment.