Release v0.1.6 2026-07-12

This commit is contained in:
Warren H
2026-07-12 23:20:15 -04:00
parent 35553c18dd
commit 122f159b94
55 changed files with 2229 additions and 587 deletions
+24 -39
View File
@@ -1,53 +1,38 @@
import { describe, expect, it } from "vitest";
import type { StorageAdapter } from "./saveRepository";
import { describe, expect, it, vi } from "vitest";
import { AccountRepository } from "./accountRepository";
import { OnlineApiError, type OnlineRepository } from "./onlineRepository";
function memoryStorage(): StorageAdapter {
const data = new Map<string, string>();
function onlineStub(overrides: Partial<OnlineRepository> = {}): OnlineRepository {
return {
getItem: (key) => data.get(key) ?? null,
setItem: (key, value) => { data.set(key, value); },
};
register: vi.fn(async (username: string) => ({ id: 1, username })),
login: vi.fn(async (username: string) => ({ id: 1, username })),
session: vi.fn(async () => null),
logout: vi.fn(async () => undefined),
...overrides,
} as unknown as OnlineRepository;
}
const testHasher = async (password: string, salt: string) => {
const checksum = [...password].reduce((total, character) => total + character.charCodeAt(0), 0);
return `derived:${salt}:${checksum}`;
};
describe("AccountRepository", () => {
it("requires both a username and password", async () => {
const repository = new AccountRepository(memoryStorage(), testHasher, () => "salt");
it("requires both username and password before contacting server", async () => {
const online = onlineStub();
const repository = new AccountRepository(online);
await expect(repository.create("", "secret")).resolves.toEqual({ ok: false, reason: "missing-credentials" });
await expect(repository.create("healer", "")).resolves.toEqual({ ok: false, reason: "missing-credentials" });
await expect(repository.authenticate("healer", "")).resolves.toEqual({ ok: false, reason: "missing-credentials" });
expect(online.register).not.toHaveBeenCalled();
});
it("requires account creation before sign-in", async () => {
const repository = new AccountRepository(memoryStorage(), testHasher, () => "salt");
await expect(repository.authenticate("new-healer", "secret")).resolves.toEqual({ ok: false, reason: "account-not-found" });
await expect(repository.create("new-healer", "secret")).resolves.toEqual({ ok: true, username: "new-healer" });
await expect(repository.authenticate("new-healer", "secret")).resolves.toEqual({ ok: true, username: "new-healer" });
it("creates and authenticates real server accounts", async () => {
const repository = new AccountRepository(onlineStub());
await expect(repository.create("Wayfinder", "long-password")).resolves.toEqual({ ok: true, username: "Wayfinder" });
await expect(repository.authenticate("Wayfinder", "long-password")).resolves.toEqual({ ok: true, username: "Wayfinder" });
});
it("rejects an incorrect password and duplicate account names", async () => {
const repository = new AccountRepository(memoryStorage(), testHasher, () => "salt");
await repository.create("Wayfinder", "correct");
await expect(repository.authenticate("wayfinder", "wrong")).resolves.toEqual({ ok: false, reason: "invalid-password" });
await expect(repository.create(" wayfinder ", "another")).resolves.toEqual({ ok: false, reason: "account-exists" });
});
it("persists only a derived password verifier", async () => {
const storage = memoryStorage();
const repository = new AccountRepository(storage, testHasher, () => "unique-salt");
await repository.create("healer", "plaintext-secret");
const persisted = storage.getItem("i-want-to-heal:accounts:v1") ?? "";
expect(persisted).toContain("derived:unique-salt:");
expect(persisted).not.toContain("plaintext-secret");
expect(JSON.parse(persisted).healer).not.toHaveProperty("password");
it("maps server conflicts, invalid credentials, and outages", async () => {
const conflict = new AccountRepository(onlineStub({ register: vi.fn(async () => { throw new OnlineApiError("exists", 409); }) }));
await expect(conflict.create("Wayfinder", "long-password")).resolves.toMatchObject({ ok: false, reason: "account-exists" });
const invalid = new AccountRepository(onlineStub({ login: vi.fn(async () => { throw new OnlineApiError("bad login", 401); }) }));
await expect(invalid.authenticate("Wayfinder", "wrong-password")).resolves.toMatchObject({ ok: false, reason: "invalid-password" });
const outage = new AccountRepository(onlineStub({ login: vi.fn(async () => { throw new OnlineApiError("offline", 0); }) }));
await expect(outage.authenticate("Wayfinder", "long-password")).resolves.toMatchObject({ ok: false, reason: "server-unavailable" });
});
});