+
Showing {page * PAGE + 1}–{Math.min((page + 1) * PAGE, total)} of {total}
setPage((p) => Math.max(0, p - 1))} disabled={page === 0}
- className="px-4 py-2 rounded-xl border border-white/20 text-sm disabled:opacity-40 hover:bg-white/10">
- ← Prev
+ className="px-4 py-2 rounded-xl border border-slate-200 text-slate-600 text-sm disabled:opacity-40 hover:bg-slate-50">
+ ← Prev
setPage((p) => p + 1)} disabled={(page + 1) * PAGE >= total}
- className="px-4 py-2 rounded-xl border border-white/20 text-sm disabled:opacity-40 hover:bg-white/10">
+ className="px-4 py-2 rounded-xl border border-slate-200 text-slate-600 text-sm disabled:opacity-40 hover:bg-slate-50">
Next →
diff --git a/app/(admin)/admin/wards/page.tsx b/app/(admin)/admin/wards/page.tsx
index c7c15e4..b2ee586 100644
--- a/app/(admin)/admin/wards/page.tsx
+++ b/app/(admin)/admin/wards/page.tsx
@@ -324,21 +324,37 @@ export default function AdminWardsPage() {
const [filterState, setFilterState] = useState("");
const [search, setSearch] = useState("");
+ const [lgaOptions, setLgaOptions] = useState<{ id: string; lgaName: string }[]>([]);
+ const [filterLgaId, setFilterLgaId] = useState("");
const fetchWards = useCallback(async () => {
setLoading(true);
const p = new URLSearchParams({ limit: String(LIMIT), offset: String(page * LIMIT) });
- if (filterState) p.set("state", filterState);
+ if (filterLgaId) p.set("lgaId", filterLgaId);
+ else if (filterState) p.set("state", filterState);
if (search) p.set("search", search);
const r = await fetch(`/api/admin/wards?${p}`, { headers: { "x-admin-secret": getAdminSecret() } });
const d = await r.json();
setWards(d.wards ?? []);
setTotal(d.total ?? 0);
setLoading(false);
- }, [page, filterState, search]);
+ }, [page, filterState, filterLgaId, search]);
useEffect(() => { fetchWards(); }, [fetchWards]);
+ // Populate the LGA picker for the selected state (LGAs are only fetchable
+ // scoped to a state — there are 774+ of them, too many for one dropdown).
+ // filterLgaId is reset by the state
's onChange, not here.
+ useEffect(() => {
+ if (!filterState) return;
+ (async () => {
+ const p = new URLSearchParams({ state: filterState, limit: "100" });
+ const r = await fetch(`/api/admin/lgas?${p}`, { headers: { "x-admin-secret": getAdminSecret() } });
+ const d = await r.json();
+ setLgaOptions((d.lgas ?? []).map((l: { id: string; lgaName: string }) => ({ id: l.id, lgaName: l.lgaName })));
+ })();
+ }, [filterState]);
+
function showToast(msg: string) { setToast(msg); setTimeout(() => setToast(""), 3000); }
async function deleteWard(id: string) {
@@ -352,14 +368,17 @@ export default function AdminWardsPage() {
async function exportCsv() {
const p = new URLSearchParams();
- if (filterState) p.set("state", filterState);
+ if (filterLgaId) p.set("lgaId", filterLgaId);
+ else if (filterState) p.set("state", filterState);
const res = await fetch(`/api/admin/wards/export?${p}`, { headers: { "x-admin-secret": getAdminSecret() } });
if (!res.ok) { showToast("Export failed."); return; }
+ const disposition = res.headers.get("Content-Disposition") ?? "";
+ const filename = disposition.match(/filename="(.+)"/)?.[1] ?? `ward-records-${new Date().toISOString().split("T")[0]}.csv`;
const blob = await res.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
- a.download = `ward-records-${new Date().toISOString().split("T")[0]}.csv`;
+ a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
@@ -413,6 +432,7 @@ export default function AdminWardsPage() {
}
const pages = Math.ceil(total / LIMIT);
+ const selectedLgaName = lgaOptions.find((l) => l.id === filterLgaId)?.lgaName;
return (
@@ -425,9 +445,10 @@ export default function AdminWardsPage() {
- Export CSV
+ {selectedLgaName ? `Export CSV — ${selectedLgaName}` : "Export CSV"}
setShowCSV(true)}
@@ -449,13 +470,25 @@ export default function AdminWardsPage() {
{ setFilterState(e.target.value); setPage(0); }}
+ onChange={(e) => { setFilterState(e.target.value); setFilterLgaId(""); setLgaOptions([]); setPage(0); }}
className="appearance-none bg-white border border-slate-200 rounded-lg pl-3 pr-8 py-1.5 text-sm text-slate-700 outline-none focus:ring-2 focus:ring-green-500"
>
{ALL_STATES.map((s) => {s || "All States"} )}
+
+ { setFilterLgaId(e.target.value); setPage(0); }}
+ disabled={!filterState}
+ className="appearance-none bg-white border border-slate-200 rounded-lg pl-3 pr-8 py-1.5 text-sm text-slate-700 outline-none focus:ring-2 focus:ring-green-500 disabled:bg-slate-50 disabled:text-slate-400 min-w-40"
+ >
+ {filterState ? "All LGAs" : "Select a state first"}
+ {lgaOptions.map((l) => {l.lgaName} )}
+
+
+
-// for a single-record CSV (one row) instead of the filtered bulk export.
+// for a single-record CSV (one row), ?lgaId= for every ward belonging
+// to one LGA, or ?state= for every ward in a state — otherwise every
+// ward across all LGAs is exported.
export async function GET(req: NextRequest) {
if (!isAdminRequest(req)) return NextResponse.json({ error: "Unauthorized." }, { status: 401 });
const { searchParams } = new URL(req.url);
const id = searchParams.get("id") ?? undefined;
+ const lgaId = searchParams.get("lgaId") ?? undefined;
const state = searchParams.get("state") ?? undefined;
const wards = await db.ward.findMany({
- where: id ? { id } : state ? { lga: { state: { equals: state, mode: "insensitive" } } } : {},
+ where: id
+ ? { id }
+ : lgaId
+ ? { lgaId }
+ : state
+ ? { lga: { state: { equals: state, mode: "insensitive" } } }
+ : {},
include: { lga: { select: { lgaName: true, state: true } } },
orderBy: [{ lga: { state: "asc" } }, { lga: { lgaName: "asc" } }, { wardNumber: "asc" }],
take: id ? 1 : 5000,
@@ -44,8 +53,11 @@ export async function GET(req: NextRequest) {
...rows.map((r) => COLUMNS.map((c) => csvEscape(r[c])).join(",")),
];
+ const slug = (s: string) => s.replace(/[^a-z0-9]+/gi, "-").toLowerCase();
const filename = id
- ? `ward-${rows[0].wardName.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}.csv`
+ ? `ward-${slug(rows[0].wardName)}.csv`
+ : lgaId && rows.length > 0
+ ? `wards-${slug(rows[0].lgaName)}.csv`
: `ward-records-${new Date().toISOString().split("T")[0]}.csv`;
return new NextResponse(lines.join("\n"), {
diff --git a/app/api/admin/wards/route.ts b/app/api/admin/wards/route.ts
index a4bd728..5fdae99 100644
--- a/app/api/admin/wards/route.ts
+++ b/app/api/admin/wards/route.ts
@@ -21,13 +21,15 @@ export async function GET(req: NextRequest) {
if (!isAdminRequest(req)) return NextResponse.json({ error: "Unauthorized." }, { status: 401 });
const { searchParams } = new URL(req.url);
+ const lgaId = searchParams.get("lgaId") ?? undefined;
const state = searchParams.get("state") ?? undefined;
const search = searchParams.get("search") ?? undefined; // matches ward name or LGA name
const limit = Math.min(Number(searchParams.get("limit") ?? "30"), 200);
const offset = Number(searchParams.get("offset") ?? "0");
const where: Record = {};
- if (state) where.lga = { state: { equals: state, mode: "insensitive" } };
+ if (lgaId) where.lgaId = lgaId;
+ else if (state) where.lga = { state: { equals: state, mode: "insensitive" } };
if (search) {
where.OR = [
{ wardName: { contains: search, mode: "insensitive" } },
diff --git a/app/api/lga/register/route.ts b/app/api/lga/register/route.ts
index c770a98..e671306 100644
--- a/app/api/lga/register/route.ts
+++ b/app/api/lga/register/route.ts
@@ -6,6 +6,16 @@ import { lgaSignUpSchema } from "@/lib/validations";
import { generateToken, sanitizeInput } from "@/lib/utils";
import { sendLGAVerificationEmail } from "@/lib/email";
+// Nigeria has exactly 774 Local Government Areas. Once /api/admin/seed has
+// loaded the official 774 (see prisma/seeds/nigeria-lgas.ts), every real
+// registration matches a seeded row by name+state and goes through the
+// "claim" path below (an update, not a create) — so this only ever blocks
+// the "fresh create" branch, which is where anything that isn't one of the
+// 774 official LGAs would otherwise slip in. Test/seed scripts can bypass
+// this with the same secret that guards /api/admin/seed.
+const TOTAL_LGA_CAP = 774;
+const SEED_SECRET = process.env.SEED_SECRET ?? "";
+
export async function POST(request: Request) {
// Rate limit: 3 per hour per IP
const ip = getClientIP(request);
@@ -108,6 +118,20 @@ export async function POST(request: Request) {
include: { chairman: { select: { id: true } } },
});
} else {
+ // No seed record — this would create an LGA that isn't one of
+ // Nigeria's official 774. Blocked unless the request carries the
+ // seed-secret test bypass.
+ const isTestBypass = Boolean(SEED_SECRET) && request.headers.get("x-seed-secret") === SEED_SECRET;
+ if (!isTestBypass) {
+ const totalLgas = await db.lGA.count();
+ if (totalLgas >= TOTAL_LGA_CAP) {
+ return NextResponse.json(
+ { error: "Nigeria has 774 Local Government Areas and that limit has been reached. If your LGA isn't listed, contact support." },
+ { status: 409 }
+ );
+ }
+ }
+
// No seed record — create fresh
lga = await db.lGA.create({
data: {
diff --git a/playwright.config.ts b/playwright.config.ts
index 35c0ae6..101a65b 100644
--- a/playwright.config.ts
+++ b/playwright.config.ts
@@ -1,4 +1,9 @@
import { defineConfig, devices } from "@playwright/test";
+import { loadEnvConfig } from "@next/env";
+
+// Load .env.local the same way `next dev` does, so tests can read
+// SEED_SECRET (and anything else app code reads from process.env).
+loadEnvConfig(process.cwd());
export default defineConfig({
testDir: "./tests/e2e",
diff --git a/tests/e2e/admin-browser.spec.ts b/tests/e2e/admin-browser.spec.ts
index c041571..35bc8ac 100644
--- a/tests/e2e/admin-browser.spec.ts
+++ b/tests/e2e/admin-browser.spec.ts
@@ -65,7 +65,7 @@ async function seedPendingLGA(request: APIRequestContext, ip: string): Promise<{
const email = `chairman_adm_${suffix}@example.com`;
const reg = await request.post("/api/lga/register", {
- headers: { "x-forwarded-for": ip },
+ headers: { "x-forwarded-for": ip, "x-seed-secret": process.env.SEED_SECRET ?? "" },
data: {
lgaName, state: "Kano", chairmanName: "Alhaji Admin",
email, phone: "08011223344", officeAddress: "3 Council Close, Kano",
diff --git a/tests/e2e/admin-lga-ward-e2e.spec.ts b/tests/e2e/admin-lga-ward-e2e.spec.ts
index d4488d0..39a50cc 100644
--- a/tests/e2e/admin-lga-ward-e2e.spec.ts
+++ b/tests/e2e/admin-lga-ward-e2e.spec.ts
@@ -53,6 +53,7 @@ async function seedApprovedLGA(ip: string): Promise<{ id: string; email: string;
const ctx = await apiRequest.newContext({ baseURL: BASE, extraHTTPHeaders: { "x-forwarded-for": ip } });
const reg = await ctx.post("/api/lga/register", {
+ headers: { "x-seed-secret": process.env.SEED_SECRET ?? "" },
data: {
lgaName, state, chairmanName: "Chief Record", email, phone: "08012345678",
officeAddress: "1 Record Road, Ikeja", sectors: ["Health"],
@@ -368,4 +369,67 @@ test.describe("Ward record lifecycle — list, edit, delete, export", () => {
const res = await c.get("/api/admin/wards/export?id=does-not-exist", { headers: ADMIN });
expect(res.status()).toBe(404);
});
+
+ test("?lgaId= exports every ward for that LGA only, not the whole table", async () => {
+ const lgaA = await seedApprovedLGA(ipFor(14));
+ const lgaB = await seedApprovedLGA(ipFor(15));
+ const c = await apiRequest.newContext({ baseURL: BASE });
+
+ await c.post("/api/admin/wards", {
+ headers: ADMIN,
+ data: [
+ { lgaName: lgaA.lgaName, state: lgaA.state, wardName: "A Ward One", councillorName: "Cllr A1" },
+ { lgaName: lgaA.lgaName, state: lgaA.state, wardName: "A Ward Two", councillorName: "Cllr A2" },
+ { lgaName: lgaB.lgaName, state: lgaB.state, wardName: "B Ward One", councillorName: "Cllr B1" },
+ ],
+ });
+
+ const exported = await c.get(`/api/admin/wards/export?lgaId=${lgaA.id}`, { headers: ADMIN });
+ expect(exported.status()).toBe(200);
+ expect(exported.headers()["content-disposition"]).toContain(`wards-${lgaA.lgaName.replace(/\s+/g, "-").toLowerCase()}`);
+
+ const lines = (await exported.text()).trim().split("\n");
+ expect(lines).toHaveLength(3); // header + 2 rows, lgaB's ward excluded
+ expect(lines.join("\n")).toContain("A Ward One");
+ expect(lines.join("\n")).toContain("A Ward Two");
+ expect(lines.join("\n")).not.toContain("B Ward One");
+
+ // Round-trips through the bulk import endpoint, updating only that LGA's wards.
+ const reimport = await c.post("/api/admin/wards", {
+ headers: ADMIN,
+ data: [
+ { lgaName: lgaA.lgaName, state: lgaA.state, wardName: "A Ward One", councillorName: "Cllr A1 Corrected" },
+ { lgaName: lgaA.lgaName, state: lgaA.state, wardName: "A Ward Two", councillorName: "Cllr A2 Corrected" },
+ ],
+ });
+ expect(reimport.status()).toBe(201);
+ expect((await reimport.json()).created).toBe(2);
+
+ const { rows } = await pool.query(
+ `SELECT "councillorName" FROM wards WHERE "lgaId" = $1 ORDER BY "wardName"`,
+ [lgaB.id]
+ );
+ expect(rows[0].councillorName, "the other LGA's ward must be untouched").toBe("Cllr B1");
+ });
+
+ test("GET /api/admin/wards?lgaId= filters the list to that LGA only", async () => {
+ const lgaA = await seedApprovedLGA(ipFor(16));
+ const lgaB = await seedApprovedLGA(ipFor(17));
+ const c = await apiRequest.newContext({ baseURL: BASE });
+
+ await c.post("/api/admin/wards", {
+ headers: ADMIN,
+ data: [
+ { lgaName: lgaA.lgaName, state: lgaA.state, wardName: "Filter Ward A", councillorName: "Cllr FA" },
+ { lgaName: lgaB.lgaName, state: lgaB.state, wardName: "Filter Ward B", councillorName: "Cllr FB" },
+ ],
+ });
+
+ const list = await c.get(`/api/admin/wards?lgaId=${lgaA.id}`, { headers: ADMIN });
+ expect(list.status()).toBe(200);
+ const body = await list.json();
+ expect(body.wards.every((w: { lga: { lgaName: string } }) => w.lga.lgaName === lgaA.lgaName)).toBe(true);
+ expect(body.wards.some((w: { wardName: string }) => w.wardName === "Filter Ward A")).toBe(true);
+ expect(body.wards.some((w: { wardName: string }) => w.wardName === "Filter Ward B")).toBe(false);
+ });
});
diff --git a/tests/e2e/auth-e2e.spec.ts b/tests/e2e/auth-e2e.spec.ts
index 08c487a..4df06c7 100644
--- a/tests/e2e/auth-e2e.spec.ts
+++ b/tests/e2e/auth-e2e.spec.ts
@@ -263,7 +263,10 @@ test.describe("Auth E2E — LGA Chairman", () => {
test("registration → 201 with lgaId", async () => {
const ctx = await ctxForIp(IP);
- const res = await ctx.post("/api/lga/register", { data: registrationPayload });
+ const res = await ctx.post("/api/lga/register", {
+ data: registrationPayload,
+ headers: { "x-seed-secret": process.env.SEED_SECRET ?? "" },
+ });
expect(res.status()).toBe(201);
const body = await res.json();
expect(body.success).toBe(true);
@@ -309,6 +312,7 @@ test.describe("Auth E2E — LGA Chairman", () => {
const ctx = await ctxForIp(IP);
const res = await ctx.post("/api/lga/register", {
data: { ...registrationPayload, lgaName: `${lgaName} Two` },
+ headers: { "x-seed-secret": process.env.SEED_SECRET ?? "" },
});
expect(res.status()).toBe(409);
});
diff --git a/tests/e2e/chairman-crud-e2e.spec.ts b/tests/e2e/chairman-crud-e2e.spec.ts
index 438f92e..56015b6 100644
--- a/tests/e2e/chairman-crud-e2e.spec.ts
+++ b/tests/e2e/chairman-crud-e2e.spec.ts
@@ -64,6 +64,7 @@ async function authedLGA(ip: string): Promise<{ ctx: APIRequestContext; lgaId: s
const ctx = await ctxForIp(ip);
const reg = await ctx.post("/api/lga/register", {
+ headers: { "x-seed-secret": process.env.SEED_SECRET ?? "" },
data: {
lgaName: `Governville ${suffix}`, state: "Lagos", chairmanName: "Chief Govern",
email, phone: "08012345678", officeAddress: "1 Council Road, Ikeja",
diff --git a/tests/e2e/citizen-lga-e2e.spec.ts b/tests/e2e/citizen-lga-e2e.spec.ts
index af256cb..3836fc9 100644
--- a/tests/e2e/citizen-lga-e2e.spec.ts
+++ b/tests/e2e/citizen-lga-e2e.spec.ts
@@ -120,6 +120,7 @@ async function authedLGA(ip: string): Promise<{ ctx: APIRequestContext; lgaId: s
const ctx = await ctxForIp(ip);
const reg = await ctx.post("/api/lga/register", {
+ headers: { "x-seed-secret": process.env.SEED_SECRET ?? "" },
data: {
lgaName, state: "Lagos", chairmanName: "Chief Portal", email, phone: "08012345678",
officeAddress: "1 Council Road, Ikeja", sectors: ["Health", "Education"],
diff --git a/tests/e2e/fr01-fr03.spec.ts b/tests/e2e/fr01-fr03.spec.ts
index 4a04b9c..8f5d81e 100644
--- a/tests/e2e/fr01-fr03.spec.ts
+++ b/tests/e2e/fr01-fr03.spec.ts
@@ -10,8 +10,11 @@
*/
import { test, expect, request as apiRequest } from "@playwright/test";
+import { Pool } from "pg";
const BASE = "http://localhost:3000";
+const pool = new Pool({ connectionString: process.env.DATABASE_URL });
+test.afterAll(async () => { await pool.end(); });
async function apiPost(
url: string,
@@ -37,6 +40,10 @@ async function apiGet(url: string, headers: Record = {}) {
const ADMIN = { "x-admin-secret": process.env.NEXT_PUBLIC_ADMIN_SECRET ?? "4a0423d4888f73e76fbbb5655ac5458c09be34d5d4eaa9522f943b9cc3d80666" };
+// Bypasses the 774-LGA total cap on POST /api/lga/register (see route.ts) —
+// the local test DB accumulates far more than 774 rows across test runs.
+const SEED_HEADER = { "x-seed-secret": process.env.SEED_SECRET ?? "" };
+
// ─── FR-01-01: Citizen Registration ─────────────────────────────────────────
test.describe("FR-01-01: Citizen Registration — API", () => {
@@ -272,7 +279,7 @@ test.describe("FR-02-01: LGA Registration — API contracts", () => {
password: "Secure@123",
confirmPassword: "Secure@123",
terms: true,
- });
+ }, SEED_HEADER);
expect([201, 429]).toContain(status);
if (status === 201) {
expect(body.success).toBe(true);
@@ -294,11 +301,11 @@ test.describe("FR-02-01: LGA Registration — API contracts", () => {
confirmPassword: "Secure@123",
terms: true,
};
- await apiPost("/api/lga/register", { ...base, email: `lga1+${ts}@mailinator.com` });
+ await apiPost("/api/lga/register", { ...base, email: `lga1+${ts}@mailinator.com` }, SEED_HEADER);
const { status } = await apiPost("/api/lga/register", {
...base,
email: `lga2+${ts}@mailinator.com`,
- });
+ }, SEED_HEADER);
// 409 on dup, or 429 if rate-limited (3 per hour)
expect([409, 429]).toContain(status);
});
@@ -314,12 +321,79 @@ test.describe("FR-02-01: LGA Registration — API contracts", () => {
password: "Secure@123",
confirmPassword: "Secure@123",
terms: true,
- });
+ }, SEED_HEADER);
expect([400, 429]).toContain(status);
if (status === 400) expect(typeof body.error).toBe("string");
});
});
+// ─── 774-LGA total cap guard ────────────────────────────────────────────────
+
+test.describe("774-LGA cap guard on POST /api/lga/register", () => {
+ test("a fresh LGA create is blocked once the total reaches 774, but the seed-secret bypass still works", async () => {
+ const { rows } = await pool.query("SELECT count(*)::int AS count FROM lgas");
+ test.skip(rows[0].count < 774, "local DB has fewer than 774 LGAs; the cap hasn't kicked in yet");
+
+ const ctx = await apiRequest.newContext({
+ baseURL: BASE,
+ extraHTTPHeaders: { "x-forwarded-for": `198.30.${Math.floor(Math.random() * 254) + 1}.1` },
+ });
+ const ts = Date.now();
+ const payload = (suffix: string) => ({
+ lgaName: `Cap Guard Test ${ts}${suffix}`,
+ state: "Lagos",
+ chairmanName: "Cap Guard Tester",
+ email: `cap-guard-${ts}${suffix}@mailinator.com`,
+ phone: "08012345678",
+ officeAddress: "1 Cap Guard Road",
+ sectors: ["Health"],
+ password: "Secure@123",
+ confirmPassword: "Secure@123",
+ terms: true,
+ });
+
+ const blocked = await ctx.post("/api/lga/register", { data: payload("a") });
+ expect(blocked.status()).toBe(409);
+ expect((await blocked.json()).error).toMatch(/774/);
+
+ const bypassed = await ctx.post("/api/lga/register", {
+ data: payload("b"),
+ headers: SEED_HEADER,
+ });
+ expect(bypassed.status()).toBe(201);
+ });
+
+ test("claiming an already-seeded LGA (no chairman yet) is never blocked by the cap", async () => {
+ const suffix = `${Date.now()}${Math.floor(Math.random() * 1000)}`;
+ const lgaName = `Seed Claim Test ${suffix}`;
+ await pool.query(
+ `INSERT INTO lgas (id, "lgaName", state, "chairmanName", email, phone, "officeAddress", status, "isVerified", sectors, "createdAt", "updatedAt")
+ VALUES ($1, $2, 'Ogun', 'Vacant', $3, '08000000000', 'N/A', 'APPROVED', true, ARRAY[]::text[], now(), now())`,
+ [`captest${suffix}`.slice(0, 25), lgaName, `vacant-${suffix}@lga.gov.ng`]
+ );
+
+ const ctx = await apiRequest.newContext({
+ baseURL: BASE,
+ extraHTTPHeaders: { "x-forwarded-for": `198.31.${Math.floor(Math.random() * 254) + 1}.1` },
+ });
+ const claim = await ctx.post("/api/lga/register", {
+ data: {
+ lgaName,
+ state: "Ogun",
+ chairmanName: "Real Chairman",
+ email: `real-chairman-${suffix}@mailinator.com`,
+ phone: "08012345678",
+ officeAddress: "1 Real Street",
+ sectors: ["Health"],
+ password: "Secure@123",
+ confirmPassword: "Secure@123",
+ terms: true,
+ },
+ });
+ expect(claim.status()).toBe(201);
+ });
+});
+
// ─── FR-02-03: Admin LGA Approval ───────────────────────────────────────────
test.describe("FR-02-03: Admin Approval — API contracts", () => {
diff --git a/tests/e2e/investor-browser.spec.ts b/tests/e2e/investor-browser.spec.ts
index 6d5c3f7..2831185 100644
--- a/tests/e2e/investor-browser.spec.ts
+++ b/tests/e2e/investor-browser.spec.ts
@@ -68,7 +68,7 @@ async function seedApprovedLGAWithEndowment(
// 1. Register
const reg = await request.post("/api/lga/register", {
- headers: { "x-forwarded-for": ip },
+ headers: { "x-forwarded-for": ip, "x-seed-secret": process.env.SEED_SECRET ?? "" },
data: {
lgaName, state: "Lagos", chairmanName: "Chief Investor",
email, phone: "08012345678", officeAddress: "2 Investment Road, Ikeja",
diff --git a/tests/e2e/investor-lga-e2e.spec.ts b/tests/e2e/investor-lga-e2e.spec.ts
index b5c71dd..72ec814 100644
--- a/tests/e2e/investor-lga-e2e.spec.ts
+++ b/tests/e2e/investor-lga-e2e.spec.ts
@@ -69,6 +69,7 @@ async function authedLGA(ip: string): Promise<{ ctx: APIRequestContext; lgaId: s
const ctx = await ctxForIp(ip);
const reg = await ctx.post("/api/lga/register", {
+ headers: { "x-seed-secret": process.env.SEED_SECRET ?? "" },
data: {
lgaName: `Endowville ${suffix}`, state: "Lagos", chairmanName: "Chief Invest",
email, phone: "08012345678", officeAddress: "1 Council Road, Ikeja",
diff --git a/tests/e2e/lga-dashboard-browser.spec.ts b/tests/e2e/lga-dashboard-browser.spec.ts
index 92343db..e97666c 100644
--- a/tests/e2e/lga-dashboard-browser.spec.ts
+++ b/tests/e2e/lga-dashboard-browser.spec.ts
@@ -147,7 +147,7 @@ async function seedVerifiedLGA(
const suffix = uniq();
const email = `chairman_${suffix}@example.com`;
const reg = await request.post("/api/lga/register", {
- headers: { "x-forwarded-for": ip },
+ headers: { "x-forwarded-for": ip, "x-seed-secret": process.env.SEED_SECRET ?? "" },
data: {
lgaName: `Browserville ${suffix}`, state: "Lagos", chairmanName: "Chief Browser",
email, phone: "08012345678", officeAddress: "1 Council Road, Ikeja",
diff --git a/tests/e2e/lga-portal-auth-e2e.spec.ts b/tests/e2e/lga-portal-auth-e2e.spec.ts
index dd0c82e..b2f840d 100644
--- a/tests/e2e/lga-portal-auth-e2e.spec.ts
+++ b/tests/e2e/lga-portal-auth-e2e.spec.ts
@@ -64,6 +64,7 @@ async function authedLGA(ip: string): Promise<{ ctx: APIRequestContext; lgaId: s
const ctx = await ctxForIp(ip);
const reg = await ctx.post("/api/lga/register", {
+ headers: { "x-seed-secret": process.env.SEED_SECRET ?? "" },
data: {
lgaName: `Portalville ${suffix}`, state: "Lagos", chairmanName: "Chief Portal",
email, phone: "08012345678", officeAddress: "1 Council Road, Ikeja",
diff --git a/tests/e2e/payments-e2e.spec.ts b/tests/e2e/payments-e2e.spec.ts
index 90a8aec..bc41e43 100644
--- a/tests/e2e/payments-e2e.spec.ts
+++ b/tests/e2e/payments-e2e.spec.ts
@@ -64,6 +64,7 @@ async function authedLGA(ip: string): Promise<{ ctx: APIRequestContext; lgaId: s
const c = await apiRequest.newContext({ baseURL: BASE, extraHTTPHeaders: { "x-forwarded-for": ip } });
const reg = await c.post("/api/lga/register", {
+ headers: { "x-seed-secret": process.env.SEED_SECRET ?? "" },
data: {
lgaName: `Payville ${suffix}`, state: "Lagos", chairmanName: "Chief Pay",
email, phone: "08012345678", officeAddress: "1 Pay Road, Ikeja",