232 lines
8.2 KiB
JavaScript
232 lines
8.2 KiB
JavaScript
#!/usr/bin/env node
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
|
|
import mysql from 'mysql2/promise';
|
|
|
|
import { createLocalGateStack } from '../release-gate/local-stack.mjs';
|
|
|
|
const root = path.resolve(new URL('..', import.meta.url).pathname);
|
|
const runId = `gate-${new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14)}`;
|
|
const stack = await createLocalGateStack({ root, runId });
|
|
const checks = [];
|
|
|
|
function record(id, passed, detail) {
|
|
checks.push({ id, passed, detail });
|
|
console.log(`${passed ? 'PASS' : 'FAIL'} ${id} ${detail}`);
|
|
}
|
|
|
|
async function register(username, password) {
|
|
const response = await fetch(`${stack.baseUrl}/auth/register`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
username,
|
|
password,
|
|
displayName: `Gate ${username}`,
|
|
email: `${username}@example.invalid`,
|
|
}),
|
|
});
|
|
const body = await response.json().catch(() => ({}));
|
|
if (!response.ok) throw new Error(`register ${username}: ${response.status} ${JSON.stringify(body)}`);
|
|
return body;
|
|
}
|
|
|
|
async function login(username, password) {
|
|
const response = await fetch(`${stack.baseUrl}/auth/login`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username, password }),
|
|
});
|
|
const body = await response.json().catch(() => ({}));
|
|
const cookie = response.headers.get('set-cookie')?.split(';')[0] ?? '';
|
|
return { response, body, cookie };
|
|
}
|
|
|
|
let failed = false;
|
|
try {
|
|
const userA = `${runId.replaceAll('-', '').slice(-12)}a`;
|
|
const userB = `${runId.replaceAll('-', '').slice(-12)}b`;
|
|
const rateUser = `${runId.replaceAll('-', '').slice(-12)}r`;
|
|
const password = 'Gate-Local-Only-2026!';
|
|
const registeredA = await register(userA, password);
|
|
const registeredB = await register(userB, password);
|
|
await register(rateUser, password);
|
|
const userAId = registeredA.user?.id;
|
|
const userBId = registeredB.user?.id;
|
|
if (!userAId || !userBId) throw new Error('registered local users have no stable ids');
|
|
|
|
const rejectedStatuses = [];
|
|
for (let attempt = 0; attempt < 5; attempt += 1) {
|
|
const rejected = await login(rateUser, `wrong-password-${attempt}`);
|
|
rejectedStatuses.push(rejected.response.status);
|
|
}
|
|
const rateLimited = await login(rateUser, password);
|
|
const retryAfter = Number(rateLimited.response.headers.get('retry-after') ?? 0);
|
|
record(
|
|
'AUTH-02',
|
|
rejectedStatuses.every((status) => status === 401)
|
|
&& rateLimited.response.status === 429
|
|
&& retryAfter > 0,
|
|
`wrong=${rejectedStatuses.join(',')} limited=${rateLimited.response.status} retry_after=${retryAfter}`,
|
|
);
|
|
|
|
const authA = await login(userA, password);
|
|
const authB = await login(userB, password);
|
|
const authenticatedStatus = await fetch(`${stack.baseUrl}/auth/status`, {
|
|
headers: { Cookie: authA.cookie },
|
|
});
|
|
const authenticatedBody = await authenticatedStatus.json().catch(() => ({}));
|
|
record(
|
|
'AUTH-01',
|
|
authA.response.ok
|
|
&& authA.body.authenticated
|
|
&& Boolean(authA.cookie)
|
|
&& authenticatedStatus.ok
|
|
&& authenticatedBody.authenticated,
|
|
`login=${authA.response.status} cookie=${Boolean(authA.cookie)} status_api=${authenticatedStatus.status}`,
|
|
);
|
|
|
|
await stack.restartPortal();
|
|
const statusAfterRestart = await fetch(`${stack.baseUrl}/auth/status`, {
|
|
headers: { Cookie: authA.cookie },
|
|
});
|
|
const restartBody = await statusAfterRestart.json().catch(() => ({}));
|
|
const ghostStatus = await fetch(`${stack.baseUrl}/auth/status`, {
|
|
headers: { Cookie: 'h5_user_session=synthetic-invalid-session' },
|
|
});
|
|
const ghostBody = await ghostStatus.json().catch(() => ({}));
|
|
record(
|
|
'AUTH-03',
|
|
statusAfterRestart.ok
|
|
&& restartBody.authenticated === true
|
|
&& ghostBody.authenticated === false,
|
|
`valid_after_restart=${Boolean(restartBody.authenticated)} invalid_after_restart=${Boolean(ghostBody.authenticated)}`,
|
|
);
|
|
|
|
const syntheticSessionId = `gate-session-${runId}`;
|
|
const sessionConnection = await mysql.createConnection(stack.mysqlUrl);
|
|
try {
|
|
await sessionConnection.execute(
|
|
`INSERT INTO h5_user_sessions
|
|
(agent_session_id, user_id, goosed_node, goosed_target, created_at)
|
|
VALUES (?, ?, 0, NULL, ?)`,
|
|
[syntheticSessionId, userAId, Date.now()],
|
|
);
|
|
} finally {
|
|
await sessionConnection.end();
|
|
}
|
|
const crossUserSession = await fetch(
|
|
`${stack.baseUrl}/api/sessions/${encodeURIComponent(syntheticSessionId)}`,
|
|
{ headers: { Cookie: authB.cookie } },
|
|
);
|
|
|
|
const createPage = await fetch(`${stack.baseUrl}/api/mindspace/v1/pages`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Cookie: authA.cookie,
|
|
},
|
|
body: JSON.stringify({
|
|
title: 'Gate isolation page',
|
|
summary: 'Synthetic local-only fixture',
|
|
content: '<p>release gate</p>',
|
|
template_id: 'article',
|
|
page_type: 'article',
|
|
}),
|
|
});
|
|
const pageBody = await createPage.json().catch(() => ({}));
|
|
const pageId = pageBody?.data?.id;
|
|
const crossUser = pageId
|
|
? await fetch(`${stack.baseUrl}/api/mindspace/v1/pages/${encodeURIComponent(pageId)}`, {
|
|
headers: { Cookie: authB.cookie },
|
|
})
|
|
: null;
|
|
|
|
const categoriesResponse = await fetch(`${stack.baseUrl}/api/mindspace/v1/space/categories`, {
|
|
headers: { Cookie: authA.cookie },
|
|
});
|
|
const categoriesBody = await categoriesResponse.json().catch(() => ({}));
|
|
const uploadCategory = (categoriesBody.data ?? []).find(
|
|
(category) => category.code === 'oa' || category.categoryCode === 'oa',
|
|
) ?? (categoriesBody.data ?? []).find(
|
|
(category) => ['oa', 'public'].includes(category.category_code),
|
|
);
|
|
const uploadContent = Buffer.from('local release gate attachment');
|
|
const createUpload = uploadCategory
|
|
? await fetch(`${stack.baseUrl}/api/mindspace/v1/uploads`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
Cookie: authA.cookie,
|
|
},
|
|
body: JSON.stringify({
|
|
category_id: uploadCategory.id,
|
|
filename: 'gate-note.txt',
|
|
size_bytes: uploadContent.length,
|
|
declared_mime_type: 'text/plain',
|
|
session_id: syntheticSessionId,
|
|
}),
|
|
})
|
|
: null;
|
|
const uploadBody = await createUpload?.json().catch(() => ({}));
|
|
const uploadId = uploadBody?.data?.id;
|
|
const writeUpload = uploadId
|
|
? await fetch(`${stack.baseUrl}/api/mindspace/v1/uploads/${encodeURIComponent(uploadId)}/content`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/octet-stream',
|
|
Cookie: authA.cookie,
|
|
},
|
|
body: uploadContent,
|
|
})
|
|
: null;
|
|
const completeUpload = uploadId && writeUpload?.ok
|
|
? await fetch(`${stack.baseUrl}/api/mindspace/v1/uploads/${encodeURIComponent(uploadId)}/complete`, {
|
|
method: 'POST',
|
|
headers: { Cookie: authA.cookie },
|
|
})
|
|
: null;
|
|
const assetBody = await completeUpload?.json().catch(() => ({}));
|
|
const assetId = assetBody?.data?.id;
|
|
const crossUserAsset = assetId
|
|
? await fetch(`${stack.baseUrl}/api/mindspace/v1/assets/${encodeURIComponent(assetId)}/download`, {
|
|
headers: { Cookie: authB.cookie },
|
|
})
|
|
: null;
|
|
record(
|
|
'AUTH-05',
|
|
crossUserSession.status === 403
|
|
&& createPage.ok
|
|
&& Boolean(pageId)
|
|
&& [403, 404].includes(crossUser?.status)
|
|
&& completeUpload?.status === 201
|
|
&& Boolean(assetId)
|
|
&& [403, 404].includes(crossUserAsset?.status),
|
|
`session=${crossUserSession.status} page_create=${createPage.status} page_cross_user=${crossUser?.status ?? 'not-run'} asset_create=${completeUpload?.status ?? 'not-run'} asset_cross_user=${crossUserAsset?.status ?? 'not-run'}`,
|
|
);
|
|
|
|
const logout = await fetch(`${stack.baseUrl}/auth/logout`, {
|
|
method: 'POST',
|
|
headers: { Cookie: authA.cookie },
|
|
});
|
|
const afterLogout = await fetch(`${stack.baseUrl}/api/me`, {
|
|
headers: { Cookie: authA.cookie },
|
|
});
|
|
record(
|
|
'AUTH-04',
|
|
logout.ok && [401, 403].includes(afterLogout.status),
|
|
`logout=${logout.status} old_cookie=${afterLogout.status}`,
|
|
);
|
|
|
|
failed = checks.some((check) => !check.passed);
|
|
await fs.writeFile(
|
|
path.join(stack.runRoot, 'smoke.json'),
|
|
`${JSON.stringify({ run_id: runId, checks }, null, 2)}\n`,
|
|
);
|
|
} finally {
|
|
await stack.cleanup();
|
|
}
|
|
|
|
if (failed) process.exit(1);
|