mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Merge pull request #1906 from PR0M3TH3AN/ai/smoke-tests-20260128-13931457920280615023
chore(ai): add smoke test script (agent)
This commit is contained in:
2 files changed
+355
-237
No files matched your search
@@ -68,6 +68,10 @@ export function startRelay(port = 8889) {
|
||||
if (typeof subId === "string") {
|
||||
subs.get(ws).delete(subId);
|
||||
}
|
||||
} else if (type === "COUNT") {
|
||||
const subId = payload[0];
|
||||
// Dummy count support
|
||||
ws.send(JSON.stringify(["COUNT", subId, { count: 0 }]));
|
||||
}
|
||||
} catch (err) {
|
||||
// console.error("Relay error processing message:", err);
|
||||
|
||||
+351
-237
@@ -1,286 +1,400 @@
|
||||
import './setup-test-env.js';
|
||||
import { indexedDB, IDBKeyRange } from "fake-indexeddb";
|
||||
import { chromium } from 'playwright';
|
||||
import { WebSocket } from 'ws';
|
||||
import * as NostrTools from 'nostr-tools';
|
||||
import { startRelay } from './load-test-relay.mjs';
|
||||
import { decryptDM } from '../../js/dmDecryptor.js';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// --- Polyfills ---
|
||||
if (!global.indexedDB) {
|
||||
global.indexedDB = indexedDB;
|
||||
}
|
||||
if (!global.IDBKeyRange) {
|
||||
global.IDBKeyRange = IDBKeyRange;
|
||||
}
|
||||
if (!global.NostrTools) {
|
||||
global.NostrTools = NostrTools;
|
||||
}
|
||||
// setup-test-env.js handles WebSocket, crypto, localStorage, window, self, navigator
|
||||
|
||||
// --- Imports from Codebase ---
|
||||
// Note: We need to use relative paths from scripts/agent/
|
||||
import { NostrClient } from '../../js/nostr/client.js';
|
||||
import { decryptDM } from '../../js/dmDecryptor.js';
|
||||
import { startRelay } from './load-test-relay.mjs';
|
||||
|
||||
// --- Configuration ---
|
||||
const ARTIFACTS_DIR = 'artifacts';
|
||||
if (!fs.existsSync(ARTIFACTS_DIR)) {
|
||||
fs.mkdirSync(ARTIFACTS_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
const RELAY_PORT = 8890;
|
||||
const SCREENSHOTS_DIR = path.join(ARTIFACTS_DIR, 'screenshots');
|
||||
const RELAY_PORT = 8889;
|
||||
const RELAY_URL = `ws://localhost:${RELAY_PORT}`;
|
||||
const HTTP_PORT = 8000;
|
||||
const HTTP_URL = `http://localhost:${HTTP_PORT}`;
|
||||
|
||||
// --- Logging ---
|
||||
const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, ''); // YYYYMMDD
|
||||
const dateStr = new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
||||
const logFile = path.join(ARTIFACTS_DIR, `smoke-${dateStr}.log`);
|
||||
|
||||
if (!fs.existsSync(ARTIFACTS_DIR)) fs.mkdirSync(ARTIFACTS_DIR, { recursive: true });
|
||||
if (!fs.existsSync(SCREENSHOTS_DIR)) fs.mkdirSync(SCREENSHOTS_DIR, { recursive: true });
|
||||
|
||||
const logStream = fs.createWriteStream(logFile, { flags: 'a' });
|
||||
|
||||
function log(msg, ...args) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const message = `[${timestamp}] ${msg} ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : a).join(' ')}`;
|
||||
console.log(message);
|
||||
logStream.write(message + '\n');
|
||||
const timestamp = new Date().toISOString();
|
||||
const message = `[${timestamp}] ${msg} ${args.map(a => typeof a === 'object' ? JSON.stringify(a) : a).join(' ')}`;
|
||||
console.log(message);
|
||||
logStream.write(message + '\n');
|
||||
}
|
||||
|
||||
function error(msg, ...args) {
|
||||
const timestamp = new Date().toISOString();
|
||||
const message = `[${timestamp}] [ERROR] ${msg} ${args.map(a => (a instanceof Error ? a.stack : (typeof a === 'object' ? JSON.stringify(a) : a))).join(' ')}`;
|
||||
console.error(message);
|
||||
logStream.write(message + '\n');
|
||||
const timestamp = new Date().toISOString();
|
||||
const message = `[${timestamp}] [ERROR] ${msg} ${args.map(a => (a instanceof Error ? a.stack : (typeof a === 'object' ? JSON.stringify(a) : a))).join(' ')}`;
|
||||
console.error(message);
|
||||
logStream.write(message + '\n');
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
async function waitForServer(url, timeoutMs = 10000) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (res.ok) return true;
|
||||
} catch (e) {
|
||||
// ignore
|
||||
async function waitForServer(url, timeoutMs = 15000) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (res.ok) return true;
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
}
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
}
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Main Test Flow ---
|
||||
// --- Main Script ---
|
||||
async function run() {
|
||||
let relayServer = null;
|
||||
let httpServer = null;
|
||||
let client = null;
|
||||
let success = true;
|
||||
let relayServer = null;
|
||||
let httpServer = null;
|
||||
let browser = null;
|
||||
let success = true;
|
||||
|
||||
try {
|
||||
log('--- Starting Smoke Test ---');
|
||||
try {
|
||||
log('--- Starting Smoke Test ---');
|
||||
|
||||
// 1. Start Relay
|
||||
log(`Starting local relay on port ${RELAY_PORT}...`);
|
||||
relayServer = startRelay(RELAY_PORT);
|
||||
// Wait a bit for relay to be ready (startRelay is synchronous in setup but async in effect?)
|
||||
// It uses WebSocketServer which starts immediately.
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
log('Relay started.');
|
||||
// 1. Start Relay
|
||||
log(`Starting local relay on port ${RELAY_PORT}...`);
|
||||
relayServer = startRelay(RELAY_PORT);
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
log('Relay started.');
|
||||
|
||||
// 2. Start HTTP Server
|
||||
log(`Starting HTTP server (npx serve) on port ${HTTP_PORT}...`);
|
||||
httpServer = spawn('npx', ['serve', '-p', String(HTTP_PORT)], {
|
||||
stdio: 'ignore', // or 'inherit' to see output
|
||||
detached: false
|
||||
});
|
||||
// 2. Start HTTP Server
|
||||
log(`Starting HTTP server on port ${HTTP_PORT}...`);
|
||||
httpServer = spawn('npx', ['serve', '-p', String(HTTP_PORT)], {
|
||||
stdio: 'ignore',
|
||||
detached: false,
|
||||
shell: true
|
||||
});
|
||||
|
||||
const serverReady = await waitForServer(HTTP_URL);
|
||||
if (!serverReady) {
|
||||
throw new Error(`HTTP server did not start on ${HTTP_URL} within timeout.`);
|
||||
}
|
||||
log('HTTP server is ready.');
|
||||
const serverReady = await waitForServer(HTTP_URL);
|
||||
if (!serverReady) throw new Error(`HTTP server failed to start at ${HTTP_URL}`);
|
||||
log('HTTP server ready.');
|
||||
|
||||
// 3. Init NostrClient
|
||||
log('Initializing NostrClient...');
|
||||
client = new NostrClient();
|
||||
client.relays = [RELAY_URL];
|
||||
client.readRelays = [RELAY_URL];
|
||||
client.writeRelays = [RELAY_URL];
|
||||
// 3. Launch Browser
|
||||
log('Launching browser...');
|
||||
browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext();
|
||||
const page = await context.newPage();
|
||||
|
||||
// Override dev logger to avoid pollution if needed, or let it log
|
||||
// We can rely on standard logging.
|
||||
page.on('console', msg => {
|
||||
const type = msg.type();
|
||||
const text = msg.text();
|
||||
if (type === 'error' || type === 'warning') {
|
||||
log(`[BROWSER ${type.toUpperCase()}] ${text}`);
|
||||
}
|
||||
});
|
||||
page.on('pageerror', err => {
|
||||
log(`[BROWSER UNCAUGHT] ${err.message}`);
|
||||
});
|
||||
|
||||
await client.init();
|
||||
log(`NostrClient initialized and connected to ${RELAY_URL}`);
|
||||
// 4. Mock Extension Setup
|
||||
const ephemeralSk = NostrTools.generateSecretKey();
|
||||
const ephemeralSkHex = Buffer.from(ephemeralSk).toString('hex'); // nostr-tools v2 uses Uint8Array, Buffer covers it
|
||||
const ephemeralPk = NostrTools.getPublicKey(ephemeralSk);
|
||||
log(`Ephemeral Pubkey: ${ephemeralPk}`);
|
||||
|
||||
// 4. Login (Ephemeral)
|
||||
log('Generating ephemeral keys for Sender...');
|
||||
const senderSk = NostrTools.generateSecretKey();
|
||||
const senderSkHex = Buffer.from(senderSk).toString('hex');
|
||||
const senderPk = NostrTools.getPublicKey(senderSk);
|
||||
// Pre-seed Relay with User's Relay List (Kind 10002)
|
||||
// This ensures the app finds the local relay when it syncs preferences, avoiding fallback to public defaults.
|
||||
{
|
||||
log('Seeding relay list event...');
|
||||
const seedWs = new WebSocket(RELAY_URL);
|
||||
await new Promise((resolve) => seedWs.on('open', resolve));
|
||||
|
||||
log(`Sender Pubkey: ${senderPk}`);
|
||||
const relayListEvent = {
|
||||
kind: 10002,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [['r', RELAY_URL]],
|
||||
content: '',
|
||||
pubkey: ephemeralPk,
|
||||
id: null,
|
||||
sig: null
|
||||
};
|
||||
const signedSeed = NostrTools.finalizeEvent(relayListEvent, ephemeralSk);
|
||||
|
||||
await client.registerPrivateKeySigner({ privateKey: senderSkHex, persist: false });
|
||||
client.pubkey = senderPk; // Manually set logged-in user
|
||||
const seedPromise = new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('Seed timeout')), 2000);
|
||||
seedWs.on('message', (data) => {
|
||||
const msg = JSON.parse(data.toString());
|
||||
if (msg[0] === 'OK' && msg[1] === signedSeed.id && msg[2]) {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
seedWs.send(JSON.stringify(['EVENT', signedSeed]));
|
||||
});
|
||||
|
||||
if (client.pubkey !== senderPk) {
|
||||
throw new Error(`Client pubkey mismatch. Expected ${senderPk}, got ${client.pubkey}`);
|
||||
}
|
||||
log('Sender logged in.');
|
||||
|
||||
// 5. Publish Video
|
||||
log('Publishing test video...');
|
||||
const videoPayload = {
|
||||
title: `Smoke Test Video ${Date.now()}`,
|
||||
description: 'Smoke test description',
|
||||
magnet: `magnet:?xt=urn:btih:${Math.random().toString(16).slice(2)}`,
|
||||
url: `${HTTP_URL}/test.mp4`, // Fake URL
|
||||
thumbnail: `${HTTP_URL}/thumb.jpg`,
|
||||
mode: 'live',
|
||||
videoRootId: `smoke-${Date.now()}`,
|
||||
isNsfw: false,
|
||||
isForKids: true
|
||||
};
|
||||
|
||||
const publishedEvent = await client.publishVideo(videoPayload, senderPk);
|
||||
if (!publishedEvent || !publishedEvent.id) {
|
||||
throw new Error('Failed to publish video event.');
|
||||
}
|
||||
log(`Video published. Event ID: ${publishedEvent.id}`);
|
||||
|
||||
// Verify Read Back
|
||||
log('Verifying video can be fetched...');
|
||||
// We use subscribeVideos for main feed, or getEventById.
|
||||
// Let's use getEventById for direct verification.
|
||||
const fetched = await client.getEventById(publishedEvent.id);
|
||||
if (!fetched) {
|
||||
throw new Error('Failed to fetch published video.');
|
||||
}
|
||||
if (fetched.title !== videoPayload.title) {
|
||||
throw new Error(`Fetched video title mismatch. Expected "${videoPayload.title}", got "${fetched.title}"`);
|
||||
}
|
||||
log('Video fetched successfully.');
|
||||
|
||||
// 6. DM Flow
|
||||
log('Testing DM Flow...');
|
||||
const recipientSk = NostrTools.generateSecretKey();
|
||||
const recipientSkHex = Buffer.from(recipientSk).toString('hex');
|
||||
const recipientPk = NostrTools.getPublicKey(recipientSk);
|
||||
const recipientNpub = NostrTools.nip19.npubEncode(recipientPk);
|
||||
|
||||
log(`Recipient Pubkey: ${recipientPk}`);
|
||||
|
||||
const dmMessage = `Smoke Test Message ${Date.now()}`;
|
||||
|
||||
// Send DM
|
||||
const sendResult = await client.sendDirectMessage(recipientNpub, dmMessage);
|
||||
if (!sendResult.ok) {
|
||||
throw new Error(`Failed to send DM: ${sendResult.error}`);
|
||||
}
|
||||
log('DM sent successfully.');
|
||||
|
||||
// Verify Decryption (Simulate Recipient)
|
||||
log('Verifying DM decryption (Recipient view)...');
|
||||
|
||||
// We need to find the DM event.
|
||||
// Since we don't have a second client syncing, we can manually query the pool/relay.
|
||||
const filter = {
|
||||
kinds: [4, 1059], // 4 for legacy, 1059 for NIP-17
|
||||
'#p': [recipientPk],
|
||||
limit: 1
|
||||
};
|
||||
|
||||
// wait a moment for propagation
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
const events = await client.pool.list([RELAY_URL], [filter]);
|
||||
if (events.length === 0) {
|
||||
throw new Error('Recipient could not find DM event on relay.');
|
||||
}
|
||||
|
||||
const dmEvent = events[0];
|
||||
log(`Found DM event kind ${dmEvent.kind} id ${dmEvent.id}`);
|
||||
|
||||
// Construct decryption context for decryptDM
|
||||
// We need a decryptor for the recipient.
|
||||
// Since we have the recipient's private key, we can use NostrTools.nip04 or nip44.
|
||||
|
||||
const decryptors = [];
|
||||
|
||||
// NIP-04 Decryptor
|
||||
decryptors.push({
|
||||
scheme: 'nip04',
|
||||
decrypt: async (remotePubkey, ciphertext) => {
|
||||
return await NostrTools.nip04.decrypt(recipientSkHex, remotePubkey, ciphertext);
|
||||
await seedPromise;
|
||||
seedWs.close();
|
||||
log('Relay list seeded successfully.');
|
||||
}
|
||||
});
|
||||
|
||||
// NIP-44 Decryptor
|
||||
decryptors.push({
|
||||
scheme: 'nip44',
|
||||
supportsGiftWrap: true,
|
||||
decrypt: (remotePubkey, ciphertext) => {
|
||||
const conversationKey = NostrTools.nip44.v2.utils.getConversationKey(recipientSk, remotePubkey);
|
||||
return NostrTools.nip44.v2.decrypt(ciphertext, conversationKey);
|
||||
// Disable whitelist mode & Inject Relay Preferences
|
||||
await page.addInitScript(({ pubkey, relayUrl }) => {
|
||||
window.localStorage.setItem('bitvid_admin_whitelist_mode', 'false');
|
||||
const key = `bitvid:profile:${pubkey}:relayList:v1`;
|
||||
const data = [{ url: relayUrl, mode: 'both' }];
|
||||
window.localStorage.setItem(key, JSON.stringify(data));
|
||||
}, { pubkey: ephemeralPk, relayUrl: RELAY_URL });
|
||||
|
||||
// Expose Node-side signing to Browser
|
||||
await page.exposeFunction('mockGetPublicKey', async () => ephemeralPk);
|
||||
await page.exposeFunction('mockSignEvent', async (event) => {
|
||||
const signed = NostrTools.finalizeEvent(event, ephemeralSk);
|
||||
return signed;
|
||||
});
|
||||
await page.exposeFunction('mockNip04Encrypt', async (pubkey, plaintext) => {
|
||||
return await NostrTools.nip04.encrypt(ephemeralSk, pubkey, plaintext);
|
||||
});
|
||||
await page.exposeFunction('mockNip04Decrypt', async (pubkey, ciphertext) => {
|
||||
return await NostrTools.nip04.decrypt(ephemeralSk, pubkey, ciphertext);
|
||||
});
|
||||
// Add NIP-44 if needed, mostly NIP-07 uses nip04 for legacy support or nip44
|
||||
|
||||
await page.addInitScript(() => {
|
||||
window.nostr = {
|
||||
getPublicKey: () => window.mockGetPublicKey(),
|
||||
signEvent: (event) => window.mockSignEvent(event),
|
||||
nip04: {
|
||||
encrypt: (pubkey, plaintext) => window.mockNip04Encrypt(pubkey, plaintext),
|
||||
decrypt: (pubkey, ciphertext) => window.mockNip04Decrypt(pubkey, ciphertext)
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// 5. Navigate and Login
|
||||
log('Navigating to app...');
|
||||
await page.goto(HTTP_URL);
|
||||
|
||||
// Wait for app to initialize (fade-in or login button)
|
||||
await page.waitForSelector('#loginButton');
|
||||
log('App loaded.');
|
||||
|
||||
// Override relays in the app to point to local relay?
|
||||
// Ideally we should via UI, but simpler to just let it connect to defaults AND our local relay.
|
||||
// Or we can inject settings.
|
||||
// Let's assume the app will connect to default relays. We want it to use OUR relay for the test.
|
||||
// We can execute: window.app.nostrClient.addRelay(...) if we could access it.
|
||||
// Or we can just let it fail to connect to public relays and hope it doesn't block UI.
|
||||
// BUT, for the publish verification to work, the app needs to publish to a relay we can read.
|
||||
// So we MUST ensure the app connects to localhost:8889.
|
||||
// We can try to inject it into localStorage before load?
|
||||
// 'nostr-relays' key?
|
||||
// Let's check `relayManager.js` or `localStorage` keys.
|
||||
// Usually `nostr:relays` or similar.
|
||||
// Assuming the app has a default list.
|
||||
// We can try to add the relay via console after load.
|
||||
// "window.nostrClient" might be available if exposed?
|
||||
// Earlier I saw `js/index.js` doesn't expose it.
|
||||
// BUT `js/nostrClientFacade.js` exports `nostrClient`.
|
||||
// Maybe I can rely on the app logic: if I publish, it goes to "write relays".
|
||||
// I'll try to use the "Manage Storage" or similar? No.
|
||||
// Let's try to infer if I can just assume it works or if I need to force it.
|
||||
// Actually, for smoke tests, we often want to test against a controlled relay.
|
||||
// I will try to inject a relay into the default set if possible.
|
||||
// `js/relayManager.js` might read from storage.
|
||||
// Let's try to set `nostr:relays` in localStorage before load.
|
||||
// Or I can just continue and see if it works (maybe defaults include localhost in dev mode?).
|
||||
// If not, I'll use `page.evaluate` to find a way.
|
||||
|
||||
// Login
|
||||
await page.click('#loginButton');
|
||||
await page.waitForSelector('[data-provider-id="nip07"]', { timeout: 10000 });
|
||||
log('Login modal open.');
|
||||
|
||||
await page.click('[data-provider-id="nip07"]');
|
||||
log('Clicked NIP-07 Login.');
|
||||
|
||||
// Wait for login success (Upload button appears)
|
||||
await page.waitForSelector('#uploadButton', { state: 'visible', timeout: 10000 });
|
||||
log('Logged in successfully.');
|
||||
|
||||
// 6. Publish Video
|
||||
log('Publishing video...');
|
||||
await page.click('#uploadButton');
|
||||
await page.waitForSelector('#uploadModal', { state: 'visible' });
|
||||
|
||||
await page.click('#btn-mode-external');
|
||||
|
||||
const testTitle = `Smoke Test Video ${Date.now()}`;
|
||||
await page.fill('#input-title', testTitle);
|
||||
await page.fill('#input-description', 'This is a smoke test video.');
|
||||
await page.fill('#input-url', 'https://example.com/test.mp4'); // Fake HTTPS URL
|
||||
|
||||
await page.click('#btn-submit');
|
||||
log('Submit clicked.');
|
||||
|
||||
// Wait for modal to close OR continue if it takes too long (public relay timeouts)
|
||||
try {
|
||||
await page.waitForSelector('#uploadModal', { state: 'hidden', timeout: 5000 });
|
||||
log('Upload modal closed.');
|
||||
} catch (e) {
|
||||
log('Upload modal did not close quickly (likely waiting for public relays), proceeding to verify content...');
|
||||
}
|
||||
});
|
||||
|
||||
const context = {
|
||||
actorPubkey: recipientPk,
|
||||
decryptors
|
||||
};
|
||||
// Verify Video in Feed
|
||||
// It might take a moment to appear.
|
||||
// The app connects to relays. If it didn't connect to OUR relay, we might not see it if we are checking via Node.
|
||||
// But the Browser is checking via the App's UI.
|
||||
// The App needs to be connected to a relay where it published.
|
||||
// If the App defaults to damus.io etc, and fails to connect (no internet in sandbox?), it might fail.
|
||||
// Sandbox has internet.
|
||||
// But I want it to use local relay.
|
||||
// I'll try to add the local relay via UI? Or just hope.
|
||||
// Wait, if I cannot force the relay, the test is flaky.
|
||||
// I'll add a step to force the relay connection via `page.evaluate` leveraging the module system if possible?
|
||||
// Or just `localStorage` key `relays`?
|
||||
// Let's try to set `localStorage.setItem('relays', JSON.stringify([{url:'ws://localhost:8889', read:true, write:true}]))` before load.
|
||||
|
||||
const decryptResult = await decryptDM(dmEvent, context);
|
||||
// Reloading with relay config
|
||||
// Actually, let's do this before the first navigation or reload.
|
||||
// I'll just clear local storage and set the relay.
|
||||
/*
|
||||
await page.goto(HTTP_URL);
|
||||
await page.evaluate((url) => {
|
||||
// Guessing the storage key.
|
||||
// If I don't know it, I might be stuck.
|
||||
// But usually apps respect NIP-07 `getRelays`?
|
||||
// If I implement `window.nostr.getRelays`, the app might use them!
|
||||
}, RELAY_URL);
|
||||
*/
|
||||
|
||||
if (!decryptResult.ok) {
|
||||
throw new Error(`DM decryption failed: ${JSON.stringify(decryptResult.errors)}`);
|
||||
// Implementation of getRelays in mock
|
||||
await page.addInitScript((relayUrl) => {
|
||||
const existing = window.nostr || {};
|
||||
window.nostr = {
|
||||
...existing,
|
||||
getRelays: async () => {
|
||||
return { [relayUrl]: { read: true, write: true } };
|
||||
}
|
||||
};
|
||||
}, RELAY_URL);
|
||||
// App likely calls `getRelays` on login.
|
||||
|
||||
log('Verifying video in UI...');
|
||||
// We might need to refresh or go to profile/feed.
|
||||
// Let's wait for the title to appear in the body.
|
||||
await page.waitForSelector(`text="${testTitle}"`, { timeout: 20000 });
|
||||
log('Video found in UI.');
|
||||
|
||||
// 7. DM Test (Node Side)
|
||||
log('Starting DM Test...');
|
||||
|
||||
// Connect Node Client to Relay
|
||||
const ws = new WebSocket(RELAY_URL);
|
||||
await new Promise((resolve) => ws.on('open', resolve));
|
||||
log('Node client connected to relay.');
|
||||
|
||||
const senderSk = NostrTools.generateSecretKey();
|
||||
const senderPk = NostrTools.getPublicKey(senderSk);
|
||||
const dmMessage = `Smoke DM ${Date.now()}`;
|
||||
|
||||
// Send DM (Kind 4 for simplicity, or 1059)
|
||||
// Using Kind 4 (Legacy) as it's easier to verify without complex wrapping if not strictly required.
|
||||
// But `dmDecryptor` supports both.
|
||||
// Let's send Kind 4.
|
||||
const ciphertext = await NostrTools.nip04.encrypt(senderSk, ephemeralPk, dmMessage);
|
||||
const dmEvent = {
|
||||
kind: 4,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [['p', ephemeralPk]],
|
||||
content: ciphertext,
|
||||
pubkey: senderPk
|
||||
};
|
||||
const signedDm = NostrTools.finalizeEvent(dmEvent, senderSk);
|
||||
|
||||
log('Sending DM...');
|
||||
ws.send(JSON.stringify(['EVENT', signedDm]));
|
||||
|
||||
// Wait for it to be echoed (confirmation it was accepted)
|
||||
// And wait for the "Recipient" (which we simulate using the code) to potentially see it.
|
||||
// Actually, we want to verify we can DECRYPT it using the ephemeral keys we generated for the BROWSER user.
|
||||
// So we act as the Browser User here in Node logic.
|
||||
|
||||
// We need to fetch the event back from the relay to ensure it was stored.
|
||||
// Or we can just use `signedDm` directly to test `decryptDM`.
|
||||
// But better to fetch it to prove relay roundtrip.
|
||||
|
||||
const fetchPromise = new Promise((resolve, reject) => {
|
||||
const subId = 'sub-dm';
|
||||
ws.on('message', (data) => {
|
||||
const msg = JSON.parse(data.toString());
|
||||
if (msg[0] === 'EVENT' && msg[2].id === signedDm.id) {
|
||||
resolve(msg[2]);
|
||||
}
|
||||
});
|
||||
ws.send(JSON.stringify(['REQ', subId, { ids: [signedDm.id] }]));
|
||||
});
|
||||
|
||||
const fetchedDm = await Promise.race([
|
||||
fetchPromise,
|
||||
new Promise((_, r) => setTimeout(() => r(new Error('DM fetch timeout')), 5000))
|
||||
]);
|
||||
|
||||
log('DM fetched from relay.');
|
||||
|
||||
// Verify Decryption
|
||||
log('Decrypting DM...');
|
||||
const decryptors = [
|
||||
{
|
||||
scheme: 'nip04',
|
||||
decrypt: async (remotePubkey, text) => {
|
||||
return await NostrTools.nip04.decrypt(ephemeralSk, remotePubkey, text); // Use Browser User's SK
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const dmContext = {
|
||||
actorPubkey: ephemeralPk,
|
||||
decryptors
|
||||
};
|
||||
|
||||
const result = await decryptDM(fetchedDm, dmContext);
|
||||
|
||||
if (!result.ok) throw new Error(`Decryption failed: ${JSON.stringify(result.errors)}`);
|
||||
if (result.plaintext !== dmMessage) throw new Error(`Plaintext mismatch. Expected ${dmMessage}, got ${result.plaintext}`);
|
||||
|
||||
log('DM Decrypted successfully.');
|
||||
ws.close();
|
||||
|
||||
} catch (err) {
|
||||
success = false;
|
||||
error('Smoke Test Failed:', err);
|
||||
if (browser) {
|
||||
const page = browser.contexts()[0]?.pages()[0];
|
||||
if (page) {
|
||||
const screenshotPath = path.join(SCREENSHOTS_DIR, `failure-${Date.now()}.png`);
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||
log(`Screenshot saved to ${screenshotPath}`);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// Teardown
|
||||
log('Teardown...');
|
||||
if (browser) await browser.close();
|
||||
if (httpServer) httpServer.kill(); // .kill() sends SIGTERM
|
||||
if (relayServer) relayServer.close();
|
||||
|
||||
const summary = {
|
||||
timestamp: new Date().toISOString(),
|
||||
success,
|
||||
logFile
|
||||
};
|
||||
fs.writeFileSync(path.join(ARTIFACTS_DIR, `smoke-summary-${dateStr}.json`), JSON.stringify(summary, null, 2));
|
||||
log('Done.');
|
||||
process.exit(success ? 0 : 1);
|
||||
}
|
||||
|
||||
if (decryptResult.plaintext !== dmMessage) {
|
||||
throw new Error(`DM plaintext mismatch. Expected "${dmMessage}", got "${decryptResult.plaintext}"`);
|
||||
}
|
||||
|
||||
log('DM verified successfully.');
|
||||
|
||||
} catch (err) {
|
||||
success = false;
|
||||
error('Smoke Test Failed!', err);
|
||||
} finally {
|
||||
// Report
|
||||
const summary = {
|
||||
timestamp: new Date().toISOString(),
|
||||
success,
|
||||
logFile
|
||||
};
|
||||
const summaryFile = path.join(ARTIFACTS_DIR, `smoke-summary-${dateStr}.json`);
|
||||
fs.writeFileSync(summaryFile, JSON.stringify(summary, null, 2));
|
||||
log(`Summary saved to ${summaryFile}`);
|
||||
|
||||
// Teardown
|
||||
log('Teardown...');
|
||||
if (client) {
|
||||
// client.logout(); // Cleans up
|
||||
// Close pool connections?
|
||||
// client.pool.close(client.relays); // SimplePool doesn't have explicit close usually?
|
||||
// NostrTools SimplePool usually manages connections.
|
||||
// We can just rely on process exit.
|
||||
}
|
||||
|
||||
if (relayServer) {
|
||||
relayServer.close();
|
||||
log('Relay stopped.');
|
||||
}
|
||||
|
||||
if (httpServer) {
|
||||
httpServer.kill();
|
||||
log('HTTP server stopped.');
|
||||
}
|
||||
|
||||
log('Done.');
|
||||
process.exit(success ? 0 : 1);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
Reference in new issue
Block a user