Skip to content

Commit 96c3ded

Browse files
kriszypclaude
andcommitted
test(integration): port token-auth suite to harperLifecycle
Fourth slice of the api-tests migration. Independent of #627 (no component-install dependency) — based directly on main. - token-auth.test.mjs (from tests/14_tokenAuth.mjs): create_authentication_tokens happy path + missing/invalid credentials, bearer-token search_by_hash, refresh_operation_token with valid/invalid tokens. - Self-contained: seeds a single northnwd.employees row in before() so the bearer-token search has a deterministic record. The legacy version inherited this from 2_dataLoad.mjs. - Preserves the legacy isDevEnv() branching by reading authentication.authorizeLocal from get_configuration in before(). With authorizeLocal=true (the integration framework default for loopback callers), the no-credentials call mints a token; otherwise it returns 401 "Must login". This matches the legacy test exactly. All 46 tests across the 6 ported suites pass in parallel (this branch omits the rest.test.mjs from PR #627; CI will see 49 once both land). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 87da5a4 commit 96c3ded

1 file changed

Lines changed: 186 additions & 0 deletions

File tree

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
/**
2+
* Token authentication integration tests.
3+
*
4+
* Ported from legacy `apiTests/tests/14_tokenAuth.mjs`. Validates:
5+
* - `create_authentication_tokens` happy path + missing/invalid-credential
6+
* paths
7+
* - bearer-token search (`search_by_hash` with `Authorization: Bearer <op>`)
8+
* - `refresh_operation_token` with valid and invalid refresh tokens
9+
*
10+
* Self-contained: each suite seeds a minimal `northnwd.employees` table
11+
* with `employeeid: 1` so the bearer-token search has a deterministic
12+
* record to fetch. The legacy version inherited this from
13+
* `2_dataLoad.mjs`, but the new layout owns its own data.
14+
*
15+
* The integration framework starts Harper on a loopback IP, which gets
16+
* auto-authorized when `authentication.authorizeLocal` is true (the
17+
* default). The legacy test branched on `isDevEnv()` to handle both
18+
* configurations; we preserve that branching since the framework's
19+
* default config controls which mode applies.
20+
*/
21+
import { suite, test, before, after } from 'node:test';
22+
import assert from 'node:assert/strict';
23+
import request from 'supertest';
24+
import { startHarper, teardownHarper } from '@harperfast/integration-testing';
25+
import { createApiClient } from './utils/client.mjs';
26+
27+
const SCHEMA = 'northnwd';
28+
const TABLE = 'employees';
29+
const PRIMARY_KEY = 'employeeid';
30+
31+
suite('Token authentication', (ctx) => {
32+
let client;
33+
let admin;
34+
let operationToken;
35+
let refreshToken;
36+
let authorizeLocal;
37+
38+
before(async () => {
39+
await startHarper(ctx, { config: {}, env: {} });
40+
client = createApiClient(ctx.harper);
41+
admin = ctx.harper.admin;
42+
43+
// Discover which auth-local mode the framework is configured with so
44+
// the no-credentials assertion below can branch correctly.
45+
const config = await client.req().send({ operation: 'get_configuration' }).expect(200);
46+
authorizeLocal = config.body.authentication?.authorizeLocal === true;
47+
48+
// Seed a single employees row so the bearer-token search has a record.
49+
await client.req().send({ operation: 'create_schema', schema: SCHEMA }).expect(200);
50+
await client
51+
.req()
52+
.send({ operation: 'create_table', schema: SCHEMA, table: TABLE, hash_attribute: PRIMARY_KEY })
53+
.expect(200);
54+
await client
55+
.req()
56+
.send({
57+
operation: 'insert',
58+
schema: SCHEMA,
59+
table: TABLE,
60+
records: [{ employeeid: 1, firstname: 'Test', lastname: 'Employee' }],
61+
})
62+
.expect(200);
63+
});
64+
65+
after(async () => {
66+
await teardownHarper(ctx);
67+
});
68+
69+
test('create_authentication_tokens with no credentials reflects authorizeLocal mode', async () => {
70+
const r = await request(client.operationsURL)
71+
.post('')
72+
.set({ 'Content-Type': 'application/json' })
73+
.send({ operation: 'create_authentication_tokens' });
74+
if (authorizeLocal) {
75+
// Loopback caller is auto-authorized → mints a token.
76+
assert.equal(r.status, 200, r.text);
77+
assert.notEqual(r.body.operation_token, undefined, r.text);
78+
} else {
79+
assert.equal(r.status, 401, r.text);
80+
assert.equal(r.body.error, 'Must login', r.text);
81+
}
82+
});
83+
84+
test('create_authentication_tokens with no password returns 401', async () => {
85+
await request(client.operationsURL)
86+
.post('')
87+
.set({ 'Content-Type': 'application/json' })
88+
.send({ operation: 'create_authentication_tokens', username: admin.username })
89+
.expect((r) => assert.equal(r.body.error, 'invalid credentials', r.text))
90+
.expect(401);
91+
});
92+
93+
test('create_authentication_tokens with bad credentials returns 401', async () => {
94+
await request(client.operationsURL)
95+
.post('')
96+
.set({ 'Content-Type': 'application/json' })
97+
.send({
98+
operation: 'create_authentication_tokens',
99+
username: 'baduser',
100+
password: 'bad',
101+
bypass_auth: true,
102+
})
103+
.expect((r) => assert.equal(r.body.error, 'invalid credentials', r.text))
104+
.expect(401);
105+
});
106+
107+
test('create_authentication_tokens happy path returns operation + refresh tokens', async () => {
108+
const response = await request(client.operationsURL)
109+
.post('')
110+
.set({ 'Content-Type': 'application/json' })
111+
.send({
112+
operation: 'create_authentication_tokens',
113+
username: admin.username,
114+
password: admin.password,
115+
})
116+
.expect(200);
117+
118+
assert.notEqual(response.body.operation_token, undefined, response.text);
119+
assert.notEqual(response.body.refresh_token, undefined, response.text);
120+
operationToken = response.body.operation_token;
121+
refreshToken = response.body.refresh_token;
122+
});
123+
124+
test('search_by_hash with valid JWT returns the seeded record', async () => {
125+
await request(client.operationsURL)
126+
.post('')
127+
.set('Content-Type', 'application/json')
128+
.set('Authorization', `Bearer ${operationToken}`)
129+
.send({
130+
operation: 'search_by_hash',
131+
schema: SCHEMA,
132+
table: TABLE,
133+
primary_key: PRIMARY_KEY,
134+
hash_values: [1],
135+
get_attributes: ['*'],
136+
})
137+
.expect((r) => assert.equal(r.body.length, 1, r.text))
138+
.expect((r) => assert.equal(r.body[0][PRIMARY_KEY], 1, r.text))
139+
.expect(200);
140+
});
141+
142+
test('search_by_hash with invalid JWT returns 401', async () => {
143+
await request(client.operationsURL)
144+
.post('')
145+
.set('Content-Type', 'application/json')
146+
.set('Authorization', 'Bearer BAD_TOKEN')
147+
.send({
148+
operation: 'search_by_hash',
149+
schema: SCHEMA,
150+
table: TABLE,
151+
primary_key: PRIMARY_KEY,
152+
hash_values: [1],
153+
get_attributes: ['*'],
154+
})
155+
.expect((r) => assert.ok(r.text.includes('"error":"invalid token"'), r.text))
156+
.expect(401);
157+
});
158+
159+
test('refresh_operation_token with valid refresh token mints a new operation token', async () => {
160+
const response = await request(client.operationsURL)
161+
.post('')
162+
.set('Content-Type', 'application/json')
163+
.set('Authorization', `Bearer ${refreshToken}`)
164+
.send({ operation: 'refresh_operation_token' })
165+
.expect(200);
166+
167+
assert.notEqual(response.body.operation_token, undefined, response.text);
168+
operationToken = response.body.operation_token;
169+
});
170+
171+
test('refresh_operation_token with invalid token returns 401', async () => {
172+
await request(client.operationsURL)
173+
.post('')
174+
.set('Content-Type', 'application/json')
175+
.set('Authorization', 'Bearer bad token')
176+
.send({ operation: 'refresh_operation_token' })
177+
.expect((r) => assert.ok(r.text.includes('invalid token'), r.text))
178+
.expect(401);
179+
});
180+
181+
test('create_authentication_tokens with basic-auth current user works', async () => {
182+
const response = await client.req().send({ operation: 'create_authentication_tokens' }).expect(200);
183+
assert.notEqual(response.body.operation_token, undefined, response.text);
184+
assert.notEqual(response.body.refresh_token, undefined, response.text);
185+
});
186+
});

0 commit comments

Comments
 (0)