Spaces:
Sleeping
Sleeping
Download scripts/e2e/lib.mjs from mahrooshishaq/SmartfolioBackend: direct link, hf CLI and curl.
- Browser
- Download file 9.04 kB
-
https://huggingface.co/spaces/mahrooshishaq/SmartfolioBackend/resolve/main/scripts/e2e/lib.mjs
- Command line
-
hf download hf://spaces/mahrooshishaq/SmartfolioBackend/scripts/e2e/lib.mjs
-
curl -L -o lib.mjs https://huggingface.co/spaces/mahrooshishaq/SmartfolioBackend/resolve/main/scripts/e2e/lib.mjs
9.04 kB
| /** | |
| * Shared harness for the campaign + verification end-to-end scripts. | |
| * | |
| * These drive the REAL HTTP API against a REAL Postgres. Nothing here mocks a | |
| * request or stubs a repository: a 201 that wrote no row is a bug the response | |
| * body cannot show, so every script asserts database state as well as status | |
| * codes. `sql()` goes through the same container the app writes to. | |
| * | |
| * Env: | |
| * API_BASE default http://localhost:3001 | |
| * PG_CONTAINER default smartfolio-pg | |
| * BACKEND_LOG path to the dev-server log, for reading mock-mode OTPs | |
| */ | |
| import { execFileSync } from 'node:child_process'; | |
| import { readFileSync } from 'node:fs'; | |
| export const API_BASE = process.env.API_BASE || 'http://localhost:3001'; | |
| const PG_CONTAINER = process.env.PG_CONTAINER || 'smartfolio-pg'; | |
| const PG_DB = process.env.PG_DB || 'smartfolio'; | |
| const PG_USER = process.env.PG_USER || 'postgres'; | |
| export const BACKEND_LOG = process.env.BACKEND_LOG || ''; | |
| /* ---------------------------------------------------------------- output -- */ | |
| const ESC = String.fromCharCode(27); | |
| const C = { | |
| reset: ESC + '[0m', | |
| dim: ESC + '[2m', | |
| red: ESC + '[31m', | |
| green: ESC + '[32m', | |
| cyan: ESC + '[36m', | |
| bold: ESC + '[1m', | |
| }; | |
| let passed = 0; | |
| let failed = 0; | |
| const failures = []; | |
| export function section(title) { | |
| const rule = '-'.repeat(Math.max(0, 62 - title.length)); | |
| console.log('\n' + C.bold + C.cyan + '-- ' + title + ' ' + rule + C.reset); | |
| } | |
| export function check(label, condition, detail = '') { | |
| if (condition) { | |
| passed++; | |
| console.log(' ' + C.green + 'PASS' + C.reset + ' ' + label + (detail ? ' ' + C.dim + detail + C.reset : '')); | |
| } else { | |
| failed++; | |
| failures.push(label); | |
| console.log(' ' + C.red + 'FAIL' + C.reset + ' ' + label + (detail ? ' ' + C.red + detail + C.reset : '')); | |
| } | |
| return condition; | |
| } | |
| export function info(message) { | |
| console.log(' ' + C.dim + '. ' + message + C.reset); | |
| } | |
| export function summary(name) { | |
| const total = passed + failed; | |
| const colour = failed === 0 ? C.green : C.red; | |
| console.log('\n' + C.bold + name + ': ' + colour + passed + '/' + total + ' checks passed' + C.reset); | |
| if (failures.length) { | |
| console.log(C.red + 'failed:' + C.reset); | |
| for (const f of failures) console.log(' - ' + f); | |
| } | |
| process.exitCode = failed === 0 ? 0 : 1; | |
| return failed === 0; | |
| } | |
| /* ------------------------------------------------------------------ http -- */ | |
| /** Fetch against the API. Returns { status, body, headers, setCookie }. */ | |
| export async function api(path, options = {}) { | |
| const { token, cookies, raw, ...init } = options; | |
| const headers = new Headers(init.headers || {}); | |
| if (token) headers.set('Authorization', 'Bearer ' + token); | |
| if (cookies) headers.set('Cookie', cookies); | |
| if (init.body && typeof init.body === 'object' && !(init.body instanceof FormData)) { | |
| if (!headers.has('Content-Type')) headers.set('Content-Type', 'application/json'); | |
| init.body = JSON.stringify(init.body); | |
| } | |
| const res = await fetch(API_BASE + path, { ...init, headers, redirect: 'manual' }); | |
| const setCookie = res.headers.getSetCookie ? res.headers.getSetCookie() : []; | |
| let body = null; | |
| if (!raw) { | |
| const text = await res.text(); | |
| try { | |
| body = text ? JSON.parse(text) : null; | |
| } catch { | |
| body = text; | |
| } | |
| } | |
| return { status: res.status, body, headers: res.headers, setCookie, res }; | |
| } | |
| /** Value of one cookie from a set-cookie array. */ | |
| export function cookieValue(setCookie, name) { | |
| for (const line of setCookie) { | |
| const match = line.match(new RegExp('^' + name + '=([^;]*)')); | |
| if (match) return match[1]; | |
| } | |
| return null; | |
| } | |
| /* -------------------------------------------------------------- database -- */ | |
| /** Run SQL and return rows as arrays of column strings. */ | |
| export function sql(query) { | |
| const out = execFileSync( | |
| 'docker', | |
| ['exec', PG_CONTAINER, 'psql', '-U', PG_USER, '-d', PG_DB, '-tA', '-c', query], | |
| { encoding: 'utf8' }, | |
| ); | |
| return out | |
| .split('\n') | |
| .filter((line) => line.trim() !== '') | |
| .map((line) => line.split('|')); | |
| } | |
| /** First column of the first row, or null. */ | |
| export function sqlOne(query) { | |
| const rows = sql(query); | |
| return rows.length ? rows[0][0] : null; | |
| } | |
| export function sqlEscape(value) { | |
| return String(value).replace(/'/g, "''"); | |
| } | |
| /* ------------------------------------------------------------------ auth -- */ | |
| /** | |
| * Pull the most recent OTP for an address out of the dev-server log. | |
| * | |
| * MAIL_MOCK_MODE prints it there rather than sending mail - which is what makes | |
| * the signup leg of the apply journey testable without a real inbox. | |
| */ | |
| export function otpFromLog(email) { | |
| if (!BACKEND_LOG) throw new Error('BACKEND_LOG is not set - cannot read mock OTPs'); | |
| const log = readFileSync(BACKEND_LOG, 'utf8'); | |
| const blocks = log.split('========== MOCK EMAIL =========='); | |
| for (let i = blocks.length - 1; i >= 0; i--) { | |
| if (!blocks[i].includes(email)) continue; | |
| const match = blocks[i].match(/OTP code is:\s*(\d{4,6})/); | |
| if (match) return match[1]; | |
| } | |
| return null; | |
| } | |
| export const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); | |
| /** | |
| * The interview link sent TO a specific address. | |
| * | |
| * Scoped to the recipient, not "the last one in the file". Two suites running | |
| * in sequence both send invites, and a search over the whole log hands the | |
| * second one the first one's token — which then resolves to somebody else's | |
| * campaign and fails in four confusing ways. | |
| */ | |
| export function inviteTokenFromLog(email) { | |
| if (!BACKEND_LOG) throw new Error('BACKEND_LOG is not set - cannot read mock mail'); | |
| const log = readFileSync(BACKEND_LOG, 'utf8'); | |
| const blocks = log.split('========== MOCK EMAIL =========='); | |
| for (let i = blocks.length - 1; i >= 0; i--) { | |
| if (!blocks[i].includes(email)) continue; | |
| const match = blocks[i].match(/\/interview\/([A-Za-z0-9_-]{20,})/); | |
| if (match) return match[1]; | |
| } | |
| return null; | |
| } | |
| /** | |
| * Create a fully verified account through the real signup -> OTP -> verify flow. | |
| * Returns { email, password, userId, accessToken, refreshToken }. | |
| */ | |
| export async function createVerifiedUser({ name, email, password = 'Password@123' }) { | |
| const signup = await api('/auth/signup', { method: 'POST', body: { name, email, password } }); | |
| if (signup.status >= 400) { | |
| throw new Error('signup failed (' + signup.status + '): ' + JSON.stringify(signup.body)); | |
| } | |
| let otp = null; | |
| for (let i = 0; i < 24 && !otp; i++) { | |
| otp = otpFromLog(email); | |
| if (!otp) await sleep(250); | |
| } | |
| if (!otp) throw new Error('no OTP found in log for ' + email); | |
| const verify = await api('/auth/verify-otp', { method: 'POST', body: { email, otp } }); | |
| if (verify.status >= 400) { | |
| throw new Error('verify-otp failed (' + verify.status + '): ' + JSON.stringify(verify.body)); | |
| } | |
| const userId = sqlOne("select id from \"user\" where email = '" + sqlEscape(email) + "'"); | |
| return { | |
| name, | |
| email, | |
| password, | |
| userId, | |
| accessToken: verify.body.accessToken, | |
| refreshToken: verify.body.refreshToken, | |
| }; | |
| } | |
| export async function login(email, password) { | |
| const res = await api('/auth/login', { method: 'POST', body: { email, password } }); | |
| if (res.status >= 400) { | |
| throw new Error('login failed (' + res.status + '): ' + JSON.stringify(res.body)); | |
| } | |
| return res.body.accessToken; | |
| } | |
| /** Promote an account to admin. There is no API for this, by design. */ | |
| export function makeAdmin(email) { | |
| sql("update \"user\" set role = 'admin' where email = '" + sqlEscape(email) + "'"); | |
| return sqlOne("select role from \"user\" where email = '" + sqlEscape(email) + "'"); | |
| } | |
| /** A stable email per run so repeated runs never collide. */ | |
| export function uniqueEmail(prefix) { | |
| return prefix + '+' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6) + '@smartfolio.test'; | |
| } | |
| /** | |
| * Give a candidate a CV with known text. | |
| * | |
| * Applications are scored by reading the CV against the job description, so a | |
| * candidate with no CV has no score — correctly, and the admin list says "Not | |
| * scored". Gates that assert on a score therefore have to provide one. | |
| */ | |
| export function giveCv(userId, text) { | |
| return sqlOne( | |
| 'insert into resumes ("userId", "originalFileName", "filePath", "fileType", "fileSizeBytes", "isExtracted", "extractedText", "fileData") values (' + | |
| "'" + userId + "', 'cv.pdf', 'db://resume', 'pdf', 1024, true, $q$" + text + "$q$, " + | |
| "decode('255044462d312e340a25','hex')) returning id", | |
| ); | |
| } | |
| export function attachCv(candidateId, userId, text) { | |
| const resumeId = sqlOne( | |
| 'insert into resumes ("userId", "originalFileName", "filePath", "fileType", "fileSizeBytes", "isExtracted", "extractedText", "fileData") values (' + | |
| "'" + userId + "', 'cv.pdf', 'db://resume', 'pdf', 1024, true, $q$" + text + "$q$, " + | |
| "decode('255044462d312e340a25','hex')) returning id", | |
| ); | |
| sql(`update campaign_candidates set "resumeId" = '${resumeId}' where id = '${candidateId}'`); | |
| return resumeId; | |
| } | |