284 lines
9.3 KiB
JavaScript
284 lines
9.3 KiB
JavaScript
const assert = require("assert");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
const vm = require("vm");
|
|
|
|
const rootDir = path.resolve(__dirname, "..", "..");
|
|
const tests = [];
|
|
const test = (name, fn) => tests.push({ name, fn });
|
|
|
|
const readSource = (relativePath) =>
|
|
fs.readFileSync(path.join(rootDir, relativePath), "utf8");
|
|
|
|
const loadAdminPageLoader = (overrides = {}) => {
|
|
const filePath = path.join(rootDir, "pages", "admin", "index.js");
|
|
let source = fs.readFileSync(filePath, "utf8");
|
|
|
|
source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, "");
|
|
source = source.slice(source.indexOf("export const getServerSideProps"));
|
|
source = source.replace(
|
|
/export\s+const\s+getServerSideProps\s*=\s*/,
|
|
"const getServerSideProps = "
|
|
);
|
|
source = source.replace(/const mapDispatchToProps[\s\S]*$/, "");
|
|
source += "\nmodule.exports = { getServerSideProps };\n";
|
|
|
|
const context = {
|
|
module: { exports: {} },
|
|
exports: {},
|
|
wrapper: {
|
|
getServerSideProps: (factory) => async (ctx) => {
|
|
const store = {
|
|
dispatch: () => {},
|
|
getState: () => ({})
|
|
};
|
|
return factory(store)(ctx);
|
|
}
|
|
},
|
|
readAdminSessionCookie: () => "valid-token",
|
|
validateAdminSession: () => true,
|
|
fetchAdminStorageData: async () => ({
|
|
data: [],
|
|
userData: [],
|
|
repCompleteCount: 0
|
|
}),
|
|
getNewAppeals: async () => ({ value: [] }),
|
|
getStatusByAppeal: async () => [],
|
|
getStatusByAppealLPA: async () => [],
|
|
getSearchDetails: async () => ({}),
|
|
setSearchResults: () => ({}),
|
|
setSearchDetails: () => ({}),
|
|
setCurrentPage: () => ({}),
|
|
process: {
|
|
env: {
|
|
ALLOWED_IPS: "203.0.113.10"
|
|
}
|
|
},
|
|
...overrides
|
|
};
|
|
|
|
vm.runInNewContext(source, context, { filename: filePath });
|
|
return context.module.exports;
|
|
};
|
|
|
|
const loadAdminLoginLoader = (overrides = {}) => {
|
|
const filePath = path.join(rootDir, "pages", "admin", "login.js");
|
|
let source = fs.readFileSync(filePath, "utf8");
|
|
|
|
source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, "");
|
|
source = source.slice(source.indexOf("export const getServerSideProps"));
|
|
source = source.replace(
|
|
/export\s+const\s+getServerSideProps\s*=\s*/,
|
|
"const getServerSideProps = "
|
|
);
|
|
source = source.replace(/export default[\s\S]*$/, "");
|
|
source += "\nmodule.exports = { getServerSideProps };\n";
|
|
|
|
const context = {
|
|
module: { exports: {} },
|
|
exports: {},
|
|
readAdminSessionCookie: () => null,
|
|
validateAdminSession: () => false,
|
|
...overrides
|
|
};
|
|
|
|
vm.runInNewContext(source, context, { filename: filePath });
|
|
return context.module.exports;
|
|
};
|
|
|
|
const createAdminContext = (ip = "203.0.113.10") => ({
|
|
query: {},
|
|
req: {
|
|
headers: { "x-forwarded-for": ip },
|
|
socket: { remoteAddress: ip }
|
|
},
|
|
res: {}
|
|
});
|
|
|
|
test("unauthenticated admin access redirects before privileged data loads", async () => {
|
|
let dataLoads = 0;
|
|
const mod = loadAdminPageLoader({
|
|
readAdminSessionCookie: () => null,
|
|
validateAdminSession: () => false,
|
|
fetchAdminStorageData: async () => {
|
|
dataLoads += 1;
|
|
return { data: [], userData: [], repCompleteCount: 0 };
|
|
},
|
|
getNewAppeals: async () => {
|
|
dataLoads += 1;
|
|
return { value: [] };
|
|
}
|
|
});
|
|
|
|
const result = await mod.getServerSideProps(createAdminContext());
|
|
|
|
assert.strictEqual(result.redirect.destination, "/admin/login");
|
|
assert.strictEqual(result.redirect.permanent, false);
|
|
assert.strictEqual(dataLoads, 0);
|
|
});
|
|
|
|
test("invalid or expired administrative sessions cannot access admin", async () => {
|
|
for (const token of ["invalid-token", "expired-token"]) {
|
|
const mod = loadAdminPageLoader({
|
|
readAdminSessionCookie: () => token,
|
|
validateAdminSession: () => false
|
|
});
|
|
const result = await mod.getServerSideProps(createAdminContext());
|
|
assert.strictEqual(result.redirect.destination, "/admin/login");
|
|
}
|
|
});
|
|
|
|
test("valid administrative session and allowed IP load existing admin data", async () => {
|
|
const calls = [];
|
|
const mod = loadAdminPageLoader({
|
|
fetchAdminStorageData: async () => {
|
|
calls.push("storage");
|
|
return { data: ["storage"], userData: ["user"], repCompleteCount: 1 };
|
|
},
|
|
getNewAppeals: async () => {
|
|
calls.push("appeals");
|
|
return { value: [] };
|
|
},
|
|
getStatusByAppeal: async () => {
|
|
calls.push("status");
|
|
return ["status"];
|
|
},
|
|
getStatusByAppealLPA: async () => {
|
|
calls.push("lpa-status");
|
|
return ["lpa-status"];
|
|
},
|
|
getSearchDetails: async () => {
|
|
calls.push("search-details");
|
|
return {};
|
|
}
|
|
});
|
|
|
|
const result = await mod.getServerSideProps(createAdminContext());
|
|
|
|
assert.deepStrictEqual(calls, [
|
|
"storage",
|
|
"appeals",
|
|
"status",
|
|
"lpa-status",
|
|
"search-details"
|
|
]);
|
|
assert.strictEqual(result.props.repCompleteCount, 1);
|
|
});
|
|
|
|
test("valid administrative session and disallowed IP retains 403 redirect", async () => {
|
|
let dataLoads = 0;
|
|
const mod = loadAdminPageLoader({
|
|
fetchAdminStorageData: async () => {
|
|
dataLoads += 1;
|
|
return { data: [], userData: [], repCompleteCount: 0 };
|
|
}
|
|
});
|
|
|
|
const result = await mod.getServerSideProps(
|
|
createAdminContext("198.51.100.25")
|
|
);
|
|
|
|
assert.strictEqual(result.redirect.destination, "/403");
|
|
assert.strictEqual(dataLoads, 0);
|
|
});
|
|
|
|
test("missing administrative configuration fails closed at the page boundary", async () => {
|
|
const mod = loadAdminPageLoader({ validateAdminSession: () => false });
|
|
const result = await mod.getServerSideProps(createAdminContext());
|
|
assert.strictEqual(result.redirect.destination, "/admin/login");
|
|
});
|
|
|
|
test("login page remains available for missing or invalid sessions", async () => {
|
|
const mod = loadAdminLoginLoader();
|
|
const result = await mod.getServerSideProps({ req: { headers: {} } });
|
|
|
|
assert.deepStrictEqual(JSON.parse(JSON.stringify(result)), { props: {} });
|
|
});
|
|
|
|
test("login page redirects an already authenticated administrator to admin", async () => {
|
|
const mod = loadAdminLoginLoader({
|
|
readAdminSessionCookie: () => "valid-token",
|
|
validateAdminSession: () => true
|
|
});
|
|
const result = await mod.getServerSideProps({ req: { headers: {} } });
|
|
|
|
assert.strictEqual(result.redirect.destination, "/admin");
|
|
assert.strictEqual(result.redirect.permanent, false);
|
|
});
|
|
|
|
test("login page uses the existing login API and generic error handling", () => {
|
|
const source = readSource("pages/admin/login.js");
|
|
|
|
assert.match(source, /fetch\("\/api\/admin\/auth\/login"/);
|
|
assert.match(source, /method:\s*"POST"/);
|
|
assert.match(source, /JSON\.stringify\(\{ passphrase \}\)/);
|
|
assert.match(source, /type="password"/);
|
|
assert.match(source, /htmlFor="admin-passphrase"/);
|
|
assert.match(source, /Sign in failed\. Check the details and try again\./);
|
|
assert.match(source, /router\.replace\("\/admin"\)/);
|
|
assert.doesNotMatch(source, /localStorage|sessionStorage/);
|
|
});
|
|
|
|
test("admin logout uses the existing endpoint and returns to login", () => {
|
|
const source = readSource("pages/admin/index.js");
|
|
|
|
assert.match(source, /fetch\("\/api\/admin\/auth\/logout"/);
|
|
assert.match(source, /method:\s*"POST"/);
|
|
assert.match(source, /router\.replace\("\/admin\/login"\)/);
|
|
assert.match(source, />\s*\{signingOut \? "Signing out\.\.\." : "Sign out"\}/);
|
|
assert.doesNotMatch(source, /document\.cookie/);
|
|
});
|
|
|
|
test("admin login integration adds no NextAuth, Prisma, or CRM authentication dependency", () => {
|
|
for (const relativePath of [
|
|
"pages/admin/login.js",
|
|
"lib/admin/adminAuth.js",
|
|
"pages/api/admin/auth/login.js",
|
|
"pages/api/admin/auth/logout.js",
|
|
"pages/api/admin/auth/session.js"
|
|
]) {
|
|
const source = readSource(relativePath);
|
|
assert.doesNotMatch(source, /next-auth|@prisma|Prisma|getPortalLogin|CRM/i);
|
|
}
|
|
});
|
|
|
|
test("existing signing and document service files remain unchanged", () => {
|
|
const protectedPaths = [
|
|
"pages/api/endpoint/gethash_api.js",
|
|
"actions/services/documentDirectService.js"
|
|
];
|
|
const changedPaths = require("child_process")
|
|
.execFileSync("git", ["diff", "--name-only", "HEAD"], {
|
|
cwd: rootDir,
|
|
encoding: "utf8"
|
|
})
|
|
.trim()
|
|
.split("\n")
|
|
.filter(Boolean);
|
|
|
|
for (const protectedPath of protectedPaths) {
|
|
assert.ok(!changedPaths.includes(protectedPath));
|
|
}
|
|
});
|
|
|
|
const run = async () => {
|
|
let passed = 0;
|
|
|
|
for (const currentTest of tests) {
|
|
await currentTest.fn();
|
|
passed += 1;
|
|
}
|
|
|
|
console.log(
|
|
`Administrative login integration tests passed (${passed}/${tests.length}).`
|
|
);
|
|
};
|
|
|
|
module.exports = run;
|
|
|
|
if (require.main === module) {
|
|
run().catch((error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|
|
} |