Skip to content
Merged
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
183 changes: 183 additions & 0 deletions integrationTests/apiTests/authentication.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
/**
* Authentication integration tests.
*
* Ported from legacy `apiTests/tests/21_authenticationTests.mjs`. Validates:
* - `describe_all` with valid / invalid basic-auth credentials
* - `describe_all` without any auth (branches on `authorizeLocal`)
* - `create_authentication_tokens` happy path + edge cases (empty fields,
* wrong credentials, empty both fields)
* - `describe_all` with a valid JWT operation token
*
* Self-contained: no schema or table setup required.
*
* `describe_all with empty credentials` is skipped on Bun: sending
* `Basic <base64(':')>` (empty user:pass) causes a stack overflow in
* Harper-on-Bun instead of a graceful 401. Tracked in issue #696.
*/
import { suite, test, before, after } from 'node:test';
import assert from 'node:assert/strict';
import request from 'supertest';
import { startHarper, teardownHarper } from '@harperfast/integration-testing';
import { createApiClient } from './utils/client.mjs';

// Empty Basic-auth credentials cause a stack overflow in Harper-on-Bun (issue #696)
const skipOnBun = process.env.HARPER_RUNTIME === 'bun';

suite('Authentication', (ctx) => {
let client;
let admin;
let authorizeLocal;
let operationToken;

before(async () => {
await startHarper(ctx, { config: {}, env: {} });
client = createApiClient(ctx.harper);
admin = ctx.harper.admin;

const config = await client.req().send({ operation: 'get_configuration' }).expect(200);
authorizeLocal = config.body.authentication?.authorizeLocal === true;
});

after(async () => {
await teardownHarper(ctx);
});

test('describe_all with valid credentials returns data', async () => {
await client.req().send({ operation: 'describe_all' }).expect(200);
});

test('describe_all with invalid password returns 401', async () => {
await request(client.operationsURL)
.post('')
.set({
'Authorization': `Basic ${Buffer.from(`${admin.username}:thisIsNotMyPassword`).toString('base64')}`,
'Content-Type': 'application/json',
})
.send({ operation: 'describe_all' })
.expect((r) => assert.ok(r.text.includes('Login failed'), r.text))
.expect(401);
});

test('describe_all with invalid username returns 401', async () => {
await request(client.operationsURL)
.post('')
.set({
'Authorization': `Basic ${Buffer.from(`thisIsNotMyUsername:${admin.password}`).toString('base64')}`,
'Content-Type': 'application/json',
})
.send({ operation: 'describe_all' })
.expect((r) => assert.ok(r.text.includes('Login failed'), r.text))
.expect(401);
});

test('describe_all with empty credentials returns 401', { skip: skipOnBun }, async () => {
await request(client.operationsURL)
.post('')
.set({
'Authorization': `Basic ${Buffer.from(':').toString('base64')}`,
'Content-Type': 'application/json',
})
.send({ operation: 'describe_all' })
.expect((r) => {
assert.ok(r.text.includes('Must login') || r.text.includes('Login failed'), r.text);
})
.expect(401);
});

test('describe_all with oversized credentials returns 401', async () => {
const longStr = 'a'.repeat(4000);
await request(client.operationsURL)
.post('')
.set({
'Authorization': `Basic ${Buffer.from(`${longStr}:${longStr}`).toString('base64')}`,
'Content-Type': 'application/json',
})
.send({ operation: 'describe_all' })
.expect((r) => assert.ok(r.text.includes('Login failed'), r.text))
.expect(401);
});

test('describe_all without auth reflects authorizeLocal mode', async () => {
const r = await request(client.operationsURL)
.post('')
.set({ 'Content-Type': 'application/json' })
.send({ operation: 'describe_all' });

if (authorizeLocal) {
// Fresh instance has no schemas yet so describe_all returns {}; just check it succeeded.
assert.equal(r.status, 200, r.text);
} else {
assert.ok(r.text.includes('Must login'), r.text);
assert.equal(r.status, 401, r.text);
}
});

test('create_authentication_tokens with valid credentials returns operation token', async () => {
const response = await request(client.operationsURL)
.post('')
.set('Content-Type', 'application/json')
.send({
operation: 'create_authentication_tokens',
username: admin.username,
password: admin.password,
})
.expect(200);

assert.ok(response.body.hasOwnProperty('operation_token'), response.text);
assert.ok(response.body.operation_token, response.text);
operationToken = response.body.operation_token;
});

test('describe_all with valid bearer token returns data', async () => {
await request(client.operationsURL)
.post('')
.set('Content-Type', 'application/json')
.set('Authorization', `Bearer ${operationToken}`)
.send({ operation: 'describe_all' })
// Fresh instance has no schemas; just verify the token was accepted (200).
.expect(200);
});

test('create_authentication_tokens with empty password returns 400', async () => {
await request(client.operationsURL)
.post('')
.set('Content-Type', 'application/json')
.send({ operation: 'create_authentication_tokens', username: admin.username, password: '' })
.expect((r) => assert.ok(JSON.stringify(r.body).includes("'password' is not allowed to be empty"), r.text))
.expect(400);
});

test('create_authentication_tokens with empty username returns 400', async () => {
await request(client.operationsURL)
.post('')
.set('Content-Type', 'application/json')
.send({ operation: 'create_authentication_tokens', username: '', password: admin.password })
.expect((r) => assert.ok(JSON.stringify(r.body).includes("'username' is not allowed to be empty"), r.text))
.expect(400);
});

test('create_authentication_tokens with wrong credentials returns 401', async () => {
await request(client.operationsURL)
.post('')
.set('Content-Type', 'application/json')
.send({ operation: 'create_authentication_tokens', username: 'wronguser', password: 'wrongpass' })
.expect((r) => assert.ok(JSON.stringify(r.body).includes('invalid credentials'), r.text))
.expect(401);
});

test('create_authentication_tokens with both fields empty reflects authorizeLocal mode', async () => {
const r = await request(client.operationsURL)
.post('')
.set('Content-Type', 'application/json')
.send({ operation: 'create_authentication_tokens', username: '', password: '' });

if (authorizeLocal) {
// Loopback auto-auth means the request reaches validation — both fields empty → 400
assert.ok(JSON.stringify(r.body).includes("'username' is not allowed to be empty"), r.text);
assert.equal(r.status, 400, r.text);
} else {
assert.ok(JSON.stringify(r.body).includes('Must login'), r.text);
assert.equal(r.status, 401, r.text);
}
});
});
Loading