@roster-lock/match-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +11 -0
- package/dist/authentication.js +53 -0
- package/dist/authentication.js.map +1 -0
- package/dist/config/constants.js +12 -0
- package/dist/config/constants.js.map +1 -0
- package/dist/config/findFile.js +32 -0
- package/dist/config/findFile.js.map +1 -0
- package/dist/config/index.js +29 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/manage.js +41 -0
- package/dist/config/manage.js.map +1 -0
- package/dist/cors.js +20 -0
- package/dist/cors.js.map +1 -0
- package/dist/editor-support/index.js +247 -0
- package/dist/editor-support/index.js.map +1 -0
- package/dist/handle-room/version-1/file-routes.js +53 -0
- package/dist/handle-room/version-1/file-routes.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/MultiAbort.js +76 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/MultiAbort.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/ensurePieceExists.js +218 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/ensurePieceExists.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/getVersions.js +18 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/getVersions.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/index.js +16 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/index.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/schema.js +370 -0
- package/dist/handle-room/version-1/globals/FolderDB/SQLFolderDB/schema.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/index.js +18 -0
- package/dist/handle-room/version-1/globals/FolderDB/index.js.map +1 -0
- package/dist/handle-room/version-1/globals/FolderDB/types.js +3 -0
- package/dist/handle-room/version-1/globals/FolderDB/types.js.map +1 -0
- package/dist/handle-room/version-1/globals/Room/WebsocketRoom.js +95 -0
- package/dist/handle-room/version-1/globals/Room/WebsocketRoom.js.map +1 -0
- package/dist/handle-room/version-1/globals/Room/index.js +18 -0
- package/dist/handle-room/version-1/globals/Room/index.js.map +1 -0
- package/dist/handle-room/version-1/globals/Room/room.js +3 -0
- package/dist/handle-room/version-1/globals/Room/room.js.map +1 -0
- package/dist/handle-room/version-1/globals/index.js +20 -0
- package/dist/handle-room/version-1/globals/index.js.map +1 -0
- package/dist/handle-room/version-1/globals/types.js +3 -0
- package/dist/handle-room/version-1/globals/types.js.map +1 -0
- package/dist/handle-room/version-1/handleDownloads/DownloadTracker.js +83 -0
- package/dist/handle-room/version-1/handleDownloads/DownloadTracker.js.map +1 -0
- package/dist/handle-room/version-1/handleDownloads/index.js +35 -0
- package/dist/handle-room/version-1/handleDownloads/index.js.map +1 -0
- package/dist/handle-room/version-1/handleDownloads/types.js +3 -0
- package/dist/handle-room/version-1/handleDownloads/types.js.map +1 -0
- package/dist/handle-room/version-1/handleRoomSelections/encryption.js +21 -0
- package/dist/handle-room/version-1/handleRoomSelections/encryption.js.map +1 -0
- package/dist/handle-room/version-1/handleRoomSelections/random.js +12 -0
- package/dist/handle-room/version-1/handleRoomSelections/random.js.map +1 -0
- package/dist/handle-room/version-1/index.js +28 -0
- package/dist/handle-room/version-1/index.js.map +1 -0
- package/dist/handle-room/version-1/piece-sort.js +71 -0
- package/dist/handle-room/version-1/piece-sort.js.map +1 -0
- package/dist/handle-room/version-1/room-handler-bridge/exchange-and-download-selections.js +89 -0
- package/dist/handle-room/version-1/room-handler-bridge/exchange-and-download-selections.js.map +1 -0
- package/dist/handle-room/version-1/room-handler-bridge/http-handler.js +19 -0
- package/dist/handle-room/version-1/room-handler-bridge/http-handler.js.map +1 -0
- package/dist/handle-room/version-1/room-handler-bridge/steps.js +170 -0
- package/dist/handle-room/version-1/room-handler-bridge/steps.js.map +1 -0
- package/dist/handle-room/version-1/room-handler-bridge/ws-handler.js +28 -0
- package/dist/handle-room/version-1/room-handler-bridge/ws-handler.js.map +1 -0
- package/dist/handle-room/version-1/schema/count.js +16 -0
- package/dist/handle-room/version-1/schema/count.js.map +1 -0
- package/dist/handle-room/version-1/schema/lock.js +46 -0
- package/dist/handle-room/version-1/schema/lock.js.map +1 -0
- package/dist/handle-room/version-1/schema/selected.js +13 -0
- package/dist/handle-room/version-1/schema/selected.js.map +1 -0
- package/dist/handle-room/version-1/select.js +94 -0
- package/dist/handle-room/version-1/select.js.map +1 -0
- package/dist/index.js +180 -0
- package/dist/index.js.map +1 -0
- package/dist/server.js +88 -0
- package/dist/server.js.map +1 -0
- package/dist/utils/fs.js +32 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/http-router.js +49 -0
- package/dist/utils/http-router.js.map +1 -0
- package/dist/utils/pagination.js +23 -0
- package/dist/utils/pagination.js.map +1 -0
- package/dist/utils/websocket-router.js +15 -0
- package/dist/utils/websocket-router.js.map +1 -0
- package/package.json +66 -0
- package/src/authentication.ts +48 -0
- package/src/config/constants.ts +7 -0
- package/src/config/findFile.ts +34 -0
- package/src/config/index.ts +17 -0
- package/src/config/manage.ts +48 -0
- package/src/cors.ts +23 -0
- package/src/editor-support/index.ts +289 -0
- package/src/handle-room/version-0/piece-download/availableGamePiece/index.ts +109 -0
- package/src/handle-room/version-0/piece-download/availableGamePiece/isUpToDate.ts +78 -0
- package/src/handle-room/version-0/piece-download/downloadPieceAssets/downloadSingleAsset.ts +129 -0
- package/src/handle-room/version-0/piece-download/downloadPieceAssets/example.ts +116 -0
- package/src/handle-room/version-0/piece-download/downloadPieceAssets/index.ts +144 -0
- package/src/handle-room/version-0/piece-download/downloadPieceAssets/types.ts +32 -0
- package/src/handle-room/version-0/piece-download/downloadPieceAssets/validateAssetFile.ts +54 -0
- package/src/handle-room/version-0/piece-download/index.ts +5 -0
- package/src/handle-room/version-0/piece-selection/constants.ts +39 -0
- package/src/handle-room/version-0/piece-selection/handleRoomSelections.ts +202 -0
- package/src/handle-room/version-0/piece-selection/index.ts +31 -0
- package/src/handle-room/version-0/piece-selection/roomErrors.ts +25 -0
- package/src/handle-room/version-0/piece-selection/steps/encrypton.ts +70 -0
- package/src/handle-room/version-0/piece-selection/steps/final-selection.ts +72 -0
- package/src/handle-room/version-0/piece-selection/steps/player-selection.ts +100 -0
- package/src/handle-room/version-1/constants.ts +44 -0
- package/src/handle-room/version-1/file-routes.ts +68 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/MultiAbort.ts +83 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/ensurePieceExists.ts +317 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/getVersions.ts +31 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/index.ts +13 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/schema.ts +547 -0
- package/src/handle-room/version-1/globals/FolderDB/SQLFolderDB/types.ts +6 -0
- package/src/handle-room/version-1/globals/FolderDB/index.ts +3 -0
- package/src/handle-room/version-1/globals/FolderDB/types.ts +72 -0
- package/src/handle-room/version-1/globals/Room/WebsocketRoom.ts +118 -0
- package/src/handle-room/version-1/globals/Room/index.ts +3 -0
- package/src/handle-room/version-1/globals/Room/room.ts +8 -0
- package/src/handle-room/version-1/globals/index.ts +4 -0
- package/src/handle-room/version-1/globals/types.ts +7 -0
- package/src/handle-room/version-1/handleDownloads/DownloadTracker.ts +98 -0
- package/src/handle-room/version-1/handleDownloads/index.ts +41 -0
- package/src/handle-room/version-1/handleDownloads/types.ts +9 -0
- package/src/handle-room/version-1/handleRoomSelections/encryption.ts +29 -0
- package/src/handle-room/version-1/handleRoomSelections/random.ts +9 -0
- package/src/handle-room/version-1/index.ts +33 -0
- package/src/handle-room/version-1/piece-sort.ts +96 -0
- package/src/handle-room/version-1/room-handler-bridge/exchange-and-download-selections.ts +125 -0
- package/src/handle-room/version-1/room-handler-bridge/http-handler.ts +16 -0
- package/src/handle-room/version-1/room-handler-bridge/server.pseudo.ts +85 -0
- package/src/handle-room/version-1/room-handler-bridge/steps.ts +255 -0
- package/src/handle-room/version-1/room-handler-bridge/ws-handler.ts +30 -0
- package/src/handle-room/version-1/room-handler-state/RoomState.ts +132 -0
- package/src/handle-room/version-1/room-handler-state/bind-to-websocket.ts +93 -0
- package/src/handle-room/version-1/room-handler-state/handleRoomSelections.ts +71 -0
- package/src/handle-room/version-1/room-handler-state/handleRoomWithErrors.ts +55 -0
- package/src/handle-room/version-1/room-handler-state/roomErrors.ts +35 -0
- package/src/handle-room/version-1/room-handler-state/user-selection.ts +98 -0
- package/src/handle-room/version-1/schema/count.ts +13 -0
- package/src/handle-room/version-1/schema/lock.ts +47 -0
- package/src/handle-room/version-1/schema/selected.ts +14 -0
- package/src/handle-room/version-1/select.ts +130 -0
- package/src/handle-room/version-1/validateUnknown.ts +11 -0
- package/src/index.old.ts +71 -0
- package/src/index.ts +231 -0
- package/src/server.ts +85 -0
- package/src/utils/fs.ts +43 -0
- package/src/utils/http-router.ts +55 -0
- package/src/utils/pagination.ts +17 -0
- package/src/utils/websocket-router.ts +23 -0
- package/tests/integration/editor-pieces-search.test.ts +147 -0
- package/tests/integration/game-complete.test.ts +72 -0
- package/tests/integration/handle-full-selection.test.ts +143 -0
- package/tests/integration/helpers/bridge.ts +22 -0
- package/tests/integration/helpers/fakeFolderDB.ts +37 -0
- package/tests/integration/helpers/fakePluginRuntime.ts +46 -0
- package/tests/integration/helpers/lockConfig.ts +71 -0
- package/tests/integration/helpers/make-tar-gz.ts +41 -0
- package/tests/integration/helpers/piece.ts +98 -0
- package/tests/integration/helpers/plugin-dir.ts +26 -0
- package/tests/integration/helpers/room.ts +42 -0
- package/tests/integration/helpers/server.ts +51 -0
- package/tests/integration/helpers/validLockConfig.ts +80 -0
- package/tests/integration/piece-download-and-list.test.ts +194 -0
- package/tests/integration/piece-ensure-ws.test.ts +256 -0
- package/tests/integration/piece-files.test.ts +139 -0
- package/tests/integration/piece-sort-list.test.ts +113 -0
- package/tests/integration/protocol.test.ts +129 -0
- package/tests/integration/root-and-auth.test.ts +100 -0
- package/tests/integration/sync-dl.test.ts +62 -0
- package/tests/tsconfig.json +14 -0
- package/tsconfig.json +22 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
+
import { startTestServer, TestServer, errorBody } from "./helpers/server";
|
|
3
|
+
import { makeTempFolder, cleanupFolder } from "./helpers/piece";
|
|
4
|
+
import { createFixturePluginDir } from "./helpers/plugin-dir";
|
|
5
|
+
import { makeValidLockConfig } from "./helpers/validLockConfig";
|
|
6
|
+
|
|
7
|
+
describe("GET /v1/piece/sort-list/available", () => {
|
|
8
|
+
const cleanups: Array<() => Promise<void> | void> = [];
|
|
9
|
+
afterEach(async () => {
|
|
10
|
+
while (cleanups.length) await cleanups.pop()!();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function get(server: TestServer) {
|
|
14
|
+
return fetch(`${server.httpUrl}/v1/piece/sort-list/available`, {
|
|
15
|
+
headers: { Authorization: `Bearer ${server.authCode}` },
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
it("returns [] when no piece-selection-sort plugins are installed", async () => {
|
|
20
|
+
const folder = await makeTempFolder();
|
|
21
|
+
cleanups.push(() => cleanupFolder(folder));
|
|
22
|
+
const server = await startTestServer(folder);
|
|
23
|
+
cleanups.push(() => server.close());
|
|
24
|
+
|
|
25
|
+
const res = await get(server);
|
|
26
|
+
expect(res.status).toBe(200);
|
|
27
|
+
expect(await res.json()).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("lists installed piece-selection-sort plugins with their publicInfo, ignoring other plugin types", async () => {
|
|
31
|
+
const fixture = await createFixturePluginDir([
|
|
32
|
+
"@roster-lock/piece-selection-sort-most-used-locally",
|
|
33
|
+
"@roster-lock/piece-selection-sort-highest-winrate-locally",
|
|
34
|
+
"@roster-lock/dl-protocol-http",
|
|
35
|
+
]);
|
|
36
|
+
cleanups.push(fixture.cleanup);
|
|
37
|
+
|
|
38
|
+
const folder = await makeTempFolder();
|
|
39
|
+
cleanups.push(() => cleanupFolder(folder));
|
|
40
|
+
const server = await startTestServer(folder, undefined, fixture.pluginDir);
|
|
41
|
+
cleanups.push(() => server.close());
|
|
42
|
+
|
|
43
|
+
const res = await get(server);
|
|
44
|
+
expect(res.status).toBe(200);
|
|
45
|
+
const body = await res.json() as Array<{ pluginName: string, publicInfo: unknown }>;
|
|
46
|
+
expect(body.map((p) => p.pluginName).sort()).toEqual([
|
|
47
|
+
"@roster-lock/piece-selection-sort-highest-winrate-locally",
|
|
48
|
+
"@roster-lock/piece-selection-sort-most-used-locally",
|
|
49
|
+
]);
|
|
50
|
+
for (const entry of body) {
|
|
51
|
+
expect(entry.publicInfo).toMatchObject({ title: expect.any(String), description: expect.any(String) });
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("POST /v1/piece/sort-list/plugin/:pluginName", () => {
|
|
57
|
+
const cleanups: Array<() => Promise<void> | void> = [];
|
|
58
|
+
afterEach(async () => {
|
|
59
|
+
while (cleanups.length) await cleanups.pop()!();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
function post(server: TestServer, pluginName: string, body: unknown) {
|
|
63
|
+
return fetch(`${server.httpUrl}/v1/piece/sort-list/plugin/${encodeURIComponent(pluginName)}`, {
|
|
64
|
+
method: "POST",
|
|
65
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${server.authCode}` },
|
|
66
|
+
body: JSON.stringify(body),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function makeServer() {
|
|
71
|
+
const fixture = await createFixturePluginDir(["@roster-lock/piece-selection-sort-most-used-locally"]);
|
|
72
|
+
cleanups.push(fixture.cleanup);
|
|
73
|
+
const folder = await makeTempFolder();
|
|
74
|
+
cleanups.push(() => cleanupFolder(folder));
|
|
75
|
+
const server = await startTestServer(folder, undefined, fixture.pluginDir);
|
|
76
|
+
cleanups.push(() => server.close());
|
|
77
|
+
return server;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
it("400s on a malformed body", async () => {
|
|
81
|
+
const server = await makeServer();
|
|
82
|
+
const res = await post(server, "@roster-lock/piece-selection-sort-most-used-locally", {});
|
|
83
|
+
expect(res.status).toBe(400);
|
|
84
|
+
expect((await errorBody(res)).error).toBe("Bad Form");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("400s when lockConfig fails schema validation", async () => {
|
|
88
|
+
const server = await makeServer();
|
|
89
|
+
const res = await post(server, "@roster-lock/piece-selection-sort-most-used-locally", {
|
|
90
|
+
lockConfig: { not: "a valid config" }, pieceType: "character", logicIds: ["1.0.0"],
|
|
91
|
+
});
|
|
92
|
+
expect(res.status).toBe(400);
|
|
93
|
+
expect((await errorBody(res)).error).toBe("Invalid lockConfig");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("500s for a plugin that isn't installed", async () => {
|
|
97
|
+
const server = await makeServer();
|
|
98
|
+
const res = await post(server, "@roster-lock/not-installed", {
|
|
99
|
+
lockConfig: makeValidLockConfig(), pieceType: "character", logicIds: ["1.0.0"],
|
|
100
|
+
});
|
|
101
|
+
expect(res.status).toBe(500);
|
|
102
|
+
expect((await errorBody(res)).error).toMatch(/Plugin not installed/);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("passes the caller's logicIds through unchanged when the plugin has no ranking for them yet", async () => {
|
|
106
|
+
const server = await makeServer();
|
|
107
|
+
const res = await post(server, "@roster-lock/piece-selection-sort-most-used-locally", {
|
|
108
|
+
lockConfig: makeValidLockConfig(), pieceType: "character", logicIds: ["1.0.0", "2.0.0"],
|
|
109
|
+
});
|
|
110
|
+
expect(res.status).toBe(200);
|
|
111
|
+
expect(await res.json()).toEqual(["1.0.0", "2.0.0"]);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { bindStepsToBridge } from "../../src/handle-room/version-1/room-handler-bridge/steps";
|
|
3
|
+
import { wireBridgePair } from "./helpers/bridge";
|
|
4
|
+
import { driveRoomProtocol } from "./helpers/room";
|
|
5
|
+
import { FakeFolderDB } from "./helpers/fakeFolderDB";
|
|
6
|
+
import { FakePluginManager } from "./helpers/fakePluginRuntime";
|
|
7
|
+
import { makeLockConfig, makeHeroSelection } from "./helpers/lockConfig";
|
|
8
|
+
|
|
9
|
+
// These exercise the two "scary" parts of the sync-dl protocol - real
|
|
10
|
+
// runSelection (roster/requirement validation + personal merge) and real
|
|
11
|
+
// handleDownloads (recursive required-piece resolution + dedup) - without
|
|
12
|
+
// a relay server, WebSocket, or network download. bindStepsToBridge only
|
|
13
|
+
// needs a MessageBridge; wireBridgePair connects two of them directly, and
|
|
14
|
+
// driveRoomProtocol plays the relay's side of the protocol in-process
|
|
15
|
+
// (mirroring room-handler-bridge/server.pseudo.ts).
|
|
16
|
+
describe("bindStepsToBridge: runSelection + handleDownloads", () => {
|
|
17
|
+
it("runs the full protocol for two users picking the same piece, dedupes the shared nested requirement, and both agents converge on the same result", async () => {
|
|
18
|
+
const lockConfig = makeLockConfig();
|
|
19
|
+
const machines = [{ publicKey: "pk-a", playerCount: 1 }, { publicKey: "pk-b", playerCount: 1 }];
|
|
20
|
+
|
|
21
|
+
const pairA = wireBridgePair();
|
|
22
|
+
const pairB = wireBridgePair();
|
|
23
|
+
|
|
24
|
+
const dbA = new FakeFolderDB();
|
|
25
|
+
const dbB = new FakeFolderDB();
|
|
26
|
+
const pm = new FakePluginManager();
|
|
27
|
+
|
|
28
|
+
const resultAPromise = bindStepsToBridge({
|
|
29
|
+
bridge: pairA.agentSide,
|
|
30
|
+
fileDB: dbA,
|
|
31
|
+
pluginRuntime: pm,
|
|
32
|
+
machines,
|
|
33
|
+
ownMachinePublicKey: "pk-a",
|
|
34
|
+
ownSelections: { 0: makeHeroSelection() },
|
|
35
|
+
lockConfig,
|
|
36
|
+
gameControlledSelections: {},
|
|
37
|
+
});
|
|
38
|
+
const resultBPromise = bindStepsToBridge({
|
|
39
|
+
bridge: pairB.agentSide,
|
|
40
|
+
fileDB: dbB,
|
|
41
|
+
pluginRuntime: pm,
|
|
42
|
+
machines,
|
|
43
|
+
ownMachinePublicKey: "pk-b",
|
|
44
|
+
ownSelections: { 0: makeHeroSelection() },
|
|
45
|
+
lockConfig,
|
|
46
|
+
gameControlledSelections: {},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await driveRoomProtocol([
|
|
50
|
+
{ bridge: pairA.roomSide, publicKey: "pk-a" },
|
|
51
|
+
{ bridge: pairB.roomSide, publicKey: "pk-b" },
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const [resultA, resultB] = await Promise.all([resultAPromise, resultBPromise]);
|
|
55
|
+
|
|
56
|
+
// runSelection: both agents decrypted the same inputs independently and
|
|
57
|
+
// must agree on the merged personal selection.
|
|
58
|
+
expect(resultA.finalSelection).toEqual(resultB.finalSelection);
|
|
59
|
+
expect(resultA.finalSelection.character).toEqual({
|
|
60
|
+
type: "personal",
|
|
61
|
+
value: {
|
|
62
|
+
"pk-a:0": makeHeroSelection().character,
|
|
63
|
+
"pk-b:0": makeHeroSelection().character,
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// handleDownloads: both users chose the same piece (which itself
|
|
68
|
+
// requires the same nested weapon) - each agent must dedupe that down
|
|
69
|
+
// to a single download call per piece, not one per reference.
|
|
70
|
+
for (const db of [dbA, dbB]) {
|
|
71
|
+
expect(db.calls).toEqual([
|
|
72
|
+
{ pieceType: "character", pieceId: "hero-1" },
|
|
73
|
+
{ pieceType: "weapon", pieceId: "sword-1" },
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
expect(resultA.downloadResults).toEqual({
|
|
78
|
+
character: { "hero-1": { pieceType: "character", pieceId: "hero-1", pieceVersions: { logic: "1.0.0", media: "1.0.0" }, folder: "/fake/character/hero-1" } },
|
|
79
|
+
weapon: { "sword-1": { pieceType: "weapon", pieceId: "sword-1", pieceVersions: { logic: "1.0.0", media: "1.0.0" }, folder: "/fake/weapon/sword-1" } },
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("rejects when a user selects a piece missing from the roster (runSelection validation)", async () => {
|
|
84
|
+
const lockConfig = makeLockConfig();
|
|
85
|
+
const machines = [{ publicKey: "pk-a", playerCount: 1 }, { publicKey: "pk-b", playerCount: 1 }];
|
|
86
|
+
|
|
87
|
+
const pairA = wireBridgePair();
|
|
88
|
+
const pairB = wireBridgePair();
|
|
89
|
+
const pm = new FakePluginManager();
|
|
90
|
+
|
|
91
|
+
const badSelection = { character: [{ id: "not-a-real-piece", required: { weapon: { mandatory: [], selectable: [] } } }] };
|
|
92
|
+
|
|
93
|
+
const resultAPromise = bindStepsToBridge({
|
|
94
|
+
bridge: pairA.agentSide,
|
|
95
|
+
fileDB: new FakeFolderDB(),
|
|
96
|
+
pluginRuntime: pm,
|
|
97
|
+
machines,
|
|
98
|
+
ownMachinePublicKey: "pk-a",
|
|
99
|
+
ownSelections: { 0: badSelection },
|
|
100
|
+
lockConfig,
|
|
101
|
+
gameControlledSelections: {},
|
|
102
|
+
});
|
|
103
|
+
const resultBPromise = bindStepsToBridge({
|
|
104
|
+
bridge: pairB.agentSide,
|
|
105
|
+
fileDB: new FakeFolderDB(),
|
|
106
|
+
pluginRuntime: pm,
|
|
107
|
+
machines,
|
|
108
|
+
ownMachinePublicKey: "pk-b",
|
|
109
|
+
ownSelections: { 0: makeHeroSelection() },
|
|
110
|
+
lockConfig,
|
|
111
|
+
gameControlledSelections: {},
|
|
112
|
+
});
|
|
113
|
+
// both agents run runSelection independently over the same decrypted
|
|
114
|
+
// inputs, so the bad selection fails validation on both sides.
|
|
115
|
+
resultAPromise.catch(() => {});
|
|
116
|
+
resultBPromise.catch(() => {});
|
|
117
|
+
|
|
118
|
+
await driveRoomProtocol([
|
|
119
|
+
{ bridge: pairA.roomSide, publicKey: "pk-a" },
|
|
120
|
+
{ bridge: pairB.roomSide, publicKey: "pk-b" },
|
|
121
|
+
]).catch(() => {});
|
|
122
|
+
|
|
123
|
+
// bindStepsToBridge rejects with whatever value the room's "error" event
|
|
124
|
+
// carried (steps.ts: bridge.onEvent("error", (error) => reject(error))) -
|
|
125
|
+
// a plain string here, not an Error instance.
|
|
126
|
+
await expect(resultAPromise).rejects.toBe("Piece not-a-real-piece is not in the roster");
|
|
127
|
+
await expect(resultBPromise).rejects.toBe("Piece not-a-real-piece is not in the roster");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { startTestServer, TestServer, errorBody } from "./helpers/server";
|
|
3
|
+
import { makeTempFolder, cleanupFolder } from "./helpers/piece";
|
|
4
|
+
|
|
5
|
+
describe("GET /", () => {
|
|
6
|
+
let server: TestServer;
|
|
7
|
+
let folder: string;
|
|
8
|
+
beforeAll(async () => { folder = await makeTempFolder(); server = await startTestServer(folder); });
|
|
9
|
+
afterAll(async () => { await server.close(); await cleanupFolder(folder); });
|
|
10
|
+
|
|
11
|
+
it("responds with hello world and requires no auth", async () => {
|
|
12
|
+
const res = await fetch(`${server.httpUrl}/`);
|
|
13
|
+
expect(res.status).toBe(200);
|
|
14
|
+
await expect(res.json()).resolves.toEqual({ hello: "world" });
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe("POST /validate-authcode", () => {
|
|
19
|
+
let server: TestServer;
|
|
20
|
+
let folder: string;
|
|
21
|
+
beforeAll(async () => { folder = await makeTempFolder(); server = await startTestServer(folder); });
|
|
22
|
+
afterAll(async () => { await server.close(); await cleanupFolder(folder); });
|
|
23
|
+
|
|
24
|
+
it("returns true for the real auth code, no auth header required", async () => {
|
|
25
|
+
const res = await fetch(`${server.httpUrl}/validate-authcode`, {
|
|
26
|
+
method: "POST",
|
|
27
|
+
headers: { "Content-Type": "application/json" },
|
|
28
|
+
body: JSON.stringify({ authCode: server.authCode }),
|
|
29
|
+
});
|
|
30
|
+
expect(res.status).toBe(200);
|
|
31
|
+
await expect(res.json()).resolves.toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("returns false for a wrong auth code", async () => {
|
|
35
|
+
const res = await fetch(`${server.httpUrl}/validate-authcode`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: { "Content-Type": "application/json" },
|
|
38
|
+
body: JSON.stringify({ authCode: "not-the-real-code" }),
|
|
39
|
+
});
|
|
40
|
+
expect(res.status).toBe(200);
|
|
41
|
+
await expect(res.json()).resolves.toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("400s on a malformed body", async () => {
|
|
45
|
+
const res = await fetch(`${server.httpUrl}/validate-authcode`, {
|
|
46
|
+
method: "POST",
|
|
47
|
+
headers: { "Content-Type": "application/json" },
|
|
48
|
+
body: JSON.stringify({ notAuthCode: 123 }),
|
|
49
|
+
});
|
|
50
|
+
expect(res.status).toBe(400);
|
|
51
|
+
expect((await errorBody(res)).error).toBe("Bad body");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("/v1 auth gate", () => {
|
|
56
|
+
let server: TestServer;
|
|
57
|
+
let folder: string;
|
|
58
|
+
beforeAll(async () => { folder = await makeTempFolder(); server = await startTestServer(folder); });
|
|
59
|
+
afterAll(async () => { await server.close(); await cleanupFolder(folder); });
|
|
60
|
+
|
|
61
|
+
const v1Url = () => `${server.httpUrl}/v1/piece/asset-files`;
|
|
62
|
+
|
|
63
|
+
it("401s with no auth provided", async () => {
|
|
64
|
+
const res = await fetch(v1Url(), { method: "POST", body: "{}" });
|
|
65
|
+
expect(res.status).toBe(401);
|
|
66
|
+
expect((await errorBody(res)).error).toBe("User Required");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("401s with a wrong Authorization header", async () => {
|
|
70
|
+
const res = await fetch(v1Url(), {
|
|
71
|
+
method: "POST",
|
|
72
|
+
headers: { Authorization: "Bearer wrong-code" },
|
|
73
|
+
body: "{}",
|
|
74
|
+
});
|
|
75
|
+
expect(res.status).toBe(401);
|
|
76
|
+
expect((await errorBody(res)).error).toBe("Invalid User");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("401s with a wrong ?authorization= query param", async () => {
|
|
80
|
+
const res = await fetch(`${v1Url()}?authorization=wrong-code`, { method: "POST", body: "{}" });
|
|
81
|
+
expect(res.status).toBe(401);
|
|
82
|
+
expect((await errorBody(res)).error).toBe("Invalid User");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("passes through with a correct Authorization header (reaches the route, fails its own validation instead)", async () => {
|
|
86
|
+
const res = await fetch(v1Url(), {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: { Authorization: `Bearer ${server.authCode}` },
|
|
89
|
+
body: "{}",
|
|
90
|
+
});
|
|
91
|
+
expect(res.status).toBe(400);
|
|
92
|
+
expect((await errorBody(res)).error).toBe("Bad Form");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("passes through with a correct ?authorization= query param", async () => {
|
|
96
|
+
const res = await fetch(`${v1Url()}?authorization=${server.authCode}`, { method: "POST", body: "{}" });
|
|
97
|
+
expect(res.status).toBe(400);
|
|
98
|
+
expect((await errorBody(res)).error).toBe("Bad Form");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { once } from "node:events";
|
|
3
|
+
import { WebSocket } from "ws";
|
|
4
|
+
import { MessageBridge } from "@roster-lock/utils";
|
|
5
|
+
import { startTestServer, TestServer, errorBody } from "./helpers/server";
|
|
6
|
+
import { makeTempFolder, cleanupFolder } from "./helpers/piece";
|
|
7
|
+
|
|
8
|
+
describe("POST /v1/sync-dl", () => {
|
|
9
|
+
let server: TestServer;
|
|
10
|
+
let folder: string;
|
|
11
|
+
beforeAll(async () => { folder = await makeTempFolder(); server = await startTestServer(folder); });
|
|
12
|
+
afterAll(async () => { await server.close(); await cleanupFolder(folder); });
|
|
13
|
+
|
|
14
|
+
it("500s on a malformed body (castRoomRequest throws a plain Error)", async () => {
|
|
15
|
+
const res = await fetch(`${server.httpUrl}/v1/sync-dl`, {
|
|
16
|
+
method: "POST",
|
|
17
|
+
headers: {
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
Authorization: `Bearer ${server.authCode}`,
|
|
20
|
+
},
|
|
21
|
+
body: JSON.stringify({ not: "a valid room request" }),
|
|
22
|
+
});
|
|
23
|
+
expect(res.status).toBe(500);
|
|
24
|
+
expect((await errorBody(res)).error).toBe("Invalid request");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("WS /v1/sync-dl", () => {
|
|
29
|
+
let server: TestServer;
|
|
30
|
+
let folder: string;
|
|
31
|
+
beforeAll(async () => { folder = await makeTempFolder(); server = await startTestServer(folder); });
|
|
32
|
+
afterAll(async () => { await server.close(); await cleanupFolder(folder); });
|
|
33
|
+
|
|
34
|
+
it("terminates the connection with no auth, without ever sending a ready event", async () => {
|
|
35
|
+
const ws = new WebSocket(`${server.wsUrl}/v1/sync-dl`);
|
|
36
|
+
let receivedMessage = false;
|
|
37
|
+
ws.on("message", () => { receivedMessage = true; });
|
|
38
|
+
ws.on("error", () => {}); // abrupt terminate() can surface as ECONNRESET - not the point of this test
|
|
39
|
+
|
|
40
|
+
await once(ws, "close");
|
|
41
|
+
expect(receivedMessage).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("sends a ready event once authenticated, and rejects an invalid connect-to-relay request", async () => {
|
|
45
|
+
const ws = new WebSocket(`${server.wsUrl}/v1/sync-dl`, {
|
|
46
|
+
headers: { Authorization: `Bearer ${server.authCode}` },
|
|
47
|
+
});
|
|
48
|
+
try {
|
|
49
|
+
const bridge = new MessageBridge((message) => ws.send(JSON.stringify(message)));
|
|
50
|
+
const readyPromise = new Promise<void>((resolve) => bridge.onEvent("ready", () => resolve()));
|
|
51
|
+
ws.on("message", (data) => bridge.handleMessage(JSON.parse(data.toString())));
|
|
52
|
+
|
|
53
|
+
await once(ws, "open");
|
|
54
|
+
await readyPromise;
|
|
55
|
+
|
|
56
|
+
await expect(bridge.sendRequest("connect-to-relay", { not: "a valid room request" }))
|
|
57
|
+
.rejects.toBe("Invalid request");
|
|
58
|
+
} finally {
|
|
59
|
+
ws.close();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2024",
|
|
4
|
+
"module": "nodenext",
|
|
5
|
+
"moduleResolution": "nodenext",
|
|
6
|
+
"types": ["node", "vitest/globals"],
|
|
7
|
+
"lib": ["es2024"],
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"noEmit": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["./**/*.ts"]
|
|
14
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2024",
|
|
4
|
+
"module": "nodenext",
|
|
5
|
+
"moduleResolution": "nodenext",
|
|
6
|
+
"types": ["node"],
|
|
7
|
+
"lib": ["es2024"],
|
|
8
|
+
"sourceMap": true,
|
|
9
|
+
"rootDir": "./src",
|
|
10
|
+
"outDir": "./dist",
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"removeComments": true,
|
|
15
|
+
"noImplicitAny": true
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"src/index.ts"
|
|
19
|
+
],
|
|
20
|
+
"include": ["./typings", "./types", "./@types", "**/*.test.ts"],
|
|
21
|
+
"exclude": ["node_modules", "**/*.spec.ts", "tests"]
|
|
22
|
+
}
|