deadhead-space-freight-tycoon/packages/engine/test/save-code.test.ts

125 lines
4.5 KiB
TypeScript

import { migrateSave } from "@deadhead/schema";
import { describe, expect, it } from "vitest";
import { fullSave } from "../../schema/test/save-fixture";
import {
decodeSave,
encodeSave,
GameEngine,
MemoryBackend,
SAVE_CODE_PREFIX,
SaveStorage,
type GameEngineOptions,
} from "../src/index";
import {
getState,
readSave,
signUpOwner,
setCredits,
TestClock,
testContract,
putBoard,
} from "./support";
function freshDevice(migrate?: GameEngineOptions["migrate"]) {
const clock = new TestClock();
const app = new GameEngine({
storage: new SaveStorage(new MemoryBackend()),
clock: { now: () => clock.now },
migrate,
});
return { app, clock };
}
describe("save export and import (Step 14.10, decision 0.5.92)", () => {
it("round-trips a save through the code", async () => {
const save = fullSave();
const code = await encodeSave(save);
expect(code.startsWith(SAVE_CODE_PREFIX)).toBe(true);
expect(code).toMatch(/^[\w.-]+$/);
expect(await decodeSave(code)).toEqual({ ok: true, save });
// Line breaks and spaces from pasting don't matter.
const wrapped = code.replace(/(.{40})/g, "$1\n ");
expect(await decodeSave(wrapped)).toEqual({ ok: true, save });
});
it("reproduces the same state on a fresh device", async () => {
const home = freshDevice();
const { shipId, avatarId } = await signUpOwner(home.app);
await setCredits(home.app, 12_345);
const contract = testContract();
await putBoard(home.app, [contract]);
await home.app.call("createDispatch", {
kind: "contract",
contractId: contract.id,
shipId,
pilotId: avatarId,
});
const before = await getState(home.app);
const { code } = await home.app.call("exportSave", {});
const other = freshDevice();
other.clock.now = home.clock.now;
const imported = await other.app.call("importSave", { code });
expect(imported.state).toEqual(before);
expect(await getState(other.app)).toEqual(before);
expect(await readSave(other.app)).toEqual(await readSave(home.app));
});
it("replaces the company already on the device", async () => {
const home = freshDevice();
await signUpOwner(home.app);
const { code } = await home.app.call("exportSave", {});
const other = freshDevice();
await signUpOwner(other.app);
const { playerId } = await other.app.call("importSave", { code });
expect(playerId).toBe((await readSave(home.app)).saveId);
expect((await getState(other.app)).company).toEqual((await getState(home.app)).company);
});
it("migrates an older save on import", async () => {
// Only save version 1 exists so far, so an older format is simulated: this
// build's migration fills in what the "older" export lacked.
const old = fullSave() as Record<string, unknown>;
delete old.stats;
const code = await encodeSave(old as never);
const migrations: unknown[] = [];
const { app } = freshDevice((raw) => {
migrations.push(raw);
return migrateSave({
...(raw as object),
stats: { contractDeliveries: 0, priorityDeliveries: 0 },
});
});
const { state } = await app.call("importSave", { code });
expect(migrations).toHaveLength(1);
expect(state.company.name).toBe(fullSave().company.name);
expect((await readSave(app)).stats).toEqual({ contractDeliveries: 0, priorityDeliveries: 0 });
});
it.each([
["not a code", "hello there", "That isn't a Deadhead save code."],
["damaged", `${SAVE_CODE_PREFIX}AAAAbroken`, "The save code is incomplete or damaged."],
])("refuses %s and changes nothing", async (_label, code, message) => {
const home = freshDevice();
await signUpOwner(home.app);
const before = await readSave(home.app);
await expect(home.app.call("importSave", { code })).rejects.toMatchObject({
code: "invalid-save",
message,
});
expect(await readSave(home.app)).toEqual(before);
});
it("refuses a code that decodes to something that isn't a save", async () => {
const { app } = freshDevice();
const code = await encodeSave({ hello: "world" } as never);
await expect(app.call("importSave", { code })).rejects.toMatchObject({ code: "invalid-save" });
await expect(app.call("getPlayerState", {})).rejects.toMatchObject({ code: "unauthorized" });
});
it("has nothing to export before a company exists", async () => {
const { app } = freshDevice();
await expect(app.call("exportSave", {})).rejects.toMatchObject({ code: "unauthorized" });
});
});