Files
chocoadmin/scripts/test-upgrade-trial.ts

274 lines
10 KiB
TypeScript

import { config } from "dotenv";
import { z } from "zod";
config({ path: ".env.local" });
async function main() {
const { db } = await import("@/lib/db");
const { createUpgradeRequest, approveUpgradeRequest } = await import(
"@/lib/upgrade-requests"
);
const { ACTIVATE_STATUSES, ACCOUNT_TYPES, REQUEST_STATUSES, PAID_ACCOUNT_TYPE_VALUES } = await import(
"@/lib/constants"
);
const { ApiError } = await import("@/lib/errors");
// Mirrors the zod schema in app/api/maestros/[id]/upgrade-requests/route.ts —
// this is what the [업그레이드] button's POST body must pass through.
const postBodySchema = z.object({
requestedAccountType: z.coerce
.number()
.int()
.refine((v) => (PAID_ACCOUNT_TYPE_VALUES as readonly number[]).includes(v)),
});
type Scenario = {
label: string;
initialActivateStatus: number;
initialAccountType: number;
requestedAccountType: number;
createExpectsError?: number;
approveExpectsError?: number;
directInsertRequest?: boolean;
};
const scenarios: Scenario[] = [
{
label: "TRIAL status + paid AccountType(BASIC_20) → PRO_100 (higher tier)",
initialActivateStatus: ACTIVATE_STATUSES.TRIAL,
initialAccountType: ACCOUNT_TYPES.BASIC_20,
requestedAccountType: ACCOUNT_TYPES.PRO_100,
},
{
label: "TRIAL status + paid AccountType(STANDARD_50) → STANDARD_50 (same-tier conversion)",
initialActivateStatus: ACTIVATE_STATUSES.TRIAL,
initialAccountType: ACCOUNT_TYPES.STANDARD_50,
requestedAccountType: ACCOUNT_TYPES.STANDARD_50,
},
{
label: "TRIAL status + trial AccountType(MAESTRO_TRIAL=101) → BASIC_20 (via admin create)",
initialActivateStatus: ACTIVATE_STATUSES.TRIAL,
initialAccountType: ACCOUNT_TYPES.MAESTRO_TRIAL,
requestedAccountType: ACCOUNT_TYPES.BASIC_20,
},
{
label: "TRIAL status + trial AccountType(STUDENT_TRIAL=100) → SCHOOL_1000 (direct-insert bypass, mimicking chocomae flow)",
initialActivateStatus: ACTIVATE_STATUSES.TRIAL,
initialAccountType: ACCOUNT_TYPES.STUDENT_TRIAL,
requestedAccountType: ACCOUNT_TYPES.SCHOOL_1000,
directInsertRequest: true,
},
];
let failures = 0;
for (const scenario of scenarios) {
const stamp = Date.now();
const testName = `__TEST_TRIAL_UPG_${stamp}__`.slice(0, 50);
const initialAvailable = new Date(2000, 0, 1);
let maestroID: number | null = null;
console.log(`\n=== ${scenario.label} ===`);
try {
const created = await db.maestro.create({
data: {
Name: testName,
Password: "",
Email: `${stamp}@test.invalid`.slice(0, 50),
AccountType: scenario.initialAccountType,
ActivateStatus: scenario.initialActivateStatus,
AvailableActivateDateTime: initialAvailable,
PlayerCount: 0,
AcceptClausesDateTime: new Date(),
AllowEditEnterCode: 0,
},
select: { MaestroID: true },
});
maestroID = created.MaestroID;
console.log(` seeded maestro MaestroID=${maestroID}`);
let maestroUpgradeID: number | null = null;
// Step 1: create request (either via admin API or direct insert)
if (scenario.directInsertRequest) {
const inserted = await db.maestro_upgrade.create({
data: {
MaestroID: maestroID,
RegisteredActivateStatus: scenario.initialActivateStatus,
RegisteredAccountType: scenario.initialAccountType,
RequestedAccountType: scenario.requestedAccountType,
RequestedDateTime: new Date(),
Status: REQUEST_STATUSES.REQUESTED,
},
select: { MaestroUpgradeID: true },
});
maestroUpgradeID = inserted.MaestroUpgradeID;
console.log(` direct-inserted upgrade request maestroUpgradeID=${maestroUpgradeID}`);
} else {
// Simulate the [업그레이드] button POST body path: zod-validate first.
const parsed = postBodySchema.safeParse({
requestedAccountType: scenario.requestedAccountType,
});
console.log(
` zod schema on { requestedAccountType: ${scenario.requestedAccountType} } → ${parsed.success ? "accepted" : "rejected"}`
);
if (!parsed.success) {
console.log(
` FAIL API zod schema rejected paid-tier target (would produce 400 from API layer)`
);
failures += 1;
continue;
}
try {
const result = await createUpgradeRequest(
maestroID,
parsed.data.requestedAccountType
);
if (scenario.createExpectsError !== undefined) {
console.log(` FAIL createUpgradeRequest expected ApiError(${scenario.createExpectsError}), succeeded`);
failures += 1;
} else {
maestroUpgradeID = result.maestroUpgradeID;
console.log(` createUpgradeRequest → maestroUpgradeID=${maestroUpgradeID}, status=${result.status}`);
}
} catch (error) {
if (
scenario.createExpectsError !== undefined &&
error instanceof ApiError &&
error.statusCode === scenario.createExpectsError
) {
console.log(` PASS createUpgradeRequest correctly threw ApiError(${error.statusCode}): ${error.message}`);
continue;
}
throw error;
}
}
if (maestroUpgradeID === null) continue;
// Step 2: approve
try {
const approveResult = await approveUpgradeRequest(maestroUpgradeID);
if (scenario.approveExpectsError !== undefined) {
console.log(` FAIL approveUpgradeRequest expected ApiError(${scenario.approveExpectsError}), succeeded`);
failures += 1;
continue;
}
console.log(` approveUpgradeRequest → availableActivateDateTime=${approveResult.availableActivateDateTime}`);
} catch (error) {
if (
scenario.approveExpectsError !== undefined &&
error instanceof ApiError &&
error.statusCode === scenario.approveExpectsError
) {
console.log(` PASS approveUpgradeRequest correctly threw ApiError(${error.statusCode}): ${error.message}`);
// Verify rollback: request row should NOT be APPLIED, and maestro state should be unchanged
const stillRequestedRow = await db.maestro_upgrade.findUniqueOrThrow({
where: { MaestroUpgradeID: maestroUpgradeID },
select: { Status: true },
});
const stillTrialMaestro = await db.maestro.findUniqueOrThrow({
where: { MaestroID: maestroID },
select: { ActivateStatus: true, AccountType: true },
});
const rollbackOK =
stillRequestedRow.Status === REQUEST_STATUSES.REQUESTED &&
stillTrialMaestro.ActivateStatus === scenario.initialActivateStatus &&
stillTrialMaestro.AccountType === scenario.initialAccountType;
console.log(` ${rollbackOK ? "PASS" : "FAIL"} transaction rollback preserved state`);
if (!rollbackOK) failures += 1;
continue;
}
throw error;
}
// Step 3: verify post-approval state
const afterApprove = await db.maestro.findUniqueOrThrow({
where: { MaestroID: maestroID },
select: {
ActivateStatus: true,
AvailableActivateDateTime: true,
AccountType: true,
PlayerCount: true,
},
});
const requestRow = await db.maestro_upgrade.findUniqueOrThrow({
where: { MaestroUpgradeID: maestroUpgradeID },
select: { Status: true },
});
const logRows = await db.maestro_log.findMany({
where: { MaestroID: maestroID },
select: { Type: true },
orderBy: { MaestroLogID: "asc" },
});
const nowPlus1yr = new Date();
nowPlus1yr.setFullYear(nowPlus1yr.getFullYear() + 1);
const checks: Array<[string, boolean, string]> = [
[
"maestro.ActivateStatus becomes ACTIVE",
afterApprove.ActivateStatus === ACTIVATE_STATUSES.ACTIVE,
`got ${afterApprove.ActivateStatus}`,
],
[
"maestro.AccountType becomes RequestedAccountType",
afterApprove.AccountType === scenario.requestedAccountType,
`got ${afterApprove.AccountType}, expected ${scenario.requestedAccountType}`,
],
[
"maestro.PlayerCount unchanged (upgrade should NOT touch PlayerCount)",
afterApprove.PlayerCount === 0,
`got ${afterApprove.PlayerCount}`,
],
[
"AvailableActivateDateTime ≈ now + 1 year",
Math.abs(afterApprove.AvailableActivateDateTime.getTime() - nowPlus1yr.getTime()) < 60_000,
`delta=${afterApprove.AvailableActivateDateTime.getTime() - nowPlus1yr.getTime()}ms`,
],
[
"upgrade request Status becomes APPLIED",
requestRow.Status === REQUEST_STATUSES.APPLIED,
`got ${requestRow.Status}`,
],
[
"logs include upgrade_maestro",
logRows.some((l) => l.Type === "upgrade_maestro"),
JSON.stringify(logRows.map((l) => l.Type)),
],
];
for (const [name, passed, detail] of checks) {
console.log(` ${passed ? "PASS" : "FAIL"} ${name} (${detail})`);
if (!passed) failures += 1;
}
} catch (error) {
failures += 1;
console.error(` ERROR:`, error);
} finally {
if (maestroID !== null) {
await db.maestro_upgrade.deleteMany({ where: { MaestroID: maestroID } });
await db.maestro_log.deleteMany({ where: { MaestroID: maestroID } });
await db.maestro.delete({ where: { MaestroID: maestroID } });
console.log(` cleanup: removed rows for MaestroID=${maestroID}`);
}
}
}
await db.$disconnect();
if (failures > 0) {
console.error(`\n${failures} check(s) failed.`);
process.exitCode = 1;
} else {
console.log(`\nAll checks passed.`);
}
}
main().catch((error) => {
console.error("Test run failed");
console.error(error);
process.exitCode = 1;
});