43 lines
3.8 KiB
TypeScript
43 lines
3.8 KiB
TypeScript
import { ConvexHttpClient } from 'convex/browser';
|
|
import { makeFunctionReference } from 'convex/server';
|
|
import { ConvexStore } from '../src/lib/server/store';
|
|
import { applicationJwt, hashSecret, randomSecret } from '../src/lib/server/security';
|
|
import assert from 'node:assert/strict';
|
|
import { SignJWT, importPKCS8 } from 'jose';
|
|
|
|
if (process.env.SMOKE_ALLOW_WRITE !== '1') throw new Error('This smoke test writes synthetic records. Set SMOKE_ALLOW_WRITE=1 only for a disposable deployment.');
|
|
const url = process.env.CONVEX_SELF_HOSTED_URL!;
|
|
const store = new ConvexStore(url, process.env.CONVEX_BRIDGE_SECRET!);
|
|
const now = Date.now(); const hash = hashSecret(randomSecret());
|
|
const characterId = 90_000_000 + Math.floor(Math.random() * 1_000_000);
|
|
const user = await store.completeLogin({ characterId, name: 'Disposable Test Pilot', ownerHash: hashSecret(randomSecret()), scopes: ['esi-assets.read_assets.v1'], intent: 'initial_login', capability: 'asset_search', requestedScopes: ['esi-assets.read_assets.v1'], approvedExistingScopes: [], encryptedTokens: 'test-only-encrypted-placeholder', tokenExpiresAt: now + 60_000, sessionHash: hash, sessionExpiresAt: now + 60_000 });
|
|
assert.equal((await store.session(hash))?.userId, user.userId);
|
|
const generation = await store.beginSync(user.userId, characterId);
|
|
await store.stageAssets(user.userId, characterId, generation, 0, [{ itemId: '9223372036854775806', characterId, typeId: 34, quantity: 50, locationId: '60003760', locationType: 'station', locationFlag: 'Hangar' }]);
|
|
await store.finishSync(user.userId, characterId, generation, 1, now, now + 3_600_000);
|
|
assert.equal((await store.assets(user.userId, characterId))[0]!.quantity, 50);
|
|
let isolated = false;
|
|
try { isolated = (await store.assets('other-user', characterId)).length === 0; } catch { isolated = true; }
|
|
assert(isolated);
|
|
const failure = await store.beginSync(user.userId, characterId);
|
|
await store.stageAssets(user.userId, characterId, failure, 0, []);
|
|
await store.failSync(user.userId, characterId, failure, 'test failure');
|
|
assert.equal((await store.assets(user.userId, characterId))[0]!.quantity, 50);
|
|
const client = new ConvexHttpClient(url, { skipConvexDeploymentUrlCheck: true, logger: false });
|
|
client.setAuth(await applicationJwt({ ...user, issuer: process.env.APP_AUTH_ISSUER!, privateKey: process.env.APP_AUTH_PRIVATE_KEY! }));
|
|
const rows = await client.query(makeFunctionReference<'query', Record<string, never>, unknown[] | null>('status:characters'), {});
|
|
assert.equal(rows?.length, 1);
|
|
assert(!JSON.stringify(rows).includes('encryptedTokens'));
|
|
const wrongAudience = new ConvexHttpClient(url, { skipConvexDeploymentUrlCheck: true, logger: false });
|
|
const key = await importPKCS8(Buffer.from(process.env.APP_AUTH_PRIVATE_KEY!, 'base64').toString('utf8'), 'RS256');
|
|
wrongAudience.setAuth(await new SignJWT({ sid: user.sessionId }).setProtectedHeader({ alg: 'RS256', typ: 'JWT', kid: 'app-v1' }).setIssuer(process.env.APP_AUTH_ISSUER!).setSubject(user.userId).setAudience('not-eve-intelligence').setIssuedAt().setExpirationTime('5m').sign(key));
|
|
let wrongAudienceDenied = false;
|
|
try { const rows = await wrongAudience.query(makeFunctionReference<'query', Record<string, never>, unknown[] | null>('status:characters'), {}); wrongAudienceDenied = rows === null; } catch { wrongAudienceDenied = true; }
|
|
assert(wrongAudienceDenied, 'Convex must reject a correctly signed token for another audience');
|
|
await store.deleteSession(hash);
|
|
assert.equal(await store.session(hash), null);
|
|
let revoked = false;
|
|
try { const result = await client.query(makeFunctionReference<'query', Record<string, never>, unknown[] | null>('status:characters'), {}); revoked = result === null || result.length === 0; } catch { revoked = true; }
|
|
assert(revoked);
|
|
console.log('Real self-hosted Convex smoke passed: login/session, snapshot promotion, failure preservation, tenant isolation, JWT-safe projection and revocation.');
|