@serverkgg/bridge 0.1.2 → 0.1.3
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/package.json +3 -2
- package/rules/package.md +60 -0
- package/src/blog/articleFrontmatter.test.ts +10 -10
- package/src/blog/compile.test.ts +9 -9
- package/src/bot/botDeploy.test.ts +16 -16
- package/src/bot/botEnvironment.test.ts +8 -8
- package/src/bot/botIdentity.test.ts +12 -12
- package/src/bot/botPanel.test.ts +13 -13
- package/src/bot/botPanel.ts +4 -4
- package/src/bot/botState.test.ts +3 -3
- package/src/bot/botTerminal.test.ts +5 -5
- package/src/bot/botVariables.test.ts +8 -8
- package/src/bot/detect/botToken.test.ts +6 -6
- package/src/bot/detect/detect.test.ts +21 -21
- package/src/bot/detect/detectTable.test.ts +5 -5
- package/src/bot/detect/envExample.test.ts +2 -2
- package/src/bot/detect/envScan.test.ts +4 -4
- package/src/bot/detect/inferBotPlatform.test.ts +4 -4
- package/src/bot/detect/keys.test.ts +3 -3
- package/src/bot/detect/sensitiveKey.test.ts +5 -5
- package/src/bot/detect/serverkJson.test.ts +5 -5
- package/src/catalogs/curseforge.test.ts +35 -35
- package/src/catalogs/thunderstore.test.ts +9 -9
- package/src/cli/detectTable.test.ts +3 -3
- package/src/cli/link.test.ts +11 -11
- package/src/cli/main.ts +8 -0
- package/src/cli/rules.test.ts +198 -0
- package/src/cli/rules.ts +188 -0
- package/src/cli/validate.test.ts +52 -30
- package/src/cli/validate.ts +2 -0
- package/src/codecs/ini.test.ts +13 -13
- package/src/codecs/javaProperties.test.ts +11 -11
- package/src/codecs/sourceCfg.test.ts +14 -14
- package/src/codecs/xmlProperties.test.ts +10 -10
- package/src/core/bridgePort.test.ts +6 -6
- package/src/core/bridgeUserError.test.ts +4 -4
- package/src/core/bridgeValue.test.ts +5 -5
- package/src/core/describeDownloadFailure.test.ts +12 -12
- package/src/core/describeDownloadFailure.ts +10 -10
- package/src/guides/markdown.test.ts +31 -31
- package/src/install/installStamp.test.ts +9 -9
- package/src/manifest/extension.ts +36 -0
- package/src/manifest/index.ts +1 -0
- package/src/manifest/manifest.test.ts +208 -90
- package/src/manifest/manifest.ts +34 -0
- package/src/net/loopback.test.ts +10 -10
- package/src/presence/rosterSync.test.ts +7 -7
- package/src/probes/a2s.test.ts +5 -5
- package/src/probes/minecraftPing.test.ts +17 -17
- package/src/probes/raknetPing.test.ts +13 -13
- package/src/rcon/rconAccess.test.ts +20 -20
- package/src/rcon/rconAccess.ts +13 -13
- package/src/rcon/rconClient.test.ts +49 -49
- package/src/rcon/rconFrame.test.ts +14 -14
- package/src/rcon/rconIds.test.ts +4 -4
- package/src/rcon/rconWriter.test.ts +6 -6
- package/src/runtime/bridgeActivity.test.ts +15 -15
- package/src/runtime/bridgeBackupLease.test.ts +9 -9
- package/src/runtime/bridgeConfigReader.test.ts +5 -5
- package/src/runtime/bridgeConsoleHandler.test.ts +8 -8
- package/src/runtime/bridgeContext.test.ts +5 -5
- package/src/runtime/bridgeDriver.test.ts +94 -94
- package/src/runtime/bridgeEvents.test.ts +15 -15
- package/src/runtime/bridgeFiles.test.ts +54 -54
- package/src/runtime/bridgeInstall.test.ts +10 -10
- package/src/runtime/bridgeLiveConfig.test.ts +6 -6
- package/src/runtime/bridgePending.test.ts +6 -6
- package/src/runtime/bridgeRedaction.test.ts +16 -16
- package/src/runtime/bridgeRegistry.test.ts +52 -52
- package/src/runtime/bridgeRegistry.ts +2 -2
- package/src/runtime/bridgeServer.test.ts +31 -31
- package/src/runtime/bridgeSetup.test.ts +27 -27
- package/src/runtime/bridgeSetup.ts +1 -1
- package/src/runtime/bridgeSetupHandlers.test.ts +13 -13
- package/src/runtime/bridgeSignal.test.ts +7 -7
- package/src/runtime/bridgeSupervisor.test.ts +42 -42
- package/src/runtime/serve.ts +4 -4
- package/src/steam/avatars.test.ts +13 -13
- package/src/steam/steamApp.test.ts +8 -8
- package/src/steam/steamcmd.test.ts +17 -17
- package/src/steam/steamcmd.ts +4 -4
- package/src/steam/steamcmdNarrator.test.ts +4 -4
- package/src/steam/workshop.test.ts +5 -5
- package/src/utils/changedSettingKeys.test.ts +5 -5
- package/src/utils/describeExecFailure.test.ts +4 -4
- package/src/utils/describeExecFailure.ts +1 -1
- package/src/utils/sameSettingValue.test.ts +7 -7
- package/src/utils/token.test.ts +7 -7
- package/src/wine/protonCommand.test.ts +5 -5
- package/src/wine/wineCommand.test.ts +6 -6
- package/src/wine/winePrefix.test.ts +6 -6
- package/src/wire/activity.test.ts +8 -8
- package/src/wire/panel.test.ts +28 -28
package/src/manifest/manifest.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { PORT_BLOCK_MAX_COUNT, PortJoin, PortProtocol, socketProtocolsOf } from
|
|
|
3
3
|
import { BridgeIcon } from "../panel/bridgeIcon";
|
|
4
4
|
import { BRIDGE_OPTION_VALUE_LENGTH_LIMIT, BRIDGE_VISIBLE_VALUES_LIMIT } from "../protocol/bridgeLimits";
|
|
5
5
|
import { GameCategory } from "./category";
|
|
6
|
+
import { type ManifestExtension, manifestExtensionsSchema } from "./extension";
|
|
6
7
|
import { type GuideEntry, type GuideTag, guideEntrySchema, guideTagSchema } from "./guide";
|
|
7
8
|
import { IMAGE_NAMESPACE } from "./image";
|
|
8
9
|
import { PackageKind } from "./kind";
|
|
@@ -157,10 +158,12 @@ const manifestMetadataSchema = z.object({
|
|
|
157
158
|
category: z.enum(GameCategory),
|
|
158
159
|
logo: zAssetPath,
|
|
159
160
|
banner: zAssetPath,
|
|
161
|
+
extensions: manifestExtensionsSchema,
|
|
160
162
|
});
|
|
161
163
|
|
|
162
164
|
const botMetadataSchema = manifestMetadataSchema.omit({
|
|
163
165
|
category: true,
|
|
166
|
+
extensions: true,
|
|
164
167
|
});
|
|
165
168
|
|
|
166
169
|
const manifestResourcesSchema = z
|
|
@@ -764,6 +767,36 @@ const packageIssues = (manifest: PackageShape): CheckIssue[] => {
|
|
|
764
767
|
];
|
|
765
768
|
};
|
|
766
769
|
|
|
770
|
+
const extensionIssues = (extensions: ManifestExtension[]): CheckIssue[] => {
|
|
771
|
+
return [
|
|
772
|
+
...duplicateIssues(
|
|
773
|
+
extensions,
|
|
774
|
+
(extension) => extension.kind,
|
|
775
|
+
(index) => [
|
|
776
|
+
"metadata",
|
|
777
|
+
"extensions",
|
|
778
|
+
index,
|
|
779
|
+
"kind",
|
|
780
|
+
],
|
|
781
|
+
(key) => `extension "${key}" is declared twice`,
|
|
782
|
+
),
|
|
783
|
+
...extensions.flatMap((extension, extensionIndex) =>
|
|
784
|
+
duplicateIssues(
|
|
785
|
+
extension.sources,
|
|
786
|
+
(source) => source,
|
|
787
|
+
(index) => [
|
|
788
|
+
"metadata",
|
|
789
|
+
"extensions",
|
|
790
|
+
extensionIndex,
|
|
791
|
+
"sources",
|
|
792
|
+
index,
|
|
793
|
+
],
|
|
794
|
+
(key) => `source "${key}" is listed twice for ${extension.kind}`,
|
|
795
|
+
),
|
|
796
|
+
),
|
|
797
|
+
];
|
|
798
|
+
};
|
|
799
|
+
|
|
767
800
|
const secretIssues = (secrets: ManifestSecret[]): CheckIssue[] => {
|
|
768
801
|
return duplicateIssues(
|
|
769
802
|
secrets,
|
|
@@ -801,6 +834,7 @@ export const gameManifestSchema = z
|
|
|
801
834
|
|
|
802
835
|
ctx.issues.push(
|
|
803
836
|
...packageIssues(manifest),
|
|
837
|
+
...extensionIssues(manifest.metadata.extensions),
|
|
804
838
|
...secretIssues(manifest.secrets),
|
|
805
839
|
...presenceIssues(manifest.presence),
|
|
806
840
|
);
|
package/src/net/loopback.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect,
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { BridgeNetError } from "../core";
|
|
3
3
|
import { loopbackJson, loopbackText } from "./loopback";
|
|
4
4
|
import { LoopbackMethod } from "./loopbackMethod";
|
|
@@ -12,7 +12,7 @@ const serve = (handler: (request: Request) => Response | Promise<Response>) => {
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
describe("loopbackJson", () => {
|
|
15
|
-
|
|
15
|
+
it("reads a json body from a loopback server", async () => {
|
|
16
16
|
const server = serve(() =>
|
|
17
17
|
Response.json({
|
|
18
18
|
online: 3,
|
|
@@ -32,7 +32,7 @@ describe("loopbackJson", () => {
|
|
|
32
32
|
}
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
it("sends the method, headers and body it is given", async () => {
|
|
36
36
|
let seen = "";
|
|
37
37
|
|
|
38
38
|
const server = serve(async (request) => {
|
|
@@ -58,7 +58,7 @@ describe("loopbackJson", () => {
|
|
|
58
58
|
}
|
|
59
59
|
});
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
it("raises a net error carrying the status when the server refuses", async () => {
|
|
62
62
|
const server = serve(
|
|
63
63
|
() =>
|
|
64
64
|
new Response("nope", {
|
|
@@ -76,7 +76,7 @@ describe("loopbackJson", () => {
|
|
|
76
76
|
}
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
it("raises a net error when the body is not json", async () => {
|
|
80
80
|
const server = serve(() => new Response("not json"));
|
|
81
81
|
|
|
82
82
|
try {
|
|
@@ -89,7 +89,7 @@ describe("loopbackJson", () => {
|
|
|
89
89
|
}
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
it("times out against a server that never answers", async () => {
|
|
93
93
|
const server = serve(async () => {
|
|
94
94
|
await Bun.sleep(600);
|
|
95
95
|
|
|
@@ -109,7 +109,7 @@ describe("loopbackJson", () => {
|
|
|
109
109
|
});
|
|
110
110
|
|
|
111
111
|
describe("loopback guard", () => {
|
|
112
|
-
|
|
112
|
+
it("refuses a host that is not loopback", async () => {
|
|
113
113
|
for (const url of [
|
|
114
114
|
"https://api.curseforge.com/v1",
|
|
115
115
|
"http://10.0.0.5:8080/",
|
|
@@ -122,14 +122,14 @@ describe("loopback guard", () => {
|
|
|
122
122
|
}
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
it("refuses a scheme that is not http", async () => {
|
|
126
126
|
const error = await loopbackText("file:///etc/passwd").catch((reason: unknown) => reason);
|
|
127
127
|
|
|
128
128
|
expect(error).toBeInstanceOf(BridgeNetError);
|
|
129
129
|
expect((error as BridgeNetError).message).toContain("is not an http url");
|
|
130
130
|
});
|
|
131
131
|
|
|
132
|
-
|
|
132
|
+
it("refuses a header carrying a line break", async () => {
|
|
133
133
|
const error = await loopbackText("http://127.0.0.1:1/", {
|
|
134
134
|
headers: {
|
|
135
135
|
"x-token": "a\r\nx-admin: yes",
|
|
@@ -140,7 +140,7 @@ describe("loopback guard", () => {
|
|
|
140
140
|
expect((error as BridgeNetError).message).toContain("line break");
|
|
141
141
|
});
|
|
142
142
|
|
|
143
|
-
|
|
143
|
+
it("accepts localhost", async () => {
|
|
144
144
|
const server = serve(() => new Response("pong"));
|
|
145
145
|
|
|
146
146
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { beforeEach, describe, expect,
|
|
1
|
+
import { beforeEach, describe, expect, it } from "bun:test";
|
|
2
2
|
import type { BridgeContext } from "../context";
|
|
3
3
|
import type { BridgeValues } from "../core";
|
|
4
4
|
import { BridgeEventName } from "../protocol";
|
|
@@ -42,7 +42,7 @@ beforeEach(() => {
|
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
describe("createRosterSync", () => {
|
|
45
|
-
|
|
45
|
+
it("emits a join for every player of the first sample", () => {
|
|
46
46
|
const sync = roster();
|
|
47
47
|
|
|
48
48
|
sync.sync(context, [
|
|
@@ -74,7 +74,7 @@ describe("createRosterSync", () => {
|
|
|
74
74
|
]);
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
it("stays quiet while the roster does not change", () => {
|
|
78
78
|
const sync = roster();
|
|
79
79
|
const rows = [
|
|
80
80
|
{
|
|
@@ -90,7 +90,7 @@ describe("createRosterSync", () => {
|
|
|
90
90
|
expect(emitted).toEqual([]);
|
|
91
91
|
});
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
it("emits a leave carrying the presence the player had", () => {
|
|
94
94
|
const sync = roster();
|
|
95
95
|
|
|
96
96
|
sync.sync(context, [
|
|
@@ -113,7 +113,7 @@ describe("createRosterSync", () => {
|
|
|
113
113
|
]);
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
it("emits both sides of a swap in one sample", () => {
|
|
117
117
|
const sync = roster();
|
|
118
118
|
|
|
119
119
|
sync.sync(context, [
|
|
@@ -136,7 +136,7 @@ describe("createRosterSync", () => {
|
|
|
136
136
|
]);
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
it("reports who it believes is online", () => {
|
|
140
140
|
const sync = roster();
|
|
141
141
|
|
|
142
142
|
sync.sync(context, [
|
|
@@ -158,7 +158,7 @@ describe("createRosterSync", () => {
|
|
|
158
158
|
expect(sync.online()).toEqual([]);
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
it("emits a fresh join after a clear", () => {
|
|
162
162
|
const sync = roster();
|
|
163
163
|
const rows = [
|
|
164
164
|
{
|
package/src/probes/a2s.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect,
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { BridgeProbeCode, BridgeProbeError } from "../core";
|
|
3
3
|
import { probeA2s } from "./a2s";
|
|
4
4
|
|
|
@@ -21,13 +21,13 @@ const listen = async (reply: Uint8Array | null) => {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
describe("probeA2s", () => {
|
|
24
|
-
|
|
24
|
+
it("rejects with an unreachable probe error when nothing listens on the port", async () => {
|
|
25
25
|
const probe = probeA2s(HOST, CLOSED_PORT, 2000);
|
|
26
26
|
|
|
27
27
|
await expect(probe).rejects.toBeInstanceOf(BridgeProbeError);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
it("survives the port-unreachable the game leaves behind before it binds", async () => {
|
|
31
31
|
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
32
32
|
await probeA2s(HOST, CLOSED_PORT, 300).catch(() => null);
|
|
33
33
|
}
|
|
@@ -35,7 +35,7 @@ describe("probeA2s", () => {
|
|
|
35
35
|
expect(process.exitCode ?? 0).toBe(0);
|
|
36
36
|
});
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
it("times out against a port that never answers", async () => {
|
|
39
39
|
const silent = await listen(null);
|
|
40
40
|
|
|
41
41
|
try {
|
|
@@ -48,7 +48,7 @@ describe("probeA2s", () => {
|
|
|
48
48
|
}
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
it("rejects a reply that is not an a2s frame", async () => {
|
|
52
52
|
const noisy = await listen(
|
|
53
53
|
new Uint8Array([
|
|
54
54
|
1,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect,
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { BridgeProbeCode, BridgeProbeError } from "../core";
|
|
3
3
|
import { parseStatus, probeMinecraftPing } from "./minecraftPing";
|
|
4
4
|
|
|
@@ -105,7 +105,7 @@ const listen = async (reply: Uint8Array | null, onServerName?: (serverName: stri
|
|
|
105
105
|
};
|
|
106
106
|
|
|
107
107
|
describe("reading a status response off the wire", () => {
|
|
108
|
-
|
|
108
|
+
it("a complete response gives the player counts, the version and the motd", () => {
|
|
109
109
|
expect(parseStatus(response(status))).toEqual({
|
|
110
110
|
players: {
|
|
111
111
|
online: 3,
|
|
@@ -116,7 +116,7 @@ describe("reading a status response off the wire", () => {
|
|
|
116
116
|
});
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
-
|
|
119
|
+
it("a response longer than one packet is still read, because the frame carries its own length", () => {
|
|
120
120
|
const packet = response(status);
|
|
121
121
|
const padded = new Uint8Array(packet.length + 4);
|
|
122
122
|
|
|
@@ -128,7 +128,7 @@ describe("reading a status response off the wire", () => {
|
|
|
128
128
|
});
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
it("a modern description object is read through its text", () => {
|
|
132
132
|
expect(
|
|
133
133
|
parseStatus(
|
|
134
134
|
response({
|
|
@@ -141,7 +141,7 @@ describe("reading a status response off the wire", () => {
|
|
|
141
141
|
).toBe("hello");
|
|
142
142
|
});
|
|
143
143
|
|
|
144
|
-
|
|
144
|
+
it("a description we cannot read is dropped rather than failing the sample", () => {
|
|
145
145
|
expect(
|
|
146
146
|
parseStatus(
|
|
147
147
|
response({
|
|
@@ -162,7 +162,7 @@ describe("reading a status response off the wire", () => {
|
|
|
162
162
|
).toBeNull();
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
it("a missing version name is dropped rather than failing the sample", () => {
|
|
166
166
|
expect(
|
|
167
167
|
parseStatus(
|
|
168
168
|
response({
|
|
@@ -172,7 +172,7 @@ describe("reading a status response off the wire", () => {
|
|
|
172
172
|
).toBeNull();
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
it("fractional or negative counts are clamped to whole players", () => {
|
|
176
176
|
expect(
|
|
177
177
|
parseStatus(
|
|
178
178
|
response({
|
|
@@ -188,7 +188,7 @@ describe("reading a status response off the wire", () => {
|
|
|
188
188
|
});
|
|
189
189
|
});
|
|
190
190
|
|
|
191
|
-
|
|
191
|
+
it("a response we have not fully received yet asks for more bytes", () => {
|
|
192
192
|
const packet = response(status);
|
|
193
193
|
|
|
194
194
|
expect(parseStatus(new Uint8Array(0))).toBeNull();
|
|
@@ -196,7 +196,7 @@ describe("reading a status response off the wire", () => {
|
|
|
196
196
|
expect(parseStatus(packet.subarray(0, packet.length - 4))).toBeNull();
|
|
197
197
|
});
|
|
198
198
|
|
|
199
|
-
|
|
199
|
+
it("a large payload whose length needs two varint bytes is read whole", () => {
|
|
200
200
|
const long = {
|
|
201
201
|
players: status.players,
|
|
202
202
|
description: "n".repeat(400),
|
|
@@ -205,11 +205,11 @@ describe("reading a status response off the wire", () => {
|
|
|
205
205
|
expect(parseStatus(response(long))?.description).toBe(long.description);
|
|
206
206
|
});
|
|
207
207
|
|
|
208
|
-
|
|
208
|
+
it("a packet that is not a status response is refused", () => {
|
|
209
209
|
expect(() => parseStatus(response(status, 0x01))).toThrow();
|
|
210
210
|
});
|
|
211
211
|
|
|
212
|
-
|
|
212
|
+
it("a response with no player counts is refused, because a sample without them is a lie", () => {
|
|
213
213
|
expect(() =>
|
|
214
214
|
parseStatus(
|
|
215
215
|
response({
|
|
@@ -223,7 +223,7 @@ describe("reading a status response off the wire", () => {
|
|
|
223
223
|
});
|
|
224
224
|
|
|
225
225
|
describe("probeMinecraftPing", () => {
|
|
226
|
-
|
|
226
|
+
it("answers with the status a listening server sends back", async () => {
|
|
227
227
|
const server = await listen(response(status));
|
|
228
228
|
|
|
229
229
|
try {
|
|
@@ -246,7 +246,7 @@ describe("probeMinecraftPing", () => {
|
|
|
246
246
|
}
|
|
247
247
|
});
|
|
248
248
|
|
|
249
|
-
|
|
249
|
+
it("puts the server name in the handshake so a virtual host still answers", async () => {
|
|
250
250
|
const seen: string[] = [];
|
|
251
251
|
|
|
252
252
|
const server = await listen(response(status), (serverName) => {
|
|
@@ -269,7 +269,7 @@ describe("probeMinecraftPing", () => {
|
|
|
269
269
|
}
|
|
270
270
|
});
|
|
271
271
|
|
|
272
|
-
|
|
272
|
+
it("falls back to the address when no server name is given", async () => {
|
|
273
273
|
const seen: string[] = [];
|
|
274
274
|
|
|
275
275
|
const server = await listen(response(status), (serverName) => {
|
|
@@ -291,7 +291,7 @@ describe("probeMinecraftPing", () => {
|
|
|
291
291
|
}
|
|
292
292
|
});
|
|
293
293
|
|
|
294
|
-
|
|
294
|
+
it("rejects as unreachable when nothing listens on the port", async () => {
|
|
295
295
|
const error = await probeMinecraftPing(
|
|
296
296
|
{
|
|
297
297
|
address: HOST,
|
|
@@ -304,7 +304,7 @@ describe("probeMinecraftPing", () => {
|
|
|
304
304
|
expect((error as BridgeProbeError).code).toBe(BridgeProbeCode.Unreachable);
|
|
305
305
|
});
|
|
306
306
|
|
|
307
|
-
|
|
307
|
+
it("times out against a port that never answers", async () => {
|
|
308
308
|
const silent = await listen(null);
|
|
309
309
|
|
|
310
310
|
try {
|
|
@@ -323,7 +323,7 @@ describe("probeMinecraftPing", () => {
|
|
|
323
323
|
}
|
|
324
324
|
});
|
|
325
325
|
|
|
326
|
-
|
|
326
|
+
it("rejects a reply that is not a status response", async () => {
|
|
327
327
|
const noisy = await listen(response(status, 0x01));
|
|
328
328
|
|
|
329
329
|
try {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect,
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { BridgeProbeCode, BridgeProbeError } from "../core";
|
|
3
3
|
import { parseUnconnectedPong, probeRaknetPing } from "./raknetPing";
|
|
4
4
|
|
|
@@ -64,7 +64,7 @@ const listen = async (reply: Uint8Array | null) => {
|
|
|
64
64
|
};
|
|
65
65
|
|
|
66
66
|
describe("reading an unconnected pong", () => {
|
|
67
|
-
|
|
67
|
+
it("a full advertisement gives the players, the version and the motd", () => {
|
|
68
68
|
expect(parseUnconnectedPong(pong(ADVERT))).toEqual({
|
|
69
69
|
players: {
|
|
70
70
|
online: 4,
|
|
@@ -79,7 +79,7 @@ describe("reading an unconnected pong", () => {
|
|
|
79
79
|
});
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
it("an advertisement that stops after the player counts is still read", () => {
|
|
83
83
|
expect(parseUnconnectedPong(pong("MCPE;Serverk;800;1.21.60;2;8"))).toEqual({
|
|
84
84
|
players: {
|
|
85
85
|
online: 2,
|
|
@@ -94,7 +94,7 @@ describe("reading an unconnected pong", () => {
|
|
|
94
94
|
});
|
|
95
95
|
});
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
it("a pong with another packet id is refused", () => {
|
|
98
98
|
expect(() =>
|
|
99
99
|
parseUnconnectedPong(
|
|
100
100
|
pong(ADVERT, {
|
|
@@ -104,7 +104,7 @@ describe("reading an unconnected pong", () => {
|
|
|
104
104
|
).toThrow();
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
it("a pong without the raknet magic is refused", () => {
|
|
108
108
|
expect(() =>
|
|
109
109
|
parseUnconnectedPong(
|
|
110
110
|
pong(ADVERT, {
|
|
@@ -114,7 +114,7 @@ describe("reading an unconnected pong", () => {
|
|
|
114
114
|
).toThrow();
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
it("a pong that advertises more payload than it carries is refused", () => {
|
|
118
118
|
expect(() =>
|
|
119
119
|
parseUnconnectedPong(
|
|
120
120
|
pong(ADVERT, {
|
|
@@ -124,21 +124,21 @@ describe("reading an unconnected pong", () => {
|
|
|
124
124
|
).toThrow();
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
it("a pong without player counts is refused, because a sample without them is a lie", () => {
|
|
128
128
|
expect(() => parseUnconnectedPong(pong("MCPE;Serverk;800;1.21.60"))).toThrow();
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
-
|
|
131
|
+
it("a datagram larger than a pong can be is refused", () => {
|
|
132
132
|
expect(() => parseUnconnectedPong(pong("n".repeat(4096)))).toThrow();
|
|
133
133
|
});
|
|
134
134
|
|
|
135
|
-
|
|
135
|
+
it("a truncated datagram is refused", () => {
|
|
136
136
|
expect(() => parseUnconnectedPong(new Uint8Array(8))).toThrow();
|
|
137
137
|
});
|
|
138
138
|
});
|
|
139
139
|
|
|
140
140
|
describe("probeRaknetPing", () => {
|
|
141
|
-
|
|
141
|
+
it("answers with the advertisement a listening server sends back", async () => {
|
|
142
142
|
const server = await listen(pong(ADVERT));
|
|
143
143
|
|
|
144
144
|
try {
|
|
@@ -155,13 +155,13 @@ describe("probeRaknetPing", () => {
|
|
|
155
155
|
}
|
|
156
156
|
});
|
|
157
157
|
|
|
158
|
-
|
|
158
|
+
it("rejects with an unreachable probe error when nothing listens on the port", async () => {
|
|
159
159
|
const probe = probeRaknetPing(HOST, CLOSED_PORT, 2000);
|
|
160
160
|
|
|
161
161
|
await expect(probe).rejects.toBeInstanceOf(BridgeProbeError);
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
it("times out against a port that never answers", async () => {
|
|
165
165
|
const silent = await listen(null);
|
|
166
166
|
|
|
167
167
|
try {
|
|
@@ -174,7 +174,7 @@ describe("probeRaknetPing", () => {
|
|
|
174
174
|
}
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
it("rejects a reply that is not a raknet pong", async () => {
|
|
178
178
|
const noisy = await listen(
|
|
179
179
|
new Uint8Array([
|
|
180
180
|
1,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, expect,
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import type { BridgeContext } from "../context";
|
|
3
3
|
import { BridgeDetailFormat, BridgeDetailTone, BridgeFormTarget, BridgeLayout } from "../panel";
|
|
4
4
|
import {
|
|
@@ -55,7 +55,7 @@ const accessWith = (password: RconAccessPassword | null, rotate?: () => Promise<
|
|
|
55
55
|
};
|
|
56
56
|
|
|
57
57
|
describe("rconExposed", () => {
|
|
58
|
-
|
|
58
|
+
it("only the string true opens remote access", () => {
|
|
59
59
|
expect(
|
|
60
60
|
rconExposed(
|
|
61
61
|
stubContext({
|
|
@@ -68,7 +68,7 @@ describe("rconExposed", () => {
|
|
|
68
68
|
});
|
|
69
69
|
|
|
70
70
|
describe("rconBindHost", () => {
|
|
71
|
-
|
|
71
|
+
it("an exposed server binds every interface and a closed one stays on loopback", () => {
|
|
72
72
|
expect(
|
|
73
73
|
rconBindHost(
|
|
74
74
|
stubContext({
|
|
@@ -81,7 +81,7 @@ describe("rconBindHost", () => {
|
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
describe("createRconAccess", () => {
|
|
84
|
-
|
|
84
|
+
it("an exposed server reads as open", async () => {
|
|
85
85
|
const detail = await accessWith({
|
|
86
86
|
value: "secret",
|
|
87
87
|
pending: false,
|
|
@@ -94,7 +94,7 @@ describe("createRconAccess", () => {
|
|
|
94
94
|
|
|
95
95
|
expect(detail?.id).toBe(RCON_ACCESS_MODULE);
|
|
96
96
|
expect(detail?.badges.map((badge) => badge.label.en)).toEqual([
|
|
97
|
-
"
|
|
97
|
+
"On",
|
|
98
98
|
]);
|
|
99
99
|
expect(detail?.badges[0]?.tone).toBe(BridgeDetailTone.Success);
|
|
100
100
|
expect(detail?.actions).toEqual([
|
|
@@ -102,19 +102,19 @@ describe("createRconAccess", () => {
|
|
|
102
102
|
]);
|
|
103
103
|
});
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
it("a closed server reads as closed", async () => {
|
|
106
106
|
const detail = await accessWith({
|
|
107
107
|
value: "secret",
|
|
108
108
|
pending: false,
|
|
109
109
|
}).read(stubContext().context);
|
|
110
110
|
|
|
111
111
|
expect(detail?.badges.map((badge) => badge.label.en)).toEqual([
|
|
112
|
-
"
|
|
112
|
+
"Off",
|
|
113
113
|
]);
|
|
114
114
|
expect(detail?.badges[0]?.tone).toBe(BridgeDetailTone.Neutral);
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
it("the port and the password are the two stats", async () => {
|
|
118
118
|
const detail = await accessWith({
|
|
119
119
|
value: "secret",
|
|
120
120
|
pending: false,
|
|
@@ -146,19 +146,19 @@ describe("createRconAccess", () => {
|
|
|
146
146
|
]);
|
|
147
147
|
});
|
|
148
148
|
|
|
149
|
-
|
|
149
|
+
it("a password waiting for the next restart carries the pending badge", async () => {
|
|
150
150
|
const detail = await accessWith({
|
|
151
151
|
value: "next",
|
|
152
152
|
pending: true,
|
|
153
153
|
}).read(stubContext().context);
|
|
154
154
|
|
|
155
155
|
expect(detail?.badges.map((badge) => badge.label.en)).toEqual([
|
|
156
|
-
"
|
|
156
|
+
"Off",
|
|
157
157
|
"Applies after restart",
|
|
158
158
|
]);
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
it("an inactive port leaves the port stat empty instead of failing the read", async () => {
|
|
162
162
|
const detail = await accessWith({
|
|
163
163
|
value: "secret",
|
|
164
164
|
pending: false,
|
|
@@ -172,11 +172,11 @@ describe("createRconAccess", () => {
|
|
|
172
172
|
expect(detail?.stats.find((stat) => stat.key === "password")?.value).toBe("secret");
|
|
173
173
|
});
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
it("a server with no password yet has no detail", async () => {
|
|
176
176
|
expect(await accessWith(null).read(stubContext().context)).toBeNull();
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
-
|
|
179
|
+
it("rotating delegates to the driver, logs without the value and answers nothing", async () => {
|
|
180
180
|
let rotated = 0;
|
|
181
181
|
|
|
182
182
|
const access = accessWith(
|
|
@@ -199,7 +199,7 @@ describe("createRconAccess", () => {
|
|
|
199
199
|
]);
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
-
|
|
202
|
+
it("the protocol name names the detail and both stats", async () => {
|
|
203
203
|
const detail = await createRconAccess({
|
|
204
204
|
protocol: {
|
|
205
205
|
ar: "Telnet",
|
|
@@ -232,7 +232,7 @@ describe("createRconAccess", () => {
|
|
|
232
232
|
]);
|
|
233
233
|
});
|
|
234
234
|
|
|
235
|
-
|
|
235
|
+
it("a tools line joins the security copy", async () => {
|
|
236
236
|
const detail = await createRconAccess({
|
|
237
237
|
tools: {
|
|
238
238
|
ar: "أدوات مثل RustAdmin.",
|
|
@@ -256,7 +256,7 @@ describe("createRconAccess", () => {
|
|
|
256
256
|
});
|
|
257
257
|
|
|
258
258
|
describe("rconAccessSections", () => {
|
|
259
|
-
|
|
259
|
+
it("the form declares the variable the manifest activates the port on", () => {
|
|
260
260
|
const [form] = rconAccessSections();
|
|
261
261
|
|
|
262
262
|
expect(form?.layout).toBe(BridgeLayout.Form);
|
|
@@ -272,7 +272,7 @@ describe("rconAccessSections", () => {
|
|
|
272
272
|
]);
|
|
273
273
|
});
|
|
274
274
|
|
|
275
|
-
|
|
275
|
+
it("the detail points at the module and offers the rotate action", () => {
|
|
276
276
|
const [, detail] = rconAccessSections();
|
|
277
277
|
|
|
278
278
|
if (detail?.layout !== BridgeLayout.Detail) {
|
|
@@ -286,7 +286,7 @@ describe("rconAccessSections", () => {
|
|
|
286
286
|
expect(detail.empty).toBeDefined();
|
|
287
287
|
});
|
|
288
288
|
|
|
289
|
-
|
|
289
|
+
it("the two sections carry different titles so the panel never repeats a heading", () => {
|
|
290
290
|
const [form, detail] = rconAccessSections();
|
|
291
291
|
|
|
292
292
|
expect(form?.title).toEqual({
|
|
@@ -300,7 +300,7 @@ describe("rconAccessSections", () => {
|
|
|
300
300
|
expect(detail?.title).not.toEqual(form?.title);
|
|
301
301
|
});
|
|
302
302
|
|
|
303
|
-
|
|
303
|
+
it("the detail is visible only while remote access is on", () => {
|
|
304
304
|
const [, detail] = rconAccessSections();
|
|
305
305
|
|
|
306
306
|
if (detail?.layout !== BridgeLayout.Detail) {
|
|
@@ -315,7 +315,7 @@ describe("rconAccessSections", () => {
|
|
|
315
315
|
});
|
|
316
316
|
});
|
|
317
317
|
|
|
318
|
-
|
|
318
|
+
it("a dedicated port opens a port and a shared one only turns access on", () => {
|
|
319
319
|
const [dedicated] = rconAccessSections({
|
|
320
320
|
portKey: RCON_ACCESS_PORT,
|
|
321
321
|
});
|