"use strict"; const assert = require("assert"); const fs = require("fs"); const path = require("path"); const walk = (directory) => fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => entry.isDirectory() ? walk(path.join(directory, entry.name)) : [path.join(directory, entry.name)], ); const apiSource = fs.readFileSync("utils/api.js", "utf8"); const apiOwners = new Set([...apiSource.matchAll(/^ async ([A-Za-z0-9_]+)\(/gm)].map((match) => match[1])); const pageSources = new Map( walk("pages") .filter((file) => file.endsWith(".vue")) .map((file) => [file.replace(/\\/g, "/"), fs.readFileSync(file, "utf8")]), ); const routedPages = new Set( JSON.parse(fs.readFileSync("pages.json", "utf8")).pages.map((page) => `${page.path}.vue`), ); assert.deepStrictEqual( [...routedPages].filter((file) => !pageSources.has(file)), [], "every configured route must have its Vue page source", ); const calls = [...pageSources].flatMap(([file, source]) => [...source.matchAll(/appApi\.([A-Za-z0-9_]+)/g)].map((match) => ({ file, method: match[1] })), ); assert.ok(calls.length > 0, "page API ownership scan found no calls"); assert.deepStrictEqual( calls.filter((call) => !apiOwners.has(call.method)), [], "every page appApi call must have one adapter owner", ); assert.deepStrictEqual( [...routedPages].filter((file) => !calls.some((call) => call.file === file)), ["pages/records/r09-life-events.vue"], "only the explicitly unavailable life-events page may have no backend call", ); const expectedCallers = { sendSmsCode: [ "pages/auth/a01-entry.vue", "pages/auth/a04-register.vue", "pages/auth/a05-reset-password.vue", "pages/profile/m05-change-phone.vue", ], changePhone: ["pages/profile/m05-change-phone.vue"], getRegionChildren: [ "pages/genealogy/g03-create-genealogy.vue", "pages/genealogy/g11-genealogy-settings.vue", ], }; for (const [method, files] of Object.entries(expectedCallers)) { const actual = [...new Set(calls.filter((call) => call.method === method).map((call) => call.file))].sort(); assert.deepStrictEqual(actual, files, `${method} page ownership drifted`); } const changePhonePage = pageSources.get("pages/profile/m05-change-phone.vue"); for (const requiredToken of [ "appApi.getCaptchaRequirement", "appApi.sendSmsCode", "appApi.changePhone", "AUTH_VERIFICATION_OPERATION.PHONE_CHANGE", "