import { randomUUID } from 'node:crypto'; import { execFileSync, spawnSync } from 'node:child_process'; import { mkdirSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; const baseUrl = process.env.D3RO_E2E_SUPABASE_URL; const anonKey = process.env.D3RO_E2E_ANON_KEY; const serviceKey = process.env.D3RO_E2E_SERVICE_KEY; const maestroBin = process.env.D3RO_MAESTRO_BIN; const androidSerial = process.env.ANDROID_SERIAL ?? 'emulator-5554'; const dbContainer = process.env.D3RO_LOCAL_DB_CONTAINER ?? 'supabase_db_d3ro-voice'; const fixtureOnly = process.env.D3RO_ADMIN_E2E_FIXTURE_ONLY === '1'; const contractProbe = process.env.D3RO_ADMIN_E2E_CONTRACT_PROBE === '1'; const skipOrdinary = process.env.D3RO_ADMIN_E2E_SKIP_ORDINARY === '1'; const skipManager = process.env.D3RO_ADMIN_E2E_SKIP_MANAGER === '1'; const repositoryRoot = resolve(import.meta.dirname, '../../..'); const flowRoot = resolve(repositoryRoot, 'apps/mobile-rn/.maestro'); const outputRoot = resolve( process.env.D3RO_ADMIN_E2E_OUTPUT ?? resolve(repositoryRoot, 'apps/mobile-rn/.maestro-output/admin-local'), ); if (baseUrl !== 'http://127.0.0.1:55321') { throw new Error('D3RO_E2E_SUPABASE_URL must be the disposable local stack at http://127.0.0.1:55321'); } if (!anonKey?.startsWith('sb_publishable_') || !serviceKey?.startsWith('sb_secret_')) { throw new Error('local publishable and secret Supabase keys are required'); } if (!maestroBin) throw new Error('D3RO_MAESTRO_BIN is required'); mkdirSync(outputRoot, { recursive: true }); let assertions = 0; let fixture = null; const cleanupUserIds = new Set(); const evidence = { localOnly: true, endpoint: baseUrl, startedAt: new Date().toISOString(), stages: [], fixtureIds: {}, cleanup: null, }; function assert(condition, message) { if (!condition) throw new Error(`assertion_failed: ${message}`); assertions += 1; } function stage(name, details = {}) { evidence.stages.push({ name, at: new Date().toISOString(), ...details }); console.log(`[admin-mobile-e2e] ${name}`); } function serviceHeaders(extra = {}) { return { apikey: serviceKey, Authorization: `Bearer ${serviceKey}`, 'Content-Type': 'application/json', ...extra, }; } function userHeaders(token, extra = {}) { return { apikey: anonKey, Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', ...extra, }; } async function jsonRequest(path, init = {}, expected = [200]) { const retryableRead = (init.method ?? 'GET') === 'GET'; let response; let lastNetworkError = null; for (let attempt = 1; attempt <= (retryableRead ? 3 : 1); attempt += 1) { try { response = await fetch(`${baseUrl}${path}`, init); break; } catch (error) { lastNetworkError = error; if (attempt < 3) await new Promise((resolvePromise) => setTimeout(resolvePromise, attempt * 200)); } } if (!response) throw lastNetworkError; const text = await response.text(); if (!expected.includes(response.status)) { throw new Error(`${init.method ?? 'GET'} ${path} -> ${response.status}: ${text.slice(0, 400)}`); } return text.length === 0 ? null : JSON.parse(text); } async function createAuthUser(key, role, tier, password, nonce) { const email = `mobile-admin-${key}-${nonce}@example.invalid`; const name = `Admin E2E ${key} ${nonce}`; const account = await jsonRequest('/auth/v1/admin/users', { method: 'POST', headers: serviceHeaders(), body: JSON.stringify({ email, password, email_confirm: true, user_metadata: { name, locale: 'en' }, app_metadata: { role }, }), }, [200, 201]); if (typeof account.id === 'string') cleanupUserIds.add(account.id); assert(typeof account.id === 'string', `${key} auth user was created`); await waitForProfile(account.id); await jsonRequest(`/rest/v1/profiles?id=eq.${account.id}`, { method: 'PATCH', headers: serviceHeaders({ Prefer: 'return=representation' }), body: JSON.stringify({ name, role, tier, locale: 'en' }), }, [200]); await jsonRequest(`/auth/v1/admin/users/${account.id}`, { method: 'PUT', headers: serviceHeaders(), body: JSON.stringify({ app_metadata: { role } }), }, [200]); return { id: account.id, email, name, role, tier }; } async function waitForProfile(userId) { const deadline = Date.now() + 5_000; while (Date.now() < deadline) { const rows = await jsonRequest(`/rest/v1/profiles?id=eq.${userId}&select=id`, { headers: serviceHeaders(), }); if (rows.length === 1) return; await new Promise((resolvePromise) => setTimeout(resolvePromise, 100)); } throw new Error(`profile trigger did not create ${userId}`); } async function setSubscription(userId, tier, credits, note) { const periodStart = new Date(Date.now() - 60_000).toISOString(); const periodEnd = new Date(Date.now() + 86_400_000).toISOString(); const rows = await jsonRequest(`/rest/v1/subscriptions?user_id=eq.${userId}`, { method: 'PATCH', headers: serviceHeaders({ Prefer: 'return=representation' }), body: JSON.stringify({ tier, status: 'active', provider: tier === 'free' ? 'none' : 'admin', payment_provider: 'none', current_period_start: periodStart, current_period_end: periodEnd, cancel_at: null, overage_credits: credits, admin_note: note, }), }, [200]); assert(rows.length === 1 && rows[0].tier === tier, `${userId} subscription tier is ${tier}`); } async function setRole(account, role) { const profiles = await jsonRequest(`/rest/v1/profiles?id=eq.${account.id}`, { method: 'PATCH', headers: serviceHeaders({ Prefer: 'return=representation' }), body: JSON.stringify({ role }), }, [200]); assert(profiles.length === 1 && profiles[0].role === role, `${account.id} profile role is ${role}`); const user = await jsonRequest(`/auth/v1/admin/users/${account.id}`, { method: 'PUT', headers: serviceHeaders(), body: JSON.stringify({ app_metadata: { role } }), }, [200]); assert(user.app_metadata?.role === role, `${account.id} auth role is ${role}`); account.role = role; } async function login(account, password) { const session = await jsonRequest('/auth/v1/token?grant_type=password', { method: 'POST', headers: { apikey: anonKey, 'Content-Type': 'application/json' }, body: JSON.stringify({ email: account.email, password }), }); assert(session.user?.id === account.id, `${account.role} fixture login returned its own account`); return session; } function redacted(value) { if (!fixture) return value; let output = value; for (const account of Object.values(fixture.accounts)) { output = output.replaceAll(account.email, '[fixture-email]'); } return output.replaceAll(fixture.password, '[fixture-password]'); } function runMaestro(flowName, actor, extraEnv = {}) { const flowPath = resolve(flowRoot, flowName); const result = spawnSync(maestroBin, ['test', flowPath], { cwd: outputRoot, env: { ...process.env, ANDROID_SERIAL: androidSerial, MAESTRO_ACTOR_EMAIL: actor.email, MAESTRO_ACTOR_PASSWORD: fixture.password, MAESTRO_TARGET_ID: fixture.accounts.target.id, MAESTRO_TARGET_NAME: fixture.accounts.target.name, ...extraEnv, }, encoding: 'utf8', shell: true, timeout: 600_000, windowsHide: true, }); const combined = redacted(`${result.stdout ?? ''}\n${result.stderr ?? ''}`); writeFileSync(resolve(outputRoot, `${flowName}.log`), combined, 'utf8'); if (result.error) throw result.error; if (result.status !== 0) { throw new Error(`Maestro ${flowName} failed (${result.status}); see sanitized log`); } assert(true, `${flowName} passed`); } function adb(...args) { return execFileSync('adb', ['-s', androidSerial, ...args], { encoding: 'utf8', timeout: 60_000, windowsHide: true, }); } function dumpUi(name) { adb('shell', 'uiautomator', 'dump', '/sdcard/d3ro-admin-e2e.xml'); const xml = adb('exec-out', 'cat', '/sdcard/d3ro-admin-e2e.xml'); writeFileSync(resolve(outputRoot, `${name}.xml`), redacted(xml), 'utf8'); return xml; } function assertRoleOptionPermissions(xml) { for (const [role, expectedEnabled] of [ ['user', 'true'], ['manager', 'true'], ['admin', 'false'], ['super_admin', 'false'], ]) { const node = xml.match(new RegExp(`]*(?:resource-id|content-desc)="[^"]*admin-role-option-${role}[^"]*"[^>]*>`))?.[0]; assert(node !== undefined, `admin ${role} role option is present in the native accessibility tree`); assert(node.includes(`enabled="${expectedEnabled}"`), `admin ${role} role option enabled=${expectedEnabled}`); } } async function subscriptionFor(userId) { const rows = await jsonRequest(`/rest/v1/subscriptions?user_id=eq.${userId}&select=*`, { headers: serviceHeaders(), }); assert(rows.length === 1, `${userId} has exactly one subscription`); return rows[0]; } async function profileFor(userId) { const rows = await jsonRequest(`/rest/v1/profiles?id=eq.${userId}&select=id,role,tier`, { headers: serviceHeaders(), }); assert(rows.length === 1, `${userId} has exactly one profile`); return rows[0]; } function edgeLogsSince(since) { const result = spawnSync('docker', ['logs', '--since', since, 'supabase_edge_runtime_d3ro-voice'], { encoding: 'utf8', timeout: 30_000, windowsHide: true, }); if (result.status !== 0) throw new Error('failed to read local edge runtime logs'); return `${result.stdout ?? ''}\n${result.stderr ?? ''}`; } async function createFixture() { const nonce = `${Date.now()}-${randomUUID().slice(0, 8)}`; const password = `D3roAdminLocal${randomUUID().replaceAll('-', '')}A7`; const definitions = [ ['user', 'user', 'free'], ['manager', 'manager', 'pro'], ['admin', 'admin', 'pro'], ['super', 'super_admin', 'pro'], ['target', 'user', 'pro'], ]; const accounts = {}; for (const [key, role, tier] of definitions) { accounts[key] = await createAuthUser(key, role, tier, password, nonce); } await setSubscription(accounts.user.id, 'free', 0, 'local ordinary user fixture'); await setSubscription(accounts.manager.id, 'pro', 0, 'local manager fixture'); await setSubscription(accounts.admin.id, 'pro', 3, 'local paid admin fixture'); await setSubscription(accounts.super.id, 'pro', 0, 'local super admin fixture'); await setSubscription(accounts.target.id, 'pro', 5, 'local manager update target'); for (const account of Object.values(accounts)) { const profile = await profileFor(account.id); assert(profile.role === account.role, `${account.role} profile role readback matches`); } return { nonce, password, accounts }; } function actorIdsSql(ids) { for (const id of ids) { assert(/^[0-9a-f-]{36}$/i.test(id), 'cleanup id is a UUID'); } return ids.map((id) => `'${id}'::uuid`).join(', '); } async function cleanupFixture() { const ids = [...cleanupUserIds]; if (ids.length === 0) return { operationRows: 0, auditRows: 0, profiles: 0, authUsers: 0 }; const sqlIds = actorIdsSql(ids); execFileSync('docker', [ 'exec', dbContainer, 'psql', '-U', 'postgres', '-d', 'postgres', '-v', 'ON_ERROR_STOP=1', '-c', `BEGIN; DELETE FROM public.admin_operation_requests WHERE actor_id IN (${sqlIds}); DELETE FROM public.audit_log WHERE admin_id IN (${sqlIds}); DELETE FROM auth.users WHERE id IN (${sqlIds}); COMMIT;`, ], { encoding: 'utf8', timeout: 30_000, windowsHide: true }); const counts = execFileSync('docker', [ 'exec', dbContainer, 'psql', '-U', 'postgres', '-d', 'postgres', '-v', 'ON_ERROR_STOP=1', '-Atc', `SELECT (SELECT count(*) FROM auth.users WHERE id IN (${sqlIds})) || ',' || (SELECT count(*) FROM public.profiles WHERE id IN (${sqlIds})) || ',' || (SELECT count(*) FROM public.admin_operation_requests WHERE actor_id IN (${sqlIds})) || ',' || (SELECT count(*) FROM public.audit_log WHERE admin_id IN (${sqlIds}));`, ], { encoding: 'utf8', timeout: 30_000, windowsHide: true }).trim().split(',').map(Number); assert(counts.length === 4 && counts.every(Number.isSafeInteger), 'cleanup readback returned four integer counts'); return { authUsers: counts[0], profiles: counts[1], operationRows: counts[2], auditRows: counts[3], }; } let primaryError = null; try { stage('fixture-create'); fixture = await createFixture(); evidence.fixtureIds = Object.fromEntries( Object.entries(fixture.accounts).map(([key, account]) => [key, account.id]), ); if (fixtureOnly) { stage('fixture-only-readback-complete'); } else if (contractProbe) { const managerSession = await login(fixture.accounts.manager, fixture.password); const userPayload = await jsonRequest( `/functions/v1/admin-users?userId=${fixture.accounts.target.id}`, { headers: userHeaders(managerSession.access_token) }, ); const paymentPayload = await jsonRequest( `/functions/v1/admin-payments?userId=${fixture.accounts.target.id}&source=db`, { headers: userHeaders(managerSession.access_token) }, ); evidence.contractProbe = { adminUsersTopLevelKeys: Object.keys(userPayload).sort(), adminUsersSubscriptionKeys: Object.keys(userPayload.subscription ?? {}).sort(), adminPaymentsTopLevelKeys: Object.keys(paymentPayload).sort(), adminPaymentsSubscriptionKeys: Object.keys(paymentPayload.subscription ?? {}).sort(), }; stage('admin-contract-probe-complete'); } else { if (skipManager) { await setSubscription( fixture.accounts.target.id, 'pro', 17, 'local manager update target', ); } const staleManagerSession = await login(fixture.accounts.manager, fixture.password); if (skipOrdinary) { stage('ordinary-user-stage-skipped-after-prior-pass'); } else { stage('ordinary-user-hidden-admin-and-test-ad'); const userFlowStartedAt = new Date().toISOString(); runMaestro('admin-local-ordinary.yaml', fixture.accounts.user); runMaestro('admin-local-ordinary-settings.yaml', fixture.accounts.user); const userLogs = edgeLogsSince(userFlowStartedAt); const ordinaryAdminRequests = (userLogs.match(/supabase\/functions\/admin-/g) ?? []).length; assert(ordinaryAdminRequests === 0, 'ordinary user emitted no admin Edge Function request'); evidence.stages.at(-1).adminRequestCount = ordinaryAdminRequests; } if (skipManager) { stage('manager-stage-skipped-after-prior-pass'); } else { stage('manager-read-and-subscription-update'); runMaestro('admin-local-manager.yaml', fixture.accounts.manager); runMaestro('admin-local-manager-read.yaml', fixture.accounts.manager); runMaestro('admin-local-manager-update.yaml', fixture.accounts.manager); const afterManager = await subscriptionFor(fixture.accounts.target.id); assert(afterManager.tier === 'pro' && afterManager.status === 'active', 'manager preserved target entitlement'); assert(afterManager.overage_credits === 17, 'manager updated target overage credits'); assert(afterManager.admin_note === 'local manager update target', 'manager preserved the target admin note'); evidence.stages.at(-1).subscription = { tier: afterManager.tier, status: afterManager.status, overageCredits: afterManager.overage_credits, }; } stage('admin-paid-hidden-ad-and-role-boundary'); runMaestro('admin-local-admin-open.yaml', fixture.accounts.admin); assertRoleOptionPermissions(dumpUi('admin-role-modal-accessibility')); runMaestro('admin-local-admin-complete.yaml', fixture.accounts.admin); const afterAdminProfile = await profileFor(fixture.accounts.target.id); const afterAdminSubscription = await subscriptionFor(fixture.accounts.target.id); assert(afterAdminProfile.role === 'manager', 'admin changed an ordinary user only to manager'); assert(afterAdminSubscription.status === 'active', 'canceling destructive confirmation preserved subscription'); assert(afterAdminSubscription.overage_credits === 17, 'canceling destructive confirmation preserved subscription data'); evidence.stages.at(-1).targetRole = afterAdminProfile.role; evidence.stages.at(-1).subscriptionStatusAfterCancel = afterAdminSubscription.status; stage('super-admin-privileged-role-change'); runMaestro('admin-local-super.yaml', fixture.accounts.super); const afterSuperProfile = await profileFor(fixture.accounts.target.id); assert(afterSuperProfile.role === 'admin', 'super admin changed manager to admin'); evidence.stages.at(-1).targetRole = afterSuperProfile.role; stage('stale-manager-open'); runMaestro('admin-local-stale-open.yaml', fixture.accounts.manager); await setRole(fixture.accounts.manager, 'user'); const staleResponse = await fetch(`${baseUrl}/functions/v1/admin-users?page=1&limit=20`, { headers: userHeaders(staleManagerSession.access_token), }); const staleResponseText = await staleResponse.text(); assert(staleResponse.status === 403, 'stale manager JWT receives an immediate 403 from current-role verification'); assert(!staleResponseText.includes(fixture.accounts.target.id), '403 response contains no target state'); evidence.stages.at(-1).staleHttpStatus = staleResponse.status; runMaestro('admin-local-stale-check.yaml', fixture.accounts.manager); const staleUi = dumpUi('stale-manager-purged-state'); assert(staleUi.includes('admin-error'), 'stale manager UI renders the fail-closed admin error'); assert(!staleUi.includes(fixture.accounts.target.id), 'stale manager UI purged the target identifier'); stage('role-e2e-complete', { assertions }); } } catch (error) { primaryError = error; evidence.failure = error instanceof Error ? error.message : String(error); } finally { stage('fixture-cleanup'); try { evidence.cleanup = await cleanupFixture(); assert( Object.values(evidence.cleanup).every((count) => count === 0), 'all disposable local fixture rows and auth users were removed', ); } catch (cleanupError) { evidence.cleanup = { error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError) }; if (!primaryError) primaryError = cleanupError; } evidence.finishedAt = new Date().toISOString(); evidence.assertions = assertions; writeFileSync(resolve(outputRoot, 'evidence.json'), `${JSON.stringify(evidence, null, 2)}\n`, 'utf8'); } if (primaryError) throw primaryError; console.log(`[admin-mobile-e2e] ${assertions} assertions passed; fixture cleanup verified`);