test(harness): testUsers helpers — registerLocal + createFederatedUser via production endpoints
This commit is contained in:
@@ -0,0 +1,133 @@
|
|||||||
|
import crypto from 'node:crypto';
|
||||||
|
import type { SpawnedInstance } from './twoInstanceHarness.js';
|
||||||
|
|
||||||
|
export interface TestUser {
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
token: string;
|
||||||
|
origin: string; // origin where this token authenticates
|
||||||
|
homeUserId?: string; // present for federated users — the id on the user's home instance
|
||||||
|
homeInstance?: string; // present for federated users
|
||||||
|
}
|
||||||
|
|
||||||
|
const nano = () => crypto.randomBytes(4).toString('hex');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a native local user on `instance` via POST /api/auth/register.
|
||||||
|
* Returns the user record + JWT.
|
||||||
|
*
|
||||||
|
* Endpoint contract (verified against packages/server/src/routes/auth.ts):
|
||||||
|
* - Request body: { username, password, displayName?, avatarColor?, homeInstance?, homeUserId? }
|
||||||
|
* - Response 201: { token, user: { id, ... } }
|
||||||
|
*/
|
||||||
|
export async function registerLocal(instance: SpawnedInstance, baseName = 't'): Promise<TestUser> {
|
||||||
|
// Username validator (auth.ts): /^[a-z0-9_]+$/, length 3-32. Lowercase the
|
||||||
|
// baseName and use underscore separator so callers can pass readable names
|
||||||
|
// like "probe-local" without tripping the validator.
|
||||||
|
const safeBase = baseName.toLowerCase().replace(/[^a-z0-9_]/g, '_');
|
||||||
|
const username = `${safeBase}_${nano()}`;
|
||||||
|
const password = `pw_${crypto.randomBytes(8).toString('hex')}`;
|
||||||
|
const res = await fetch(`${instance.origin}/api/auth/register`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ username, password, displayName: username }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const txt = await res.text();
|
||||||
|
throw new Error(`registerLocal failed (${instance.origin}): ${res.status} ${txt}`);
|
||||||
|
}
|
||||||
|
const data = await res.json() as { user: { id: string }; token: string };
|
||||||
|
return {
|
||||||
|
id: data.user.id,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
token: data.token,
|
||||||
|
origin: instance.origin,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a federated account: register `home` first (native), then create the
|
||||||
|
* `username@homeDomain` mirror on `remote` via POST /api/auth/register with
|
||||||
|
* homeInstance + homeUserId — the production federated-account-creation path
|
||||||
|
* documented in client-federation.md §1. Returns BOTH user records.
|
||||||
|
*
|
||||||
|
* Also seeds the home's user_federation_registry via PUT
|
||||||
|
* /api/users/@me/federation-registry so the home's federation-identity
|
||||||
|
* delete endpoint sees this remote in the user's registry.
|
||||||
|
*
|
||||||
|
* Registry PUT contract (verified against packages/server/src/routes/users.ts):
|
||||||
|
* - Body: { registry: Array<{...}>, updatedAt: number }
|
||||||
|
* NOTE: the route's array key is `registry`, NOT `entries` as the original
|
||||||
|
* plan template suggested. The deviation has been applied here.
|
||||||
|
* - Per-entry required: { origin: string, status: 'connected'|'disconnected'|'unreachable'|'auth_expired', addedAt: number }
|
||||||
|
* - Per-entry optional: { label, username, remoteUserId, lastConnectedAt, disconnectedAt, errorMessage }
|
||||||
|
* - LWW guard: updatedAt must be > stored federationRegistryUpdatedAt (else 409).
|
||||||
|
*/
|
||||||
|
export async function createFederatedUser(
|
||||||
|
home: SpawnedInstance,
|
||||||
|
remote: SpawnedInstance,
|
||||||
|
baseName = 't',
|
||||||
|
): Promise<{ homeUser: TestUser; remoteUser: TestUser }> {
|
||||||
|
const homeUser = await registerLocal(home, baseName);
|
||||||
|
const federatedUsername = `${homeUser.username}@${home.domain}`;
|
||||||
|
|
||||||
|
const res = await fetch(`${remote.origin}/api/auth/register`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: federatedUsername,
|
||||||
|
password: homeUser.password,
|
||||||
|
displayName: homeUser.username,
|
||||||
|
homeInstance: home.domain,
|
||||||
|
homeUserId: homeUser.id,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const txt = await res.text();
|
||||||
|
throw new Error(`createFederatedUser remote register failed: ${res.status} ${txt}`);
|
||||||
|
}
|
||||||
|
const data = await res.json() as { user: { id: string }; token: string };
|
||||||
|
|
||||||
|
const remoteUser: TestUser = {
|
||||||
|
id: data.user.id,
|
||||||
|
username: federatedUsername,
|
||||||
|
password: homeUser.password,
|
||||||
|
token: data.token,
|
||||||
|
origin: remote.origin,
|
||||||
|
homeUserId: homeUser.id,
|
||||||
|
homeInstance: home.domain,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Seed the home user's federation registry. Body shape matches the route's
|
||||||
|
// declared Body<{ registry, updatedAt }> — see users.ts L591-L594.
|
||||||
|
const registryUpdatedAt = Date.now();
|
||||||
|
const registryRes = await fetch(`${home.origin}/api/users/@me/federation-registry`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Bearer ${homeUser.token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
updatedAt: registryUpdatedAt,
|
||||||
|
registry: [
|
||||||
|
{
|
||||||
|
origin: remote.origin,
|
||||||
|
label: remote.domain,
|
||||||
|
username: federatedUsername,
|
||||||
|
remoteUserId: remoteUser.id,
|
||||||
|
status: 'connected',
|
||||||
|
addedAt: registryUpdatedAt,
|
||||||
|
lastConnectedAt: registryUpdatedAt,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
if (!registryRes.ok) {
|
||||||
|
const txt = await registryRes.text();
|
||||||
|
throw new Error(`registry sync failed: ${registryRes.status} ${txt}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { homeUser, remoteUser };
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user