@underpostnet/cyberia 3.3.76 → 3.4.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/.env.example +1 -0
- package/.github/workflows/coverall.cyberia.ci.yml +17 -3
- package/.github/workflows/cyberia-client.cd.yml +2 -1
- package/.github/workflows/cyberia-server.cd.yml +2 -1
- package/.github/workflows/docker-image.cyberia-client.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-server.ci.yml +1 -1
- package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +1 -1
- package/.github/workflows/docker-image.engine-cyberia.ci.yml +1 -1
- package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +1 -1
- package/.github/workflows/engine-cyberia.ci.yml +7 -1
- package/.github/workflows/ghpkg.ci.yml +19 -2
- package/.github/workflows/gitlab.ci.yml +8 -6
- package/.github/workflows/hardhat.ci.yml +4 -1
- package/.github/workflows/npmpkg.ci.yml +9 -5
- package/.github/workflows/publish.ci.yml +21 -12
- package/.github/workflows/publish.cyberia.ci.yml +25 -14
- package/.github/workflows/pwa-microservices-template-page.cd.yml +4 -2
- package/CHANGELOG.md +110 -262
- package/CLI-HELP.md +29 -2
- package/Dockerfile +1 -1
- package/Dockerfile.dev +1 -1
- package/Dockerfile.test +1 -1
- package/README.md +1 -1
- package/bin/build.js +67 -14
- package/bin/cyberia.js +4 -4
- package/bin/index.js +4 -4
- package/compose.env +1 -0
- package/conf.js +1 -1043
- package/deploy/cyberia-client/deploy.sh +13 -11
- package/deploy/cyberia-server/deploy.sh +13 -11
- package/deploy/dd-cyberia/sync-deploy.sh +7 -28
- package/deploy/lib/config.sh +1 -1
- package/deploy/lib/github-actions-logging.sh +3 -2
- package/deploy/lib/host.sh +1 -1
- package/deploy/pwa-microservices-template/deploy.sh +7 -1
- package/deployment.yaml +1 -1
- package/docker-compose.yml +7 -7
- package/docs/coverage/cyberia/api/cyberia-instance/cyberia-fallback-capture.js.html +1591 -0
- package/docs/coverage/cyberia/api/cyberia-instance/index.html +116 -0
- package/docs/coverage/cyberia/api/cyberia-server-defaults/cyberia-server-defaults.js.html +4138 -0
- package/docs/coverage/cyberia/api/cyberia-server-defaults/index.html +116 -0
- package/docs/coverage/cyberia/api/object-layer/index.html +116 -0
- package/docs/coverage/cyberia/api/object-layer/object-layer.model.js.html +1393 -0
- package/docs/coverage/cyberia/base.css +362 -0
- package/docs/coverage/cyberia/block-navigation.js +82 -0
- package/docs/coverage/cyberia/favicon.png +0 -0
- package/docs/coverage/cyberia/index.html +161 -0
- package/docs/coverage/cyberia/prettify.css +101 -0
- package/docs/coverage/cyberia/prettify.js +937 -0
- package/docs/coverage/cyberia/projects/cyberia/atlas-sprite-sheet-generator.js.html +1315 -0
- package/docs/coverage/cyberia/projects/cyberia/index.html +176 -0
- package/docs/coverage/cyberia/projects/cyberia/instance-backup.js.html +715 -0
- package/docs/coverage/cyberia/projects/cyberia/instance-data.js.html +2182 -0
- package/docs/coverage/cyberia/projects/cyberia/shape-generator.js.html +3265 -0
- package/docs/coverage/cyberia/projects/cyberia/stat-balance.js.html +328 -0
- package/docs/coverage/cyberia/sort-arrow-sprite.png +0 -0
- package/docs/coverage/cyberia/sorter.js +205 -0
- package/hardhat/package-lock.json +6 -6
- package/hardhat/package.json +2 -2
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +1 -1
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +1 -1
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/package.json +19 -17
- package/scripts/test-monitor.sh +1 -1
- package/src/api/file/file.router.js +7 -1
- package/src/cli/baremetal.js +1 -1
- package/src/cli/cluster.js +73 -18
- package/src/cli/deploy.js +211 -30
- package/src/cli/docker-compose.js +1 -1
- package/src/cli/index.js +36 -0
- package/src/cli/kubectl.js +4 -3
- package/src/cli/release.js +5 -1
- package/src/cli/repository.js +14 -11
- package/src/cli/run.js +10 -3
- package/src/cli/secrets.js +393 -200
- package/src/cli/socketsecurity.js +260 -0
- package/src/client/components/core/Badge.js +0 -1
- package/src/client/components/core/CssCore.js +11 -1
- package/src/client/components/core/Modal.js +189 -66
- package/src/client/components/core/SearchBox.js +0 -1
- package/src/client/components/core/ToolTip.js +15 -0
- package/src/client/components/cryptokoyn/AppShellCryptokoyn.js +0 -2
- package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +0 -4
- package/src/client/components/default/AppShellDefault.js +1 -4
- package/src/client/components/itemledger/AppShellItemledger.js +0 -2
- package/src/client/components/underpost/AppShellUnderpost.js +12 -0
- package/src/client/components/underpost/CssUnderpost.js +15 -2
- package/src/client/public/cyberia-docs/CYBERIA-CLIENT.md +8 -0
- package/src/client/public/cyberia-docs/CYBERIA.md +1 -1
- package/src/client/public/cyberia-docs/OFF-CHAIN-ECONOMY.md +54 -56
- package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
- package/src/client/public/cyberia-docs/UNDERPOST-PLATFORM.md +30 -29
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
- package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +54 -25
- package/src/client/ssr/head/Css.js +12 -10
- package/src/client/ssr/views/Cyberia404.js +9 -13
- package/src/client/ssr/views/NotFound.js +75 -0
- package/src/client-builder/client-bundle.js +2 -2
- package/src/index.js +13 -1
- package/src/runtime/engine-cyberia/Dockerfile +1 -1
- package/src/runtime/engine-cyberia/Dockerfile.dev +1 -1
- package/src/runtime/engine-cyberia/Dockerfile.test +1 -1
- package/src/runtime/engine-cyberia/compose.env +1 -0
- package/src/runtime/engine-cyberia/docker-compose.yml +7 -7
- package/src/server/build/catalog.js +1 -0
- package/src/server/build/execution.js +19 -5
- package/src/server/build/package.js +166 -3
- package/src/server/build/testing.js +2 -1
- package/src/server/network/underpost-gateway.js +113 -14
- package/src/server/runtime/conf.js +57 -7
- package/src/server/runtime/config-scope.js +2 -0
- package/src/server/runtime/process.js +4 -3
- package/src/server/runtime/start.js +11 -2
- package/src/server/security/socketsecurity.js +1735 -0
- package/test/integration/infra/1-security/config-scope.test.js +3 -0
- package/test/integration/infra/1-security/socketsecurity.test.js +1041 -0
- package/test/integration/infra/1-security/sops-secret-store.test.js +201 -71
- package/test/integration/infra/4-ingress/underpost-ingress.test.js +1 -1
- package/test/unit/build-template.test.js +24 -0
- package/test/unit/catalog.test.js +13 -2
- package/test/unit/conf-resolution.test.js +41 -7
- package/test/unit/cyberia/instance-data.test.js +314 -0
- package/test/unit/deploy-legacy-gateway-sweep.test.js +63 -0
- package/test/unit/deploy-log-table.test.js +38 -0
- package/test/unit/execution-profiles.test.js +1 -0
- package/test/unit/gateway-pod-fetch.test.js +95 -0
- package/test/unit/letsencrypt-issuer.test.js +101 -0
- package/test/unit/package.test.js +118 -0
- package/test/unit/release-bump.test.js +3 -1
- package/test/unit/start-options.test.js +12 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
buildCyberiaMmoInstanceEnv,
|
|
4
|
+
getInstanceModels,
|
|
5
|
+
parseRgba,
|
|
6
|
+
toEntityMsg,
|
|
7
|
+
toMapMsg,
|
|
8
|
+
toInstanceMsg,
|
|
9
|
+
toObjectLayerMsg,
|
|
10
|
+
toActionMsg,
|
|
11
|
+
toQuestMsg,
|
|
12
|
+
toInstanceConfig,
|
|
13
|
+
buildFallbackConfig,
|
|
14
|
+
pingData,
|
|
15
|
+
objectLayerQueryFilter,
|
|
16
|
+
fetchObjectLayerBatch,
|
|
17
|
+
fetchObjectLayer,
|
|
18
|
+
fetchMapData,
|
|
19
|
+
fetchObjectLayerManifest,
|
|
20
|
+
fetchFullInstance,
|
|
21
|
+
skillDocsToConfig,
|
|
22
|
+
entityTypeDefaultDocsToConfig,
|
|
23
|
+
mergeEntityDefaults,
|
|
24
|
+
itemTypesOf,
|
|
25
|
+
} from '../../../src/projects/cyberia/instance-data.js';
|
|
26
|
+
import {
|
|
27
|
+
CYBERIA_INSTANCE_CONF_DEFAULTS,
|
|
28
|
+
DEFAULT_DEAD_ITEM_ID,
|
|
29
|
+
ENTITY_TYPE_DEFAULTS,
|
|
30
|
+
} from '../../../src/api/cyberia-server-defaults/cyberia-server-defaults.js';
|
|
31
|
+
import { DEFAULT_INSTANCE_CODE } from '../../../src/client/components/cyberia/SharedDefaultsCyberia.js';
|
|
32
|
+
|
|
33
|
+
const lean = (value) => ({ lean: async () => value, populate: () => lean(value) });
|
|
34
|
+
|
|
35
|
+
describe('instance env', () => {
|
|
36
|
+
it('derives the server and client variables from the variant runtime', () => {
|
|
37
|
+
const server = buildCyberiaMmoInstanceEnv({
|
|
38
|
+
instance: { runtime: 'cyberia-server', path: '/forest', instanceCode: 'FOREST' },
|
|
39
|
+
env: { PORT: '4001' },
|
|
40
|
+
});
|
|
41
|
+
expect(server).toEqual({ PORT: '4001', INSTANCE_CODE: 'FOREST', CYBERIA_BASE_PATH: '/forest' });
|
|
42
|
+
|
|
43
|
+
const client = buildCyberiaMmoInstanceEnv({ instance: { runtime: 'cyberia-client', path: '/' } });
|
|
44
|
+
expect(client).toEqual({
|
|
45
|
+
CYBERIA_INSTANCE_CODE: DEFAULT_INSTANCE_CODE,
|
|
46
|
+
CYBERIA_DEFAULT_INSTANCE: DEFAULT_INSTANCE_CODE,
|
|
47
|
+
CYBERIA_BASE_PATH: '/',
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('leaves a runtime it does not know alone', () => {
|
|
52
|
+
expect(buildCyberiaMmoInstanceEnv({ instance: { runtime: 'nodejs', path: '/x' }, env: { A: '1' } })).toEqual({
|
|
53
|
+
A: '1',
|
|
54
|
+
});
|
|
55
|
+
expect(buildCyberiaMmoInstanceEnv({})).toEqual({});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('names the context when no database provider was loaded for it', () => {
|
|
59
|
+
expect(() => getInstanceModels({ host: 'nowhere.test', path: '/' })).toThrow('nowhere.test');
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('wire converters', () => {
|
|
64
|
+
it('parses css colours into wire components and treats anything else as transparent', () => {
|
|
65
|
+
expect(parseRgba('rgba(10, 20, 30, 0.5)')).toEqual({ r: 10, g: 20, b: 30, a: 128 });
|
|
66
|
+
expect(parseRgba('rgb(1,2,3)')).toEqual({ r: 1, g: 2, b: 3, a: 255 });
|
|
67
|
+
expect(parseRgba('')).toEqual({ r: 0, g: 0, b: 0, a: 0 });
|
|
68
|
+
expect(parseRgba('#ff0000')).toEqual({ r: 0, g: 0, b: 0, a: 0 });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('fills every entity, map and instance field an empty document leaves out', () => {
|
|
72
|
+
const entity = toEntityMsg({});
|
|
73
|
+
expect(entity).toMatchObject({ entityType: 'floor', level: 0, dimX: 1, dimY: 1, objectLayerItemIds: [] });
|
|
74
|
+
expect(toEntityMsg({ level: 3, color: 'rgb(9,8,7)' })).toMatchObject({ level: 3, colorR: 9, colorB: 7 });
|
|
75
|
+
|
|
76
|
+
const map = toMapMsg({ _id: 'm1', entities: [{ entityType: 'bot' }] });
|
|
77
|
+
expect(map).toMatchObject({ mongoId: 'm1', gridX: 16, cellWidth: 32 });
|
|
78
|
+
expect(map.entities[0].entityType).toBe('bot');
|
|
79
|
+
|
|
80
|
+
const instance = toInstanceMsg({ _id: 'i1', portals: [{ sourceMapCode: 'a' }] });
|
|
81
|
+
expect(instance).toMatchObject({ mongoId: 'i1', topologyMode: 'hybrid', mapCodes: [] });
|
|
82
|
+
expect(instance.portals[0]).toMatchObject({ sourceMapCode: 'a', portalMode: 'inter-portal', targetCellX: 0 });
|
|
83
|
+
expect(instance.playerSpawn).toEqual({ sourceMapCode: '', sourceCellX: 0, sourceCellY: 0, random: false });
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('carries an object layer with its ledger and render, defaulting the ledger to off-chain', () => {
|
|
87
|
+
const msg = toObjectLayerMsg({ _id: 'o1', data: { item: { id: 'hatchet', type: 'weapon', activable: 1 } } });
|
|
88
|
+
expect(msg).toMatchObject({
|
|
89
|
+
mongoId: 'o1',
|
|
90
|
+
item: { id: 'hatchet', type: 'weapon', description: '', activable: true },
|
|
91
|
+
ledger: { type: 'OFF_CHAIN', address: '', tokenId: '' },
|
|
92
|
+
render: { cid: '', metadataCid: '' },
|
|
93
|
+
sha256: '',
|
|
94
|
+
cid: '',
|
|
95
|
+
});
|
|
96
|
+
expect(toObjectLayerMsg({ _id: 'o2' }).item.id).toBe('');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('carries vendor, craft and quest content with their defaults', () => {
|
|
100
|
+
const action = toActionMsg({
|
|
101
|
+
code: 'shop',
|
|
102
|
+
shopItems: [{ itemId: 'hatchet' }],
|
|
103
|
+
craftRecipes: [{ outputItems: [{ itemId: 'plank' }], ingredients: [{ itemId: 'wood-1', qty: 2 }] }],
|
|
104
|
+
questDialogueCodes: [{ questCode: 'q1' }],
|
|
105
|
+
});
|
|
106
|
+
expect(action.shopItems[0]).toEqual({ itemId: 'hatchet', priceItemId: 'coin', priceQty: 1 });
|
|
107
|
+
expect(action.craftRecipes[0]).toEqual({
|
|
108
|
+
outputItems: [{ itemId: 'plank', qty: 1 }],
|
|
109
|
+
ingredients: [{ itemId: 'wood-1', qty: 2 }],
|
|
110
|
+
craftTimeMs: 0,
|
|
111
|
+
});
|
|
112
|
+
expect(action.questDialogueCodes[0]).toEqual({ questCode: 'q1', dialogCode: '' });
|
|
113
|
+
expect(toActionMsg({})).toMatchObject({ code: '', storageSlots: 0, shopItems: [] });
|
|
114
|
+
|
|
115
|
+
const quest = toQuestMsg({
|
|
116
|
+
code: 'q1',
|
|
117
|
+
steps: [{ id: 's1', objectives: [{ type: 'collect', itemId: 'wood-1' }] }],
|
|
118
|
+
rewards: [{ itemId: 'coin' }],
|
|
119
|
+
});
|
|
120
|
+
expect(quest.steps[0].objectives[0]).toEqual({ type: 'collect', itemId: 'wood-1', quantity: 1 });
|
|
121
|
+
expect(quest.rewards[0]).toEqual({ itemId: 'coin', quantity: 1 });
|
|
122
|
+
expect(toQuestMsg({})).toMatchObject({ code: '', steps: [], unlocksQuestCodes: [] });
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('instance config', () => {
|
|
127
|
+
it('is the canonical defaults for a world without a conf', () => {
|
|
128
|
+
expect(toInstanceConfig(null)).toEqual(buildFallbackConfig());
|
|
129
|
+
expect(buildFallbackConfig()).toEqual(CYBERIA_INSTANCE_CONF_DEFAULTS);
|
|
130
|
+
expect(buildFallbackConfig()).not.toBe(CYBERIA_INSTANCE_CONF_DEFAULTS);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('keeps every field a conf sets and fills the rest from the defaults', () => {
|
|
134
|
+
const config = toInstanceConfig({
|
|
135
|
+
tickRate: 5,
|
|
136
|
+
economyRules: { portalFee: 9 },
|
|
137
|
+
skillRules: { projectileWidth: 3, doppelgangerSpawnChance: 0.2 },
|
|
138
|
+
equipmentRules: { requireSkin: false },
|
|
139
|
+
lifeRegenChance: 7,
|
|
140
|
+
});
|
|
141
|
+
const fb = CYBERIA_INSTANCE_CONF_DEFAULTS;
|
|
142
|
+
expect(config.tickRate).toBe(5);
|
|
143
|
+
expect(config.snapshotRate).toBe(fb.snapshotRate);
|
|
144
|
+
expect(config.economyRules).toEqual({ ...fb.economyRules, portalFee: 9 });
|
|
145
|
+
expect(config.skillRules).toEqual({ ...fb.skillRules, projectileWidth: 3, doppelgangerSpawnChance: 0.2 });
|
|
146
|
+
expect(config.equipmentRules).toEqual({ ...fb.equipmentRules, requireSkin: false });
|
|
147
|
+
expect(config.lifeRegenChance).toBe(fb.lifeRegenChance);
|
|
148
|
+
expect(config.skillConfig).toEqual([]);
|
|
149
|
+
expect(config.entityDefaults.map(({ entityType }) => entityType)).toEqual(
|
|
150
|
+
ENTITY_TYPE_DEFAULTS.map(({ entityType }) => entityType),
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('shapes skill and entity-type documents for the wire and drops the ones without a key', () => {
|
|
155
|
+
expect(skillDocsToConfig([{ triggerItemId: 'gun', skills: [{ logicEventId: 'projectile' }] }, {}, null])).toEqual([
|
|
156
|
+
{
|
|
157
|
+
triggerItemId: 'gun',
|
|
158
|
+
skills: [{ logicEventId: 'projectile', name: '', description: '', summonedEntityItemId: '' }],
|
|
159
|
+
},
|
|
160
|
+
]);
|
|
161
|
+
expect(
|
|
162
|
+
entityTypeDefaultDocsToConfig([{ entityType: 'bot', liveItemIds: ['x'] }, { liveItemIds: ['y'] }]),
|
|
163
|
+
).toMatchObject([{ entityType: 'bot', liveItemIds: ['x'], deadItemIds: [] }]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('completes a partial set of entity defaults with the canonical types it does not cover', () => {
|
|
167
|
+
const merged = mergeEntityDefaults([{ entityType: 'bot', liveItemIds: ['ghost'] }], { ghost: 'skin' });
|
|
168
|
+
const bots = merged.filter(({ entityType }) => entityType === 'bot');
|
|
169
|
+
expect(bots).toHaveLength(1);
|
|
170
|
+
expect(bots[0].liveItemIds).toEqual(['ghost']);
|
|
171
|
+
expect(merged.some(({ entityType }) => entityType === 'player')).toBe(true);
|
|
172
|
+
expect(itemTypesOf([{ data: { item: { id: 'ghost', type: 'skin' } } }, { data: {} }, null])).toEqual({
|
|
173
|
+
ghost: 'skin',
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('transport-agnostic fetchers', () => {
|
|
179
|
+
const ol = (id, type = 'skin') => ({ _id: `ol-${id}`, sha256: `sha-${id}`, data: { item: { id, type } } });
|
|
180
|
+
|
|
181
|
+
it('answers a ping with the server clock', () => {
|
|
182
|
+
expect(pingData().serverTimeMs).toBeGreaterThan(0);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('filters object layers by item type only when one is asked for', async () => {
|
|
186
|
+
expect(objectLayerQueryFilter('')).toEqual({});
|
|
187
|
+
expect(objectLayerQueryFilter('skin')).toEqual({ 'data.item.type': 'skin' });
|
|
188
|
+
const filters = [];
|
|
189
|
+
const models = { ObjectLayer: { find: (filter) => (filters.push(filter), lean([ol('anon')])) } };
|
|
190
|
+
const batch = await fetchObjectLayerBatch(models, 'skin');
|
|
191
|
+
expect(filters).toEqual([{ 'data.item.type': 'skin' }]);
|
|
192
|
+
expect(batch.map(({ item }) => item.id)).toEqual(['anon']);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('resolves one object layer by item id, or null', async () => {
|
|
196
|
+
const models = { ObjectLayer: { findOne: ({ 'data.item.id': id }) => lean(id === 'anon' ? ol('anon') : null) } };
|
|
197
|
+
expect((await fetchObjectLayer(models, 'anon')).mongoId).toBe('ol-anon');
|
|
198
|
+
expect(await fetchObjectLayer(models, 'ghost')).toBeNull();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('lists the id and digest of every object layer', async () => {
|
|
202
|
+
const models = { ObjectLayer: { find: () => lean([ol('anon'), { _id: 'x' }]) } };
|
|
203
|
+
expect(await fetchObjectLayerManifest(models)).toEqual({
|
|
204
|
+
entries: [
|
|
205
|
+
{ itemId: 'anon', sha256: 'sha-anon' },
|
|
206
|
+
{ itemId: '', sha256: '' },
|
|
207
|
+
],
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('serves a map and records which instance serves it, surviving a registry failure', async () => {
|
|
212
|
+
const registry = [];
|
|
213
|
+
const models = {
|
|
214
|
+
CyberiaMap: { findOne: ({ code }) => lean(code === 'forest-1' ? { _id: 'm1', code } : null) },
|
|
215
|
+
GlobalMapCodeRegistry: {
|
|
216
|
+
findOneAndUpdate: (filter, update) => {
|
|
217
|
+
registry.push({ filter, update });
|
|
218
|
+
return Promise.reject(new Error('registry down'));
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
expect(await fetchMapData(models, { mapCode: 'ghost' })).toBeNull();
|
|
223
|
+
expect(await fetchMapData(models, { mapCode: 'forest-1' })).toMatchObject({ map: { code: 'forest-1' } });
|
|
224
|
+
expect(registry).toEqual([]);
|
|
225
|
+
const served = await fetchMapData(models, { mapCode: 'forest-1', instanceCode: 'FOREST' });
|
|
226
|
+
expect(served.map.mongoId).toBe('m1');
|
|
227
|
+
expect(registry).toEqual([
|
|
228
|
+
{ filter: { mapCode: 'forest-1' }, update: { instanceCode: 'FOREST', status: 'active' } },
|
|
229
|
+
]);
|
|
230
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('full world load', () => {
|
|
235
|
+
const ol = (id, type = 'skin') => ({ _id: `ol-${id}`, sha256: `sha-${id}`, data: { item: { id, type } } });
|
|
236
|
+
|
|
237
|
+
it('serves the procedural world, with the canonical content, when the instance is unknown', async () => {
|
|
238
|
+
const queries = [];
|
|
239
|
+
const models = {
|
|
240
|
+
CyberiaInstance: { findOne: () => lean(null) },
|
|
241
|
+
ObjectLayer: { find: (filter) => (queries.push(filter), lean([ol('anon'), ol(DEFAULT_DEAD_ITEM_ID)])) },
|
|
242
|
+
};
|
|
243
|
+
const world = await fetchFullInstance(models, '');
|
|
244
|
+
expect(world.instance.seed).toBe('default');
|
|
245
|
+
expect(world.maps.length).toBeGreaterThan(0);
|
|
246
|
+
expect(world.maps[0].entities[0]).toHaveProperty('colorA');
|
|
247
|
+
expect(world.objectLayers.map(({ item }) => item.id)).toEqual(['anon', DEFAULT_DEAD_ITEM_ID]);
|
|
248
|
+
expect(world.config.skillConfig.map(({ triggerItemId }) => triggerItemId)).toContain('atlas_pistol_mk2');
|
|
249
|
+
expect(world.actions.length).toBeGreaterThan(0);
|
|
250
|
+
expect(world.quests.length).toBeGreaterThan(0);
|
|
251
|
+
expect(world.version).toMatch(/^fallback-[0-9a-f]{64}$/);
|
|
252
|
+
// Every canonical item is asked for, so anything a player can pick up resolves an atlas.
|
|
253
|
+
const wanted = queries[0]['data.item.id'].$in;
|
|
254
|
+
expect(wanted).toContain('anon');
|
|
255
|
+
expect(wanted).toContain(DEFAULT_DEAD_ITEM_ID);
|
|
256
|
+
|
|
257
|
+
const again = await fetchFullInstance(models, '');
|
|
258
|
+
expect(again.version).toBe(world.version);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('loads a stored world from its conf, maps, content and the skills its items trigger', async () => {
|
|
262
|
+
const conf = { _id: 'c1', instanceCode: 'FOREST', tickRate: 7, entityTypeDefaults: [], updatedAt: 't1' };
|
|
263
|
+
const models = {
|
|
264
|
+
CyberiaInstance: {
|
|
265
|
+
findOne: () => lean({ _id: 'i1', code: 'FOREST', cyberiaMapCodes: ['forest-1'], conf, updatedAt: 't0' }),
|
|
266
|
+
},
|
|
267
|
+
CyberiaMap: {
|
|
268
|
+
find: () => lean([{ _id: 'm1', code: 'forest-1', entities: [{ objectLayerItemIds: ['atlas_pistol_mk2'] }] }]),
|
|
269
|
+
},
|
|
270
|
+
CyberiaEntityTypeDefault: { find: () => lean([]) },
|
|
271
|
+
CyberiaAction: {
|
|
272
|
+
find: () => lean([{ code: 'shop', sourceMapCode: 'forest-1', shopItems: [{ itemId: 'hatchet' }] }]),
|
|
273
|
+
},
|
|
274
|
+
CyberiaQuest: { find: () => lean([{ code: 'q1', sourceMapCode: 'forest-1' }]) },
|
|
275
|
+
CyberiaSkill: { find: () => lean([]) },
|
|
276
|
+
ObjectLayer: {
|
|
277
|
+
find: ({ 'data.item.id': { $in } }) =>
|
|
278
|
+
lean(
|
|
279
|
+
['atlas_pistol_mk2', 'hatchet', 'atlas_pistol_mk2_bullet']
|
|
280
|
+
.filter((id) => $in.includes(id))
|
|
281
|
+
.map((id) => ol(id)),
|
|
282
|
+
),
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
const world = await fetchFullInstance(models, 'FOREST');
|
|
286
|
+
expect(world.instance.code).toBe('FOREST');
|
|
287
|
+
expect(world.config.tickRate).toBe(7);
|
|
288
|
+
// The map places the pistol and the vendor sells the hatchet: both trigger skills this world runs.
|
|
289
|
+
const triggers = world.config.skillConfig.map(({ triggerItemId }) => triggerItemId);
|
|
290
|
+
expect(triggers).toContain('atlas_pistol_mk2');
|
|
291
|
+
expect(triggers).toContain('hatchet');
|
|
292
|
+
// What the skill summons needs an atlas too.
|
|
293
|
+
expect(world.objectLayers.map(({ item }) => item.id)).toContain('atlas_pistol_mk2_bullet');
|
|
294
|
+
expect(world.objectLayers.map(({ item }) => item.id)).toContain('hatchet');
|
|
295
|
+
expect(world.actions[0].shopItems[0].itemId).toBe('hatchet');
|
|
296
|
+
expect(world.quests[0].code).toBe('q1');
|
|
297
|
+
expect(world.version).toMatch(/^[0-9a-f]{64}$/);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('runs a world whose conf reference is gone on the canonical defaults', async () => {
|
|
301
|
+
const models = {
|
|
302
|
+
CyberiaInstance: { findOne: () => lean({ _id: 'i1', code: 'BARE', cyberiaMapCodes: [], conf: null }) },
|
|
303
|
+
CyberiaInstanceConf: { findOne: () => lean(null) },
|
|
304
|
+
CyberiaEntityTypeDefault: { find: () => lean([]) },
|
|
305
|
+
ObjectLayer: { find: () => lean([]) },
|
|
306
|
+
};
|
|
307
|
+
const world = await fetchFullInstance(models, 'BARE');
|
|
308
|
+
expect(world.config.tickRate).toBe(CYBERIA_INSTANCE_CONF_DEFAULTS.tickRate);
|
|
309
|
+
expect(world.maps).toEqual([]);
|
|
310
|
+
expect(world.actions).toEqual([]);
|
|
311
|
+
expect(world.quests).toEqual([]);
|
|
312
|
+
expect(world.objectLayers).toEqual([]);
|
|
313
|
+
});
|
|
314
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { expect } from 'chai';
|
|
4
|
+
import shell from 'shelljs';
|
|
5
|
+
import { vi } from 'vitest';
|
|
6
|
+
import UnderpostDeploy from '../../src/cli/deploy.js';
|
|
7
|
+
|
|
8
|
+
describe('deploy legacy Gateway API sweep', () => {
|
|
9
|
+
let commands;
|
|
10
|
+
let listed;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
commands = [];
|
|
14
|
+
listed = { Gateway: '', ClientTrafficPolicy: '' };
|
|
15
|
+
vi.spyOn(shell, 'exec').mockImplementation((command) => {
|
|
16
|
+
commands.push(command);
|
|
17
|
+
const kind = command.match(/^kubectl get (\w+) /)?.[1];
|
|
18
|
+
const stdout = kind ? listed[kind] : '';
|
|
19
|
+
return { code: 0, stdout, stderr: '', toString: () => stdout };
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => vi.restoreAllMocks());
|
|
24
|
+
|
|
25
|
+
const deletes = () => commands.filter((command) => command.includes('kubectl delete'));
|
|
26
|
+
|
|
27
|
+
it('lists each kind once and deletes nothing on a namespace that was already migrated', () => {
|
|
28
|
+
// Regression: the sweep deleted `Gateway <host>`, `Gateway undefined` and both
|
|
29
|
+
// `-http3` policies blind, once per host — two dozen no-op round-trips per sync.
|
|
30
|
+
listed.Gateway = 'gateway.gateway.networking.k8s.io/dd-prototype-production\n';
|
|
31
|
+
listed.ClientTrafficPolicy = 'clienttrafficpolicy.gateway.envoyproxy.io/dd-prototype-production-http3\n';
|
|
32
|
+
const removed = UnderpostDeploy.API.sweepLegacyGatewayObjects({
|
|
33
|
+
hosts: ['healthcare.nexodev.org', 'www.bymyelectrics.com', 'bymyelectrics.com'],
|
|
34
|
+
namespace: 'default',
|
|
35
|
+
});
|
|
36
|
+
expect(removed).to.deep.equal({ gateways: [], policies: [] });
|
|
37
|
+
expect(commands.filter((command) => command.startsWith('kubectl get ')).length).to.equal(2);
|
|
38
|
+
expect(deletes()).to.deep.equal([]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('removes only the per-host and unresolved-host objects that still exist', () => {
|
|
42
|
+
listed.Gateway = [
|
|
43
|
+
'gateway.gateway.networking.k8s.io/dd-prototype-production',
|
|
44
|
+
'gateway.gateway.networking.k8s.io/healthcare.nexodev.org',
|
|
45
|
+
'gateway.gateway.networking.k8s.io/undefined',
|
|
46
|
+
'gateway.gateway.networking.k8s.io/other-deploy-production',
|
|
47
|
+
].join('\n');
|
|
48
|
+
listed.ClientTrafficPolicy = 'clienttrafficpolicy.gateway.envoyproxy.io/undefined-http3\n';
|
|
49
|
+
const removed = UnderpostDeploy.API.sweepLegacyGatewayObjects({
|
|
50
|
+
hosts: ['healthcare.nexodev.org', 'www.bymyelectrics.com'],
|
|
51
|
+
namespace: 'default',
|
|
52
|
+
});
|
|
53
|
+
expect(removed).to.deep.equal({
|
|
54
|
+
gateways: ['healthcare.nexodev.org', 'undefined'],
|
|
55
|
+
policies: ['undefined-http3'],
|
|
56
|
+
});
|
|
57
|
+
expect(deletes()).to.deep.equal([
|
|
58
|
+
'sudo kubectl delete Gateway healthcare.nexodev.org -n default --ignore-not-found',
|
|
59
|
+
'sudo kubectl delete Gateway undefined -n default --ignore-not-found',
|
|
60
|
+
'sudo kubectl delete ClientTrafficPolicy undefined-http3 -n default --ignore-not-found',
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -516,3 +516,41 @@ function render(output, columns, lines) {
|
|
|
516
516
|
|
|
517
517
|
return screen.filter((line) => line.trim().length > 0);
|
|
518
518
|
}
|
|
519
|
+
|
|
520
|
+
describe('run_quiet stream wiring', () => {
|
|
521
|
+
let wiringDir;
|
|
522
|
+
|
|
523
|
+
beforeEach(() => {
|
|
524
|
+
wiringDir = fs.mkdtempSync(path.join(os.tmpdir(), 'run-quiet-wiring-'));
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
afterEach(() => fs.removeSync(wiringDir));
|
|
528
|
+
|
|
529
|
+
// A command that finishes before tee holds the merged writer lets awk read EOF, and tee then
|
|
530
|
+
// blocks on an open with no reader until the 10 s drain kills it. A delay between tee's two
|
|
531
|
+
// opens, in the order the helper declares them, turns that scheduling race into a certainty.
|
|
532
|
+
it('finishes a short command even when tee opens its streams late', () => {
|
|
533
|
+
const wiring = /^(\s*)tee -a "\$error_log" (\S+) (\S+) &$/m;
|
|
534
|
+
const source = fs.readFileSync(loggingLib, 'utf8');
|
|
535
|
+
const [line, indent, first, second] = source.match(wiring) ?? [];
|
|
536
|
+
expect(line, 'tee wiring line').to.be.a('string');
|
|
537
|
+
const delayedLib = path.join(wiringDir, 'logging.sh');
|
|
538
|
+
fs.writeFileSync(
|
|
539
|
+
delayedLib,
|
|
540
|
+
source.replace(line, `${indent}{ sleep 0.3; exec tee -a "$error_log" ${second}; } ${first} &`),
|
|
541
|
+
);
|
|
542
|
+
const env = { ...process.env, TERM: 'xterm' };
|
|
543
|
+
for (const key of ['GITHUB_ACTIONS', 'RUN_QUIET_CI', 'RUN_QUIET_DEBUG']) delete env[key];
|
|
544
|
+
|
|
545
|
+
const started = Date.now();
|
|
546
|
+
execFileSync(
|
|
547
|
+
'bash',
|
|
548
|
+
[
|
|
549
|
+
'-c',
|
|
550
|
+
`source ${JSON.stringify(delayedLib)}; run_quiet "Sync probe" "Target pod:" 14 bash -c 'echo out; echo err >&2'`,
|
|
551
|
+
],
|
|
552
|
+
{ env, stdio: ['ignore', 'pipe', 'pipe'] },
|
|
553
|
+
);
|
|
554
|
+
expect(Date.now() - started).to.be.below(5000);
|
|
555
|
+
}, 20_000);
|
|
556
|
+
});
|
|
@@ -61,6 +61,7 @@ describe('command capability classification', () => {
|
|
|
61
61
|
expect(classifyCommand('sudo dnf install -y cockpit')).to.equal('host:write');
|
|
62
62
|
expect(classifyCommand('sudo systemctl enable --now cockpit.socket')).to.equal('host:write');
|
|
63
63
|
expect(classifyCommand('curl https://example.com')).to.equal('net');
|
|
64
|
+
expect(classifyCommand("'/x/node_modules/.bin/socket' 'scan' 'create' '--json' '.'")).to.equal('net');
|
|
64
65
|
expect(classifyCommand('git add .')).to.equal('git');
|
|
65
66
|
});
|
|
66
67
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { expect } from 'chai';
|
|
4
|
+
import shell from 'shelljs';
|
|
5
|
+
import { vi } from 'vitest';
|
|
6
|
+
import { gatewayPodFetch, parseRawHttpResponse } from '../../src/server/network/underpost-gateway.js';
|
|
7
|
+
|
|
8
|
+
describe('gateway pod fetch', () => {
|
|
9
|
+
let commands;
|
|
10
|
+
let answer;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
commands = [];
|
|
14
|
+
answer = (command) =>
|
|
15
|
+
command.includes('| nc ') ? 'HTTP/1.1 502 Bad Gateway\r\nServer: nginx\r\n\r\n<h1>maintenance</h1>\n' : '';
|
|
16
|
+
vi.spyOn(shell, 'exec').mockImplementation((command) => {
|
|
17
|
+
commands.push(command);
|
|
18
|
+
const stdout = answer(command);
|
|
19
|
+
return { code: 0, stdout, stderr: '', toString: () => stdout };
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => vi.restoreAllMocks());
|
|
24
|
+
|
|
25
|
+
it('reads the status and the body of an error response off the socket', () => {
|
|
26
|
+
// Regression: busybox wget discards the body of any non-2xx answer, so the fallback
|
|
27
|
+
// document — delivered with an upstream-failure status by definition — never
|
|
28
|
+
// matched, and the probe polled every attempt against a correct edge.
|
|
29
|
+
const result = gatewayPodFetch({ namespace: 'default', host: 'www.bymyelectrics.com', path: '/' });
|
|
30
|
+
expect(result).to.deep.equal({ status: '502', body: '<h1>maintenance</h1>\n' });
|
|
31
|
+
expect(commands.length).to.equal(1);
|
|
32
|
+
expect(commands[0]).to.include(
|
|
33
|
+
"printf 'GET / HTTP/1.0\\r\\nHost: www.bymyelectrics.com\\r\\n\\r\\n' | nc -w 10 127.0.0.1 80",
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('falls back to the wget pair for the status where nc is absent', () => {
|
|
38
|
+
answer = (command) => (command.includes('wget -S') ? 'HTTP/1.1 503 Service Unavailable\n' : '');
|
|
39
|
+
const result = gatewayPodFetch({ namespace: 'default', host: 'healthcare.nexodev.org' });
|
|
40
|
+
expect(result.status).to.equal('503');
|
|
41
|
+
expect(commands.length).to.equal(3);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('parses a raw response byte for byte after the first blank line', () => {
|
|
45
|
+
expect(parseRawHttpResponse('')).to.deep.equal({ status: '', body: '' });
|
|
46
|
+
expect(parseRawHttpResponse('HTTP/1.0 200 OK\r\nX: y\r\n\r\n')).to.deep.equal({ status: '200', body: '' });
|
|
47
|
+
expect(parseRawHttpResponse('HTTP/1.1 504 Gateway Time-out\n\nline\n\nmore')).to.deep.equal({
|
|
48
|
+
status: '504',
|
|
49
|
+
body: 'line\n\nmore',
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('gateway fetch', () => {
|
|
55
|
+
afterEach(() => vi.restoreAllMocks());
|
|
56
|
+
|
|
57
|
+
const mock = (answer) => {
|
|
58
|
+
const commands = [];
|
|
59
|
+
vi.spyOn(shell, 'exec').mockImplementation((command) => {
|
|
60
|
+
commands.push(command);
|
|
61
|
+
const stdout = answer(command);
|
|
62
|
+
return { code: 0, stdout, stderr: '', toString: () => stdout };
|
|
63
|
+
});
|
|
64
|
+
return commands;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
it('asks the gateway Service from this host with curl, keeping the error body', async () => {
|
|
68
|
+
const { gatewayFetch } = await import('../../src/server/network/underpost-gateway.js');
|
|
69
|
+
const commands = mock((command) => {
|
|
70
|
+
if (command.includes('kubectl get svc underpost-gateway-service')) return '10.96.151.82\n';
|
|
71
|
+
if (command.startsWith('curl ')) return 'HTTP/1.1 502 Bad Gateway\r\nServer: nginx\r\n\r\n<h1>maintenance</h1>\n';
|
|
72
|
+
return '';
|
|
73
|
+
});
|
|
74
|
+
const result = gatewayFetch({ namespace: 'default', host: 'healthcare.nexodev.org', path: '/' });
|
|
75
|
+
expect(result).to.deep.equal({ status: '502', body: '<h1>maintenance</h1>\n', via: 'host' });
|
|
76
|
+
expect(commands.find((command) => command.startsWith('curl '))).to.include(
|
|
77
|
+
"-H 'Host: healthcare.nexodev.org' http://10.96.151.82:80/",
|
|
78
|
+
);
|
|
79
|
+
expect(commands.some((command) => command.includes('kubectl exec'))).to.equal(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('falls back to the pod when the Service cannot be reached from here', async () => {
|
|
83
|
+
const { gatewayFetch } = await import('../../src/server/network/underpost-gateway.js');
|
|
84
|
+
mock((command) => {
|
|
85
|
+
if (command.includes('kubectl get svc underpost-gateway-service')) return '10.96.151.82\n';
|
|
86
|
+
if (command.includes('| nc ')) return 'HTTP/1.1 503 Service Unavailable\r\n\r\nbody';
|
|
87
|
+
return '';
|
|
88
|
+
});
|
|
89
|
+
expect(gatewayFetch({ namespace: 'default', host: 'healthcare.nexodev.org' })).to.deep.equal({
|
|
90
|
+
status: '503',
|
|
91
|
+
body: 'body',
|
|
92
|
+
via: 'pod',
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
import { expect } from 'chai';
|
|
4
|
+
import { load as yamlLoad } from 'js-yaml';
|
|
5
|
+
import shell from 'shelljs';
|
|
6
|
+
import { vi } from 'vitest';
|
|
7
|
+
import UnderpostCluster from '../../src/cli/cluster.js';
|
|
8
|
+
import UnderpostDeploy, { LETSENCRYPT_ISSUER_MANIFEST, LETSENCRYPT_ISSUER_NAME } from '../../src/cli/deploy.js';
|
|
9
|
+
|
|
10
|
+
describe("Let's Encrypt issuer for the ingress stack in use", () => {
|
|
11
|
+
afterEach(() => vi.restoreAllMocks());
|
|
12
|
+
|
|
13
|
+
describe('acmeSolverFactory', () => {
|
|
14
|
+
it('answers through the Gateway API whenever its data plane is installed', () => {
|
|
15
|
+
// The `cluster` runner installs the issuer in an invocation that carries no stack
|
|
16
|
+
// flag, so the decision has to come from the cluster, not from the flags.
|
|
17
|
+
const presence = { contour: false, gateway: true };
|
|
18
|
+
expect(UnderpostCluster.API.acmeSolverFactory({ options: {}, presence })).to.equal('gateway');
|
|
19
|
+
expect(UnderpostCluster.API.acmeSolverFactory({ options: { gatewayApi: true }, presence })).to.equal('gateway');
|
|
20
|
+
expect(
|
|
21
|
+
UnderpostCluster.API.acmeSolverFactory({
|
|
22
|
+
options: { gatewayApi: true },
|
|
23
|
+
presence: { contour: true, gateway: false },
|
|
24
|
+
}),
|
|
25
|
+
).to.equal('gateway');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('keeps the Contour Ingress solver only where nothing else carries the challenge', () => {
|
|
29
|
+
expect(
|
|
30
|
+
UnderpostCluster.API.acmeSolverFactory({ options: {}, presence: { contour: true, gateway: false } }),
|
|
31
|
+
).to.equal('ingress');
|
|
32
|
+
expect(
|
|
33
|
+
UnderpostCluster.API.acmeSolverFactory({
|
|
34
|
+
options: { contour: true },
|
|
35
|
+
presence: { contour: false, gateway: true },
|
|
36
|
+
}),
|
|
37
|
+
).to.equal('ingress');
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe('letsEncryptIssuerYamlFactory', () => {
|
|
42
|
+
it('keeps the account from the base manifest and swaps in the Gateway solver', () => {
|
|
43
|
+
const base = yamlLoad(
|
|
44
|
+
UnderpostDeploy.API.letsEncryptIssuerYamlFactory({ solver: 'ingress', basePath: LETSENCRYPT_ISSUER_MANIFEST }),
|
|
45
|
+
);
|
|
46
|
+
const issuer = yamlLoad(
|
|
47
|
+
UnderpostDeploy.API.letsEncryptIssuerYamlFactory({
|
|
48
|
+
solver: 'gateway',
|
|
49
|
+
parentRefs: [
|
|
50
|
+
{ name: 'dd-core-production', namespace: 'default' },
|
|
51
|
+
{ name: 'dd-prototype-production', namespace: 'default' },
|
|
52
|
+
],
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
expect(issuer.kind).to.equal('ClusterIssuer');
|
|
56
|
+
expect(issuer.metadata.name).to.equal(LETSENCRYPT_ISSUER_NAME);
|
|
57
|
+
expect(issuer.spec.acme.email).to.equal(base.spec.acme.email);
|
|
58
|
+
expect(issuer.spec.acme.server).to.equal(base.spec.acme.server);
|
|
59
|
+
expect(issuer.spec.acme.privateKeySecretRef).to.deep.equal(base.spec.acme.privateKeySecretRef);
|
|
60
|
+
expect(issuer.spec.acme.solvers).to.deep.equal([
|
|
61
|
+
{
|
|
62
|
+
http01: {
|
|
63
|
+
gatewayHTTPRoute: {
|
|
64
|
+
parentRefs: [
|
|
65
|
+
{ name: 'dd-core-production', namespace: 'default', kind: 'Gateway' },
|
|
66
|
+
{ name: 'dd-prototype-production', namespace: 'default', kind: 'Gateway' },
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
]);
|
|
72
|
+
expect(base.spec.acme.solvers).to.deep.equal([{ http01: { ingress: { class: 'contour' } } }]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('refuses a Gateway solver with nothing to attach the challenge route to', () => {
|
|
76
|
+
expect(() => UnderpostDeploy.API.letsEncryptIssuerYamlFactory({ solver: 'gateway', parentRefs: [] })).to.throw(
|
|
77
|
+
/parentRef/,
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('acmeGatewayParentRefsFactory', () => {
|
|
83
|
+
it('unions the live Gateways with the routed deploys, once each, sorted', () => {
|
|
84
|
+
vi.spyOn(shell, 'exec').mockImplementation((command) => {
|
|
85
|
+
const stdout = command.startsWith('kubectl get gateway')
|
|
86
|
+
? 'gateway.gateway.networking.k8s.io/dd-core-production\ngateway.gateway.networking.k8s.io/other-production\n'
|
|
87
|
+
: '';
|
|
88
|
+
return { code: 0, stdout, stderr: '', toString: () => stdout };
|
|
89
|
+
});
|
|
90
|
+
vi.spyOn(UnderpostDeploy.API, 'gatewayNameFactory').mockImplementation(
|
|
91
|
+
({ deployId, env }) => `${deployId}-${env}`,
|
|
92
|
+
);
|
|
93
|
+
const refs = UnderpostDeploy.API.acmeGatewayParentRefsFactory({ namespace: 'default', env: 'production' });
|
|
94
|
+
const names = refs.map(({ name }) => name);
|
|
95
|
+
expect(names).to.include('dd-core-production');
|
|
96
|
+
expect(names).to.include('other-production');
|
|
97
|
+
expect(names).to.deep.equal([...new Set(names)].sort());
|
|
98
|
+
expect(refs.every(({ namespace }) => namespace === 'default')).to.equal(true);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
});
|