@serverkgg/bridge 0.0.11 → 0.0.13
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 +11 -1
- package/src/blog/articleFrontmatter.test.ts +95 -0
- package/src/blog/articleFrontmatter.ts +99 -0
- package/src/blog/compile.test.ts +148 -0
- package/src/blog/compile.ts +172 -0
- package/src/blog/compiled.ts +22 -0
- package/src/blog/index.ts +3 -0
- package/src/catalogs/curseforge.test.ts +580 -0
- package/src/catalogs/curseforge.ts +461 -0
- package/src/catalogs/index.ts +2 -0
- package/src/catalogs/thunderstore.test.ts +94 -0
- package/src/catalogs/thunderstore.ts +239 -0
- package/src/cli/validate.ts +7 -0
- package/src/codecs/index.ts +3 -0
- package/src/codecs/ini.test.ts +152 -0
- package/src/codecs/ini.ts +158 -0
- package/src/codecs/sourceCfg.test.ts +100 -0
- package/src/codecs/sourceCfg.ts +111 -0
- package/src/codecs/xmlProperties.test.ts +104 -0
- package/src/codecs/xmlProperties.ts +133 -0
- package/src/context/bridgeContext.ts +40 -0
- package/src/core/bridgeRconError.ts +19 -0
- package/src/core/index.ts +1 -0
- package/src/guides/compile.ts +70 -54
- package/src/install/index.ts +1 -0
- package/src/install/installStamp.test.ts +125 -0
- package/src/install/installStamp.ts +40 -0
- package/src/main.ts +1 -0
- package/src/manifest/index.ts +1 -0
- package/src/manifest/manifest.test.ts +460 -2
- package/src/manifest/manifest.ts +141 -13
- package/src/manifest/primitives.ts +6 -0
- package/src/manifest/runtime.ts +89 -0
- package/src/net/index.ts +2 -0
- package/src/net/loopback.test.ts +152 -0
- package/src/net/loopback.ts +109 -0
- package/src/net/loopbackMethod.ts +7 -0
- package/src/presence/index.ts +1 -0
- package/src/presence/rosterSync.test.ts +179 -0
- package/src/presence/rosterSync.ts +57 -0
- package/src/probes/index.ts +9 -0
- package/src/probes/minecraftPing.test.ts +344 -0
- package/src/probes/minecraftPing.ts +283 -0
- package/src/probes/raknetPing.test.ts +197 -0
- package/src/probes/raknetPing.ts +228 -0
- package/src/protocol/bridgeEvents.ts +4 -0
- package/src/rcon/index.ts +3 -0
- package/src/rcon/sourceRcon.test.ts +308 -0
- package/src/rcon/sourceRcon.ts +276 -0
- package/src/rcon/sourceRconFrame.test.ts +152 -0
- package/src/rcon/sourceRconFrame.ts +99 -0
- package/src/runtime/bridgeCodec.ts +45 -2
- package/src/runtime/bridgeContext.ts +13 -6
- package/src/runtime/bridgeDriver.test.ts +1 -0
- package/src/runtime/bridgeLiveConfig.test.ts +149 -0
- package/src/runtime/bridgeLiveConfig.ts +63 -0
- package/src/runtime/bridgeProbe.ts +13 -1
- package/src/runtime/bridgeRcon.ts +15 -0
- package/src/runtime/bridgeRegistry.ts +1 -0
- package/src/steam/index.ts +7 -0
- package/src/steam/steamApp.test.ts +78 -0
- package/src/steam/steamApp.ts +35 -0
- package/src/steam/steamcmd.test.ts +157 -0
- package/src/steam/steamcmd.ts +273 -0
- package/src/steam/steamcmdLogin.ts +34 -0
- package/src/steam/steamcmdNarrator.test.ts +90 -0
- package/src/steam/steamcmdNarrator.ts +43 -0
- package/src/steam/steamcmdPlatform.ts +4 -0
- package/src/steam/webApi.ts +70 -0
- package/src/steam/workshop.test.ts +45 -0
- package/src/steam/workshop.ts +148 -0
- package/src/utils/execDetail.ts +16 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/token.test.ts +50 -0
- package/src/utils/token.ts +35 -0
- package/src/wine/index.ts +3 -0
- package/src/wine/protonCommand.test.ts +67 -0
- package/src/wine/protonCommand.ts +49 -0
- package/src/wine/wineCommand.test.ts +92 -0
- package/src/wine/wineCommand.ts +54 -0
- package/src/wine/winePrefix.test.ts +111 -0
- package/src/wine/winePrefix.ts +88 -0
- package/src/runtime/bridgeSecrets.test.ts +0 -121
- package/src/runtime/bridgeSecrets.ts +0 -43
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { BridgeContext } from "../context";
|
|
3
|
+
import { INSTALL_STAMP_FILE, parseStamp, readStamp, writeStamp } from "./installStamp";
|
|
4
|
+
|
|
5
|
+
interface TestStamp {
|
|
6
|
+
buildId: string | null;
|
|
7
|
+
rconPassword?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const testContext = (seed: Record<string, string> = {}) => {
|
|
11
|
+
const stored = new Map(Object.entries(seed));
|
|
12
|
+
|
|
13
|
+
const context = {
|
|
14
|
+
files: {
|
|
15
|
+
async exists(path: string) {
|
|
16
|
+
return stored.has(path);
|
|
17
|
+
},
|
|
18
|
+
async read(path: string) {
|
|
19
|
+
return stored.get(path) ?? "";
|
|
20
|
+
},
|
|
21
|
+
async write(path: string, content: string) {
|
|
22
|
+
stored.set(path, content);
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
} as unknown as BridgeContext;
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
context,
|
|
29
|
+
stored,
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
describe("parseStamp", () => {
|
|
34
|
+
test("reads a json object", () => {
|
|
35
|
+
expect(
|
|
36
|
+
parseStamp<TestStamp>(
|
|
37
|
+
JSON.stringify({
|
|
38
|
+
buildId: "123",
|
|
39
|
+
}),
|
|
40
|
+
),
|
|
41
|
+
).toEqual({
|
|
42
|
+
buildId: "123",
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("returns null for anything that is not a json object", () => {
|
|
47
|
+
expect(parseStamp("")).toBeNull();
|
|
48
|
+
expect(parseStamp("{")).toBeNull();
|
|
49
|
+
expect(parseStamp("null")).toBeNull();
|
|
50
|
+
expect(parseStamp("[1,2]")).toBeNull();
|
|
51
|
+
expect(parseStamp('"text"')).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("readStamp", () => {
|
|
56
|
+
test("returns null when the stamp file is absent", async () => {
|
|
57
|
+
const { context } = testContext();
|
|
58
|
+
|
|
59
|
+
expect(await readStamp(context)).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("reads the stamp the writer left behind", async () => {
|
|
63
|
+
const { context } = testContext();
|
|
64
|
+
|
|
65
|
+
await writeStamp<TestStamp>(context, {
|
|
66
|
+
buildId: "4711",
|
|
67
|
+
rconPassword: "secret",
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(await readStamp<TestStamp>(context)).toEqual({
|
|
71
|
+
buildId: "4711",
|
|
72
|
+
rconPassword: "secret",
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("writes pretty json ending in a newline", async () => {
|
|
77
|
+
const { context, stored } = testContext();
|
|
78
|
+
|
|
79
|
+
await writeStamp(context, {
|
|
80
|
+
buildId: "1",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(stored.get(INSTALL_STAMP_FILE)).toBe('{\n "buildId": "1"\n}\n');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("honours a custom stamp path", async () => {
|
|
87
|
+
const { context, stored } = testContext();
|
|
88
|
+
|
|
89
|
+
await writeStamp(
|
|
90
|
+
context,
|
|
91
|
+
{
|
|
92
|
+
ready: true,
|
|
93
|
+
},
|
|
94
|
+
".serverk-wine.json",
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(stored.has(INSTALL_STAMP_FILE)).toBe(false);
|
|
98
|
+
expect(await readStamp(context, ".serverk-wine.json")).toEqual({
|
|
99
|
+
ready: true,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("returns null when the read fails after the file was seen", async () => {
|
|
104
|
+
const context = {
|
|
105
|
+
files: {
|
|
106
|
+
async exists() {
|
|
107
|
+
return true;
|
|
108
|
+
},
|
|
109
|
+
async read() {
|
|
110
|
+
throw new Error("vanished");
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
} as unknown as BridgeContext;
|
|
114
|
+
|
|
115
|
+
expect(await readStamp(context)).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("returns null when the stamp file is corrupt", async () => {
|
|
119
|
+
const { context } = testContext({
|
|
120
|
+
[INSTALL_STAMP_FILE]: "not json",
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
expect(await readStamp(context)).toBeNull();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { BridgeContext } from "../context";
|
|
2
|
+
|
|
3
|
+
export const INSTALL_STAMP_FILE = ".serverk-install.json";
|
|
4
|
+
|
|
5
|
+
export const parseStamp = <Stamp extends object = Record<string, unknown>>(raw: string): Stamp | null => {
|
|
6
|
+
try {
|
|
7
|
+
const parsed: unknown = JSON.parse(raw);
|
|
8
|
+
|
|
9
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return parsed as Stamp;
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const readStamp = async <Stamp extends object = Record<string, unknown>>(
|
|
20
|
+
context: BridgeContext,
|
|
21
|
+
path: string = INSTALL_STAMP_FILE,
|
|
22
|
+
): Promise<Stamp | null> => {
|
|
23
|
+
if (!(await context.files.exists(path))) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
return parseStamp<Stamp>(await context.files.read(path));
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const writeStamp = async <Stamp extends object>(
|
|
35
|
+
context: BridgeContext,
|
|
36
|
+
stamp: Stamp,
|
|
37
|
+
path: string = INSTALL_STAMP_FILE,
|
|
38
|
+
): Promise<void> => {
|
|
39
|
+
await context.files.write(path, `${JSON.stringify(stamp, null, 2)}\n`);
|
|
40
|
+
};
|
package/src/main.ts
CHANGED
package/src/manifest/index.ts
CHANGED
|
@@ -1,15 +1,53 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import { PortJoin } from "../core";
|
|
3
|
+
import { PortJoin, PortProtocol } from "../core";
|
|
4
4
|
import { GameCategory } from "./category";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
guidePackageSchema,
|
|
7
|
+
MANIFEST_SCHEMA_VERSION,
|
|
8
|
+
type Manifest,
|
|
9
|
+
manifestSchema,
|
|
10
|
+
PORT_FOLLOW_MIN_OFFSET,
|
|
11
|
+
packageSchema,
|
|
12
|
+
} from "./manifest";
|
|
6
13
|
import { isServerkAvatarTemplate, PRESENCE_AVATAR_LENGTH_LIMIT } from "./presence";
|
|
14
|
+
import {
|
|
15
|
+
BOOT_BUDGET_MIN_MINUTES,
|
|
16
|
+
INSTALL_BUDGET_MAX_MINUTES,
|
|
17
|
+
MANIFEST_RUNTIME_DEFAULTS,
|
|
18
|
+
ManifestCapability,
|
|
19
|
+
ManifestPlatform,
|
|
20
|
+
manifestRuntimeOf,
|
|
21
|
+
PIDS_LIMIT_MIN,
|
|
22
|
+
SHM_MAX_MB,
|
|
23
|
+
WINE_MIN_SHM_MB,
|
|
24
|
+
} from "./runtime";
|
|
7
25
|
|
|
8
26
|
const text = (value: string) => ({
|
|
9
27
|
ar: value,
|
|
10
28
|
en: value,
|
|
11
29
|
});
|
|
12
30
|
|
|
31
|
+
const guide = (id: string, extra: Record<string, unknown> = {}) => ({
|
|
32
|
+
id,
|
|
33
|
+
title: text(id),
|
|
34
|
+
summary: text(id),
|
|
35
|
+
bodyPath: {
|
|
36
|
+
ar: `guides/${id}/ar.md`,
|
|
37
|
+
en: `guides/${id}/en.md`,
|
|
38
|
+
},
|
|
39
|
+
...extra,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const guidePackage = (guides: unknown[]) => ({
|
|
43
|
+
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
44
|
+
kind: "GuidePackage",
|
|
45
|
+
metadata: {
|
|
46
|
+
id: "platform",
|
|
47
|
+
},
|
|
48
|
+
guides,
|
|
49
|
+
});
|
|
50
|
+
|
|
13
51
|
const base = () => ({
|
|
14
52
|
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
15
53
|
kind: "GamePackage",
|
|
@@ -79,6 +117,42 @@ const withPort = (extra: Record<string, unknown>) => {
|
|
|
79
117
|
};
|
|
80
118
|
};
|
|
81
119
|
|
|
120
|
+
const withPorts = (ports: unknown[]) => {
|
|
121
|
+
const manifest = base();
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
...manifest,
|
|
125
|
+
container: {
|
|
126
|
+
...manifest.container,
|
|
127
|
+
ports,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const withRuntime = (runtime: Record<string, unknown>) => {
|
|
133
|
+
const manifest = base();
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
...manifest,
|
|
137
|
+
container: {
|
|
138
|
+
...manifest.container,
|
|
139
|
+
runtime,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const udpPort = (key: string, containerPort: number, hostDefault: number) => ({
|
|
145
|
+
key,
|
|
146
|
+
containerPort,
|
|
147
|
+
protocol: PortProtocol.Udp,
|
|
148
|
+
hostRange: {
|
|
149
|
+
default: hostDefault,
|
|
150
|
+
min: hostDefault,
|
|
151
|
+
max: hostDefault + 99,
|
|
152
|
+
},
|
|
153
|
+
mirror: true,
|
|
154
|
+
});
|
|
155
|
+
|
|
82
156
|
const withPortSrv = (srv: unknown) => {
|
|
83
157
|
return withPort({
|
|
84
158
|
srv,
|
|
@@ -586,6 +660,19 @@ describe("packageSchema", () => {
|
|
|
586
660
|
expect(result.success).toBe(false);
|
|
587
661
|
expect(result.error && z.prettifyError(result.error)).toContain('tag "start" is not declared in guideTags');
|
|
588
662
|
});
|
|
663
|
+
|
|
664
|
+
test("carries up to 64 guides", () => {
|
|
665
|
+
const many = (count: number) =>
|
|
666
|
+
Array.from(
|
|
667
|
+
{
|
|
668
|
+
length: count,
|
|
669
|
+
},
|
|
670
|
+
(_, index) => guide(`guide-${index}`),
|
|
671
|
+
);
|
|
672
|
+
|
|
673
|
+
expect(guidePackageSchema.safeParse(guidePackage(many(64))).success).toBe(true);
|
|
674
|
+
expect(guidePackageSchema.safeParse(guidePackage(many(65))).success).toBe(false);
|
|
675
|
+
});
|
|
589
676
|
});
|
|
590
677
|
|
|
591
678
|
describe("presence", () => {
|
|
@@ -718,3 +805,374 @@ describe("presence", () => {
|
|
|
718
805
|
expect(result.error && z.prettifyError(result.error)).toContain('"platform" is declared more than once');
|
|
719
806
|
});
|
|
720
807
|
});
|
|
808
|
+
|
|
809
|
+
describe("container runtime", () => {
|
|
810
|
+
test("materializes the defaults when the manifest declares no runtime", () => {
|
|
811
|
+
const manifest = manifestSchema.parse(base());
|
|
812
|
+
|
|
813
|
+
expect(manifest.container.runtime).toEqual({
|
|
814
|
+
platform: ManifestPlatform.Linux,
|
|
815
|
+
installBudgetMinutes: 15,
|
|
816
|
+
bootBudgetMinutes: 12,
|
|
817
|
+
shmMb: 64,
|
|
818
|
+
pidsLimit: 512,
|
|
819
|
+
capabilities: [],
|
|
820
|
+
});
|
|
821
|
+
expect(MANIFEST_RUNTIME_DEFAULTS).toEqual(manifest.container.runtime);
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
test("keeps every field a manifest declares", () => {
|
|
825
|
+
const manifest = manifestSchema.parse(
|
|
826
|
+
withRuntime({
|
|
827
|
+
platform: ManifestPlatform.Wine,
|
|
828
|
+
installBudgetMinutes: 60,
|
|
829
|
+
bootBudgetMinutes: 30,
|
|
830
|
+
shmMb: 512,
|
|
831
|
+
pidsLimit: 2048,
|
|
832
|
+
capabilities: [
|
|
833
|
+
ManifestCapability.SysNice,
|
|
834
|
+
ManifestCapability.IpcLock,
|
|
835
|
+
],
|
|
836
|
+
}),
|
|
837
|
+
);
|
|
838
|
+
|
|
839
|
+
expect(manifest.container.runtime).toEqual({
|
|
840
|
+
platform: ManifestPlatform.Wine,
|
|
841
|
+
installBudgetMinutes: 60,
|
|
842
|
+
bootBudgetMinutes: 30,
|
|
843
|
+
shmMb: 512,
|
|
844
|
+
pidsLimit: 2048,
|
|
845
|
+
capabilities: [
|
|
846
|
+
ManifestCapability.SysNice,
|
|
847
|
+
ManifestCapability.IpcLock,
|
|
848
|
+
],
|
|
849
|
+
});
|
|
850
|
+
});
|
|
851
|
+
|
|
852
|
+
test("clamps an install budget above the ceiling", () => {
|
|
853
|
+
expect(
|
|
854
|
+
failure(
|
|
855
|
+
withRuntime({
|
|
856
|
+
installBudgetMinutes: INSTALL_BUDGET_MAX_MINUTES + 1,
|
|
857
|
+
}),
|
|
858
|
+
),
|
|
859
|
+
).toEqual([
|
|
860
|
+
{
|
|
861
|
+
path: "container.runtime.installBudgetMinutes",
|
|
862
|
+
message: `Too big: expected number to be <=${INSTALL_BUDGET_MAX_MINUTES}`,
|
|
863
|
+
},
|
|
864
|
+
]);
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
test("clamps a boot budget below the floor", () => {
|
|
868
|
+
expect(
|
|
869
|
+
failure(
|
|
870
|
+
withRuntime({
|
|
871
|
+
bootBudgetMinutes: BOOT_BUDGET_MIN_MINUTES - 1,
|
|
872
|
+
}),
|
|
873
|
+
),
|
|
874
|
+
).toEqual([
|
|
875
|
+
{
|
|
876
|
+
path: "container.runtime.bootBudgetMinutes",
|
|
877
|
+
message: `Too small: expected number to be >=${BOOT_BUDGET_MIN_MINUTES}`,
|
|
878
|
+
},
|
|
879
|
+
]);
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
test("clamps shared memory and the pids limit to their range", () => {
|
|
883
|
+
expect(
|
|
884
|
+
failure(
|
|
885
|
+
withRuntime({
|
|
886
|
+
shmMb: SHM_MAX_MB + 1,
|
|
887
|
+
pidsLimit: PIDS_LIMIT_MIN - 1,
|
|
888
|
+
}),
|
|
889
|
+
),
|
|
890
|
+
).toEqual([
|
|
891
|
+
{
|
|
892
|
+
path: "container.runtime.shmMb",
|
|
893
|
+
message: `Too big: expected number to be <=${SHM_MAX_MB}`,
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
path: "container.runtime.pidsLimit",
|
|
897
|
+
message: `Too small: expected number to be >=${PIDS_LIMIT_MIN}`,
|
|
898
|
+
},
|
|
899
|
+
]);
|
|
900
|
+
});
|
|
901
|
+
|
|
902
|
+
test("refuses a fourth capability", () => {
|
|
903
|
+
expect(
|
|
904
|
+
failure(
|
|
905
|
+
withRuntime({
|
|
906
|
+
capabilities: [
|
|
907
|
+
ManifestCapability.SysNice,
|
|
908
|
+
ManifestCapability.IpcLock,
|
|
909
|
+
ManifestCapability.SysPtrace,
|
|
910
|
+
ManifestCapability.SysNice,
|
|
911
|
+
],
|
|
912
|
+
}),
|
|
913
|
+
),
|
|
914
|
+
).toEqual([
|
|
915
|
+
{
|
|
916
|
+
path: "container.runtime.capabilities",
|
|
917
|
+
message: "Too big: expected array to have <=3 items",
|
|
918
|
+
},
|
|
919
|
+
]);
|
|
920
|
+
});
|
|
921
|
+
|
|
922
|
+
test("refuses a capability outside the allow-list", () => {
|
|
923
|
+
expect(
|
|
924
|
+
failure(
|
|
925
|
+
withRuntime({
|
|
926
|
+
capabilities: [
|
|
927
|
+
"NET_ADMIN",
|
|
928
|
+
],
|
|
929
|
+
}),
|
|
930
|
+
),
|
|
931
|
+
).toEqual([
|
|
932
|
+
{
|
|
933
|
+
path: "container.runtime.capabilities.0",
|
|
934
|
+
message: 'Invalid option: expected one of "SYS_NICE"|"IPC_LOCK"|"SYS_PTRACE"',
|
|
935
|
+
},
|
|
936
|
+
]);
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
test("refuses a wine platform running on the default shared memory", () => {
|
|
940
|
+
expect(
|
|
941
|
+
failure(
|
|
942
|
+
withRuntime({
|
|
943
|
+
platform: ManifestPlatform.Wine,
|
|
944
|
+
}),
|
|
945
|
+
),
|
|
946
|
+
).toEqual([
|
|
947
|
+
{
|
|
948
|
+
path: "container.runtime.shmMb",
|
|
949
|
+
message: `must be at least ${WINE_MIN_SHM_MB} when platform is "wine"`,
|
|
950
|
+
},
|
|
951
|
+
]);
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
test("accepts a wine platform once shared memory clears the floor", () => {
|
|
955
|
+
const manifest = manifestSchema.parse(
|
|
956
|
+
withRuntime({
|
|
957
|
+
platform: ManifestPlatform.Wine,
|
|
958
|
+
shmMb: WINE_MIN_SHM_MB,
|
|
959
|
+
}),
|
|
960
|
+
);
|
|
961
|
+
|
|
962
|
+
expect(manifest.container.runtime.shmMb).toBe(WINE_MIN_SHM_MB);
|
|
963
|
+
});
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
describe("manifestRuntimeOf", () => {
|
|
967
|
+
test("reads the runtime a manifest declares", () => {
|
|
968
|
+
const manifest = manifestSchema.parse(
|
|
969
|
+
withRuntime({
|
|
970
|
+
platform: ManifestPlatform.Wine,
|
|
971
|
+
shmMb: 512,
|
|
972
|
+
}),
|
|
973
|
+
);
|
|
974
|
+
|
|
975
|
+
expect(manifestRuntimeOf(manifest)).toEqual({
|
|
976
|
+
...MANIFEST_RUNTIME_DEFAULTS,
|
|
977
|
+
platform: ManifestPlatform.Wine,
|
|
978
|
+
shmMb: 512,
|
|
979
|
+
});
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
test("falls back to the defaults for a bundle published before the field existed", () => {
|
|
983
|
+
const manifest = manifestSchema.parse(base());
|
|
984
|
+
const legacy = {
|
|
985
|
+
...manifest,
|
|
986
|
+
container: {
|
|
987
|
+
image: manifest.container.image,
|
|
988
|
+
ports: manifest.container.ports,
|
|
989
|
+
},
|
|
990
|
+
} as Manifest;
|
|
991
|
+
|
|
992
|
+
expect(manifestRuntimeOf(legacy)).toEqual(MANIFEST_RUNTIME_DEFAULTS);
|
|
993
|
+
});
|
|
994
|
+
});
|
|
995
|
+
|
|
996
|
+
describe("port mirroring", () => {
|
|
997
|
+
test("defaults to no mirror and no follower", () => {
|
|
998
|
+
const manifest = manifestSchema.parse(base());
|
|
999
|
+
|
|
1000
|
+
expect(manifest.container.ports[0]?.mirror).toBe(false);
|
|
1001
|
+
expect(manifest.container.ports[0]?.follows).toBeUndefined();
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
test("keeps the mirror flag and the follow rule a manifest declares", () => {
|
|
1005
|
+
const manifest = manifestSchema.parse(
|
|
1006
|
+
withPorts([
|
|
1007
|
+
udpPort("game", 27_015, 27_015),
|
|
1008
|
+
{
|
|
1009
|
+
...udpPort("query", 27_016, 27_115),
|
|
1010
|
+
follows: {
|
|
1011
|
+
key: "game",
|
|
1012
|
+
offset: 1,
|
|
1013
|
+
},
|
|
1014
|
+
},
|
|
1015
|
+
]),
|
|
1016
|
+
);
|
|
1017
|
+
|
|
1018
|
+
expect(manifest.container.ports[0]?.mirror).toBe(true);
|
|
1019
|
+
expect(manifest.container.ports[1]?.follows).toEqual({
|
|
1020
|
+
key: "game",
|
|
1021
|
+
offset: 1,
|
|
1022
|
+
});
|
|
1023
|
+
});
|
|
1024
|
+
|
|
1025
|
+
test("refuses a follow rule naming a port that is not declared", () => {
|
|
1026
|
+
expect(
|
|
1027
|
+
failure(
|
|
1028
|
+
withPorts([
|
|
1029
|
+
{
|
|
1030
|
+
...udpPort("game", 27_015, 27_015),
|
|
1031
|
+
follows: {
|
|
1032
|
+
key: "query",
|
|
1033
|
+
offset: 1,
|
|
1034
|
+
},
|
|
1035
|
+
},
|
|
1036
|
+
]),
|
|
1037
|
+
),
|
|
1038
|
+
).toEqual([
|
|
1039
|
+
{
|
|
1040
|
+
path: "container.ports.0.follows.key",
|
|
1041
|
+
message: '"query" is not a declared port',
|
|
1042
|
+
},
|
|
1043
|
+
]);
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
test("refuses a port that follows itself", () => {
|
|
1047
|
+
expect(
|
|
1048
|
+
failure(
|
|
1049
|
+
withPorts([
|
|
1050
|
+
{
|
|
1051
|
+
...udpPort("game", 27_015, 27_015),
|
|
1052
|
+
follows: {
|
|
1053
|
+
key: "game",
|
|
1054
|
+
offset: 1,
|
|
1055
|
+
},
|
|
1056
|
+
},
|
|
1057
|
+
]),
|
|
1058
|
+
),
|
|
1059
|
+
).toEqual([
|
|
1060
|
+
{
|
|
1061
|
+
path: "container.ports.0.follows.key",
|
|
1062
|
+
message: 'port "game" cannot follow itself',
|
|
1063
|
+
},
|
|
1064
|
+
]);
|
|
1065
|
+
});
|
|
1066
|
+
|
|
1067
|
+
test("refuses a follow rule crossing protocols", () => {
|
|
1068
|
+
const [tcp] = base().container.ports;
|
|
1069
|
+
|
|
1070
|
+
if (!tcp) {
|
|
1071
|
+
throw new Error("fixture is missing its port");
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
expect(
|
|
1075
|
+
failure(
|
|
1076
|
+
withPorts([
|
|
1077
|
+
tcp,
|
|
1078
|
+
{
|
|
1079
|
+
...udpPort("query", 27_016, 27_115),
|
|
1080
|
+
follows: {
|
|
1081
|
+
key: "game",
|
|
1082
|
+
offset: 1,
|
|
1083
|
+
},
|
|
1084
|
+
},
|
|
1085
|
+
]),
|
|
1086
|
+
),
|
|
1087
|
+
).toEqual([
|
|
1088
|
+
{
|
|
1089
|
+
path: "container.ports.1.follows.key",
|
|
1090
|
+
message: '"game" is a tcp port — a port only follows one of its own protocol',
|
|
1091
|
+
},
|
|
1092
|
+
]);
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
test("refuses a port that both follows and is followed", () => {
|
|
1096
|
+
expect(
|
|
1097
|
+
failure(
|
|
1098
|
+
withPorts([
|
|
1099
|
+
udpPort("game", 27_015, 27_015),
|
|
1100
|
+
{
|
|
1101
|
+
...udpPort("query", 27_016, 27_115),
|
|
1102
|
+
follows: {
|
|
1103
|
+
key: "game",
|
|
1104
|
+
offset: 1,
|
|
1105
|
+
},
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
...udpPort("raw", 27_017, 27_215),
|
|
1109
|
+
follows: {
|
|
1110
|
+
key: "query",
|
|
1111
|
+
offset: 1,
|
|
1112
|
+
},
|
|
1113
|
+
},
|
|
1114
|
+
]),
|
|
1115
|
+
),
|
|
1116
|
+
).toEqual([
|
|
1117
|
+
{
|
|
1118
|
+
path: "container.ports.1.follows.key",
|
|
1119
|
+
message: 'port "query" is followed by another port and cannot follow one itself',
|
|
1120
|
+
},
|
|
1121
|
+
]);
|
|
1122
|
+
});
|
|
1123
|
+
|
|
1124
|
+
test("lets a follower's host range overlap the port it follows", () => {
|
|
1125
|
+
const manifest = manifestSchema.parse(
|
|
1126
|
+
withPorts([
|
|
1127
|
+
udpPort("game", 27_015, 27_015),
|
|
1128
|
+
{
|
|
1129
|
+
...udpPort("query", 27_016, 27_016),
|
|
1130
|
+
follows: {
|
|
1131
|
+
key: "game",
|
|
1132
|
+
offset: 1,
|
|
1133
|
+
},
|
|
1134
|
+
},
|
|
1135
|
+
]),
|
|
1136
|
+
);
|
|
1137
|
+
|
|
1138
|
+
expect(manifest.container.ports[1]?.hostRange.min).toBe(27_016);
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
test("still refuses two independent ports whose host ranges overlap", () => {
|
|
1142
|
+
expect(
|
|
1143
|
+
failure(
|
|
1144
|
+
withPorts([
|
|
1145
|
+
udpPort("game", 27_015, 27_015),
|
|
1146
|
+
udpPort("query", 27_016, 27_016),
|
|
1147
|
+
]),
|
|
1148
|
+
),
|
|
1149
|
+
).toEqual([
|
|
1150
|
+
{
|
|
1151
|
+
path: "container.ports.1.hostRange",
|
|
1152
|
+
message: 'host range overlaps port "game" — two udp ports cannot share host ports',
|
|
1153
|
+
},
|
|
1154
|
+
]);
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
test("refuses an offset outside its range", () => {
|
|
1158
|
+
expect(
|
|
1159
|
+
failure(
|
|
1160
|
+
withPorts([
|
|
1161
|
+
udpPort("game", 27_015, 27_015),
|
|
1162
|
+
{
|
|
1163
|
+
...udpPort("query", 27_016, 27_115),
|
|
1164
|
+
follows: {
|
|
1165
|
+
key: "game",
|
|
1166
|
+
offset: 0,
|
|
1167
|
+
},
|
|
1168
|
+
},
|
|
1169
|
+
]),
|
|
1170
|
+
),
|
|
1171
|
+
).toEqual([
|
|
1172
|
+
{
|
|
1173
|
+
path: "container.ports.1.follows.offset",
|
|
1174
|
+
message: `Too small: expected number to be >=${PORT_FOLLOW_MIN_OFFSET}`,
|
|
1175
|
+
},
|
|
1176
|
+
]);
|
|
1177
|
+
});
|
|
1178
|
+
});
|