125 lines
4.5 KiB
TypeScript
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" });
|
|
});
|
|
});
|