@celilo/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1566 -0
- package/bin/celilo +16 -0
- package/drizzle/0000_complex_puma.sql +179 -0
- package/drizzle/0001_dizzy_wolfpack.sql +2 -0
- package/drizzle/0002_web_routes.sql +16 -0
- package/drizzle/0003_backup_storage.sql +32 -0
- package/drizzle/meta/0000_snapshot.json +1151 -0
- package/drizzle/meta/0001_snapshot.json +1167 -0
- package/drizzle/meta/0002_snapshot.json +1257 -0
- package/drizzle/meta/_journal.json +27 -0
- package/package.json +64 -0
- package/schemas/system_config.json +106 -0
- package/src/__integration__/container-services-cli.integration.test.ts +246 -0
- package/src/ansible/dependencies.test.ts +309 -0
- package/src/ansible/dependencies.ts +896 -0
- package/src/ansible/inventory.test.ts +463 -0
- package/src/ansible/inventory.ts +445 -0
- package/src/ansible/secrets.ts +222 -0
- package/src/ansible/validation.test.ts +92 -0
- package/src/ansible/validation.ts +272 -0
- package/src/api-clients/digitalocean.ts +94 -0
- package/src/api-clients/proxmox.ts +655 -0
- package/src/capabilities/logging-wrapper.test.ts +217 -0
- package/src/capabilities/lookup.test.ts +149 -0
- package/src/capabilities/lookup.ts +89 -0
- package/src/capabilities/public-web-helpers.test.ts +198 -0
- package/src/capabilities/public-web-publish.test.ts +458 -0
- package/src/capabilities/registration.test.ts +395 -0
- package/src/capabilities/registration.ts +200 -0
- package/src/capabilities/route-validation.test.ts +121 -0
- package/src/capabilities/route-validation.ts +96 -0
- package/src/capabilities/secret-ref.test.ts +313 -0
- package/src/capabilities/secret-validation.ts +157 -0
- package/src/capabilities/secrets.test.ts +750 -0
- package/src/capabilities/secrets.ts +244 -0
- package/src/capabilities/validation.test.ts +613 -0
- package/src/capabilities/validation.ts +160 -0
- package/src/capabilities/well-known.test.ts +238 -0
- package/src/capabilities/well-known.ts +222 -0
- package/src/cli/cli.test.ts +654 -0
- package/src/cli/command-registry.ts +742 -0
- package/src/cli/command-tree-parser.test.ts +180 -0
- package/src/cli/command-tree-parser.ts +193 -0
- package/src/cli/commands/backup-create.ts +137 -0
- package/src/cli/commands/backup-delete.ts +74 -0
- package/src/cli/commands/backup-import.ts +97 -0
- package/src/cli/commands/backup-list.ts +132 -0
- package/src/cli/commands/backup-name.ts +73 -0
- package/src/cli/commands/backup-prune.ts +98 -0
- package/src/cli/commands/backup-restore.ts +122 -0
- package/src/cli/commands/capability-info.ts +121 -0
- package/src/cli/commands/capability-list.ts +47 -0
- package/src/cli/commands/completion.ts +87 -0
- package/src/cli/commands/hook-run.ts +176 -0
- package/src/cli/commands/ipam.ts +607 -0
- package/src/cli/commands/machine-add.ts +235 -0
- package/src/cli/commands/machine-earmark.ts +82 -0
- package/src/cli/commands/machine-list.ts +77 -0
- package/src/cli/commands/machine-remove.ts +90 -0
- package/src/cli/commands/machine-status.ts +131 -0
- package/src/cli/commands/module-audit.ts +51 -0
- package/src/cli/commands/module-build.ts +60 -0
- package/src/cli/commands/module-config.ts +170 -0
- package/src/cli/commands/module-deploy.ts +71 -0
- package/src/cli/commands/module-generate.ts +236 -0
- package/src/cli/commands/module-health.ts +108 -0
- package/src/cli/commands/module-import.ts +80 -0
- package/src/cli/commands/module-list.ts +43 -0
- package/src/cli/commands/module-logs.ts +73 -0
- package/src/cli/commands/module-remove.ts +162 -0
- package/src/cli/commands/module-show.ts +208 -0
- package/src/cli/commands/module-status.ts +131 -0
- package/src/cli/commands/module-types.ts +189 -0
- package/src/cli/commands/module-upgrade.ts +192 -0
- package/src/cli/commands/package.ts +68 -0
- package/src/cli/commands/secret-list.ts +99 -0
- package/src/cli/commands/secret-set.ts +134 -0
- package/src/cli/commands/service-add-digitalocean.ts +133 -0
- package/src/cli/commands/service-add-proxmox.ts +342 -0
- package/src/cli/commands/service-config-get.ts +83 -0
- package/src/cli/commands/service-config-set.ts +145 -0
- package/src/cli/commands/service-list.ts +74 -0
- package/src/cli/commands/service-reconfigure.ts +230 -0
- package/src/cli/commands/service-remove.ts +103 -0
- package/src/cli/commands/service-verify.ts +240 -0
- package/src/cli/commands/status.ts +216 -0
- package/src/cli/commands/storage-add-local.ts +106 -0
- package/src/cli/commands/storage-add-s3.ts +114 -0
- package/src/cli/commands/storage-list.ts +72 -0
- package/src/cli/commands/storage-remove.ts +54 -0
- package/src/cli/commands/storage-set-default.ts +44 -0
- package/src/cli/commands/storage-verify.ts +54 -0
- package/src/cli/commands/system-config.ts +168 -0
- package/src/cli/commands/system-init.ts +314 -0
- package/src/cli/commands/system-secret-get.ts +98 -0
- package/src/cli/commands/system-secret-set.ts +76 -0
- package/src/cli/commands/system-vault-password.ts +34 -0
- package/src/cli/completion.test.ts +37 -0
- package/src/cli/completion.ts +482 -0
- package/src/cli/fuel-gauge.test.ts +208 -0
- package/src/cli/fuel-gauge.ts +405 -0
- package/src/cli/generate-zsh-completion.test.ts +95 -0
- package/src/cli/generate-zsh-completion.ts +497 -0
- package/src/cli/index.ts +1583 -0
- package/src/cli/interactive-config.test.ts +201 -0
- package/src/cli/interactive-config.ts +62 -0
- package/src/cli/parser.test.ts +227 -0
- package/src/cli/parser.ts +244 -0
- package/src/cli/prompts.test.ts +33 -0
- package/src/cli/prompts.ts +121 -0
- package/src/cli/types.ts +38 -0
- package/src/cli/validators.test.ts +235 -0
- package/src/cli/validators.ts +188 -0
- package/src/config/env.ts +41 -0
- package/src/config/paths.test.ts +172 -0
- package/src/config/paths.ts +108 -0
- package/src/db/client.ts +190 -0
- package/src/db/migrate.ts +30 -0
- package/src/db/schema.test.ts +221 -0
- package/src/db/schema.ts +434 -0
- package/src/hooks/capability-loader-firewall.test.ts +246 -0
- package/src/hooks/capability-loader.test.ts +100 -0
- package/src/hooks/capability-loader.ts +520 -0
- package/src/hooks/define-hook.test.ts +488 -0
- package/src/hooks/executor.test.ts +462 -0
- package/src/hooks/executor.ts +469 -0
- package/src/hooks/logger.test.ts +54 -0
- package/src/hooks/logger.ts +95 -0
- package/src/hooks/test-fixtures/failing-hook.ts +13 -0
- package/src/hooks/test-fixtures/no-default-hook.ts +6 -0
- package/src/hooks/test-fixtures/success-hook.ts +20 -0
- package/src/hooks/test-fixtures/unbranded-hook.ts +11 -0
- package/src/hooks/test-fixtures/void-hook.ts +13 -0
- package/src/hooks/types.ts +89 -0
- package/src/infrastructure/property-extractor.test.ts +194 -0
- package/src/infrastructure/property-extractor.ts +151 -0
- package/src/ipam/allocator.test.ts +442 -0
- package/src/ipam/allocator.ts +369 -0
- package/src/ipam/auto-allocator.test.ts +247 -0
- package/src/ipam/auto-allocator.ts +270 -0
- package/src/ipam/subnet-parser.test.ts +107 -0
- package/src/ipam/subnet-parser.ts +136 -0
- package/src/manifest/contracts/index.ts +61 -0
- package/src/manifest/contracts/v1.ts +118 -0
- package/src/manifest/json-schema-roundtrip.test.ts +99 -0
- package/src/manifest/schema.ts +367 -0
- package/src/manifest/template-validator.test.ts +231 -0
- package/src/manifest/template-validator.ts +322 -0
- package/src/manifest/validate.test.ts +1180 -0
- package/src/manifest/validate.ts +415 -0
- package/src/module/import.test.ts +355 -0
- package/src/module/import.ts +676 -0
- package/src/module/packaging/audit.ts +169 -0
- package/src/module/packaging/build.ts +228 -0
- package/src/module/packaging/checksum.ts +41 -0
- package/src/module/packaging/extract.ts +234 -0
- package/src/module/packaging/signature.ts +47 -0
- package/src/secrets/encryption.test.ts +284 -0
- package/src/secrets/encryption.ts +162 -0
- package/src/secrets/generators.test.ts +112 -0
- package/src/secrets/generators.ts +127 -0
- package/src/secrets/master-key.test.ts +159 -0
- package/src/secrets/master-key.ts +114 -0
- package/src/secrets/storage.test.ts +115 -0
- package/src/secrets/storage.ts +106 -0
- package/src/secrets/vault.test.ts +35 -0
- package/src/secrets/vault.ts +42 -0
- package/src/services/backup-create.ts +532 -0
- package/src/services/backup-metadata.ts +198 -0
- package/src/services/backup-restore.ts +229 -0
- package/src/services/backup-retention.ts +84 -0
- package/src/services/backup-storage.ts +281 -0
- package/src/services/build-stream.test.ts +122 -0
- package/src/services/build-stream.ts +201 -0
- package/src/services/config-interview.ts +694 -0
- package/src/services/container-service.test.ts +298 -0
- package/src/services/container-service.ts +401 -0
- package/src/services/cross-module-data-manager.test.ts +405 -0
- package/src/services/cross-module-data-manager.ts +412 -0
- package/src/services/deploy-ansible.ts +88 -0
- package/src/services/deploy-planner.ts +153 -0
- package/src/services/deploy-preflight.ts +274 -0
- package/src/services/deploy-ssh.ts +131 -0
- package/src/services/deploy-terraform.test.ts +55 -0
- package/src/services/deploy-terraform.ts +445 -0
- package/src/services/deploy-validation.ts +311 -0
- package/src/services/dns-auto-register.ts +211 -0
- package/src/services/health-runner.ts +184 -0
- package/src/services/infrastructure-selector.test.ts +485 -0
- package/src/services/infrastructure-selector.ts +245 -0
- package/src/services/infrastructure-variable-resolver.test.ts +751 -0
- package/src/services/infrastructure-variable-resolver.ts +234 -0
- package/src/services/machine-detector.ts +328 -0
- package/src/services/machine-pool.test.ts +405 -0
- package/src/services/machine-pool.ts +316 -0
- package/src/services/manifest-validation.ts +120 -0
- package/src/services/module-build.test.ts +290 -0
- package/src/services/module-build.ts +431 -0
- package/src/services/module-config.test.ts +237 -0
- package/src/services/module-config.ts +298 -0
- package/src/services/module-deploy.ts +862 -0
- package/src/services/module-types-drift.test.ts +73 -0
- package/src/services/module-types-generator.test.ts +288 -0
- package/src/services/module-types-generator.ts +189 -0
- package/src/services/proxmox-state-recovery.ts +140 -0
- package/src/services/schema-validation.ts +155 -0
- package/src/services/secret-schema-loader.test.ts +311 -0
- package/src/services/secret-schema-loader.ts +239 -0
- package/src/services/ssh-key-manager.test.ts +283 -0
- package/src/services/ssh-key-manager.ts +193 -0
- package/src/services/storage-providers/local.ts +105 -0
- package/src/services/storage-providers/s3.ts +182 -0
- package/src/services/storage-providers/types.ts +24 -0
- package/src/services/system-config-schema-types.ts +25 -0
- package/src/services/system-config-validator.test.ts +160 -0
- package/src/services/system-config-validator.ts +74 -0
- package/src/services/system-init.test.ts +153 -0
- package/src/services/system-init.ts +253 -0
- package/src/services/terraform-safety.ts +174 -0
- package/src/services/zone-detector.test.ts +110 -0
- package/src/services/zone-detector.ts +102 -0
- package/src/services/zone-policy.test.ts +97 -0
- package/src/services/zone-policy.ts +126 -0
- package/src/templates/generator.test.ts +645 -0
- package/src/templates/generator.ts +1119 -0
- package/src/templates/types.ts +62 -0
- package/src/test-utils/INTERACTIVE_PROMPTS.md +167 -0
- package/src/test-utils/cli-context-interactive.test.ts +152 -0
- package/src/test-utils/cli-context-server.test.ts +66 -0
- package/src/test-utils/cli-context.test.ts +273 -0
- package/src/test-utils/cli-context.ts +677 -0
- package/src/test-utils/cli-result.test.ts +282 -0
- package/src/test-utils/cli-result.ts +241 -0
- package/src/test-utils/cli.ts +55 -0
- package/src/test-utils/completion-harness.test.ts +126 -0
- package/src/test-utils/completion-harness.ts +82 -0
- package/src/test-utils/database.test.ts +182 -0
- package/src/test-utils/database.ts +126 -0
- package/src/test-utils/filesystem.test.ts +208 -0
- package/src/test-utils/filesystem.ts +142 -0
- package/src/test-utils/fixtures.test.ts +123 -0
- package/src/test-utils/fixtures.ts +160 -0
- package/src/test-utils/golden-diff.ts +197 -0
- package/src/test-utils/index.ts +77 -0
- package/src/test-utils/integration.ts +81 -0
- package/src/test-utils/module-fixtures.ts +468 -0
- package/src/test-utils/modules.test.ts +144 -0
- package/src/test-utils/modules.ts +183 -0
- package/src/test-utils/setup-test-db.ts +90 -0
- package/src/test-utils/value-extractor.test.ts +231 -0
- package/src/test-utils/value-extractor.ts +228 -0
- package/src/types/infrastructure.ts +157 -0
- package/src/utils/shell.test.ts +365 -0
- package/src/utils/shell.ts +159 -0
- package/src/validation/schemas.ts +166 -0
- package/src/variables/ansible-resolver.test.ts +142 -0
- package/src/variables/ansible-resolver.ts +69 -0
- package/src/variables/capability-self-ref.test.ts +220 -0
- package/src/variables/context.test.ts +1265 -0
- package/src/variables/context.ts +624 -0
- package/src/variables/declarative-derivation.test.ts +743 -0
- package/src/variables/declarative-derivation.ts +200 -0
- package/src/variables/parser.test.ts +231 -0
- package/src/variables/parser.ts +76 -0
- package/src/variables/resolver.test.ts +458 -0
- package/src/variables/resolver.ts +282 -0
- package/src/variables/types.ts +59 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `wrapWithLogging` (HOOK_API_V2 Phase 6 / D6).
|
|
3
|
+
*
|
|
4
|
+
* The wrapper is the framework's auto-logging layer for capability
|
|
5
|
+
* methods. The contract: log `→ <cap>.<method>` before, `✓` after,
|
|
6
|
+
* `✗` on failure (then re-throw). Method names ONLY — never any
|
|
7
|
+
* payload, never any result, because capability requests can carry
|
|
8
|
+
* secrets.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, test } from 'bun:test';
|
|
12
|
+
import { wrapWithLogging } from '@celilo/capabilities';
|
|
13
|
+
import { createCapturingLogger } from '../hooks/logger';
|
|
14
|
+
|
|
15
|
+
describe('wrapWithLogging', () => {
|
|
16
|
+
test('wraps async methods and emits arrow markers in order', async () => {
|
|
17
|
+
const { logger, messages } = createCapturingLogger();
|
|
18
|
+
|
|
19
|
+
const wrapped = wrapWithLogging(
|
|
20
|
+
{
|
|
21
|
+
async greet(name: string): Promise<string> {
|
|
22
|
+
return `hello, ${name}`;
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
logger,
|
|
26
|
+
'public_web',
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const result = await wrapped.greet('world');
|
|
30
|
+
|
|
31
|
+
expect(result).toBe('hello, world');
|
|
32
|
+
expect(messages).toEqual([
|
|
33
|
+
{ level: 'info', message: '→ public_web.greet' },
|
|
34
|
+
{ level: 'info', message: '✓ public_web.greet' },
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('captures errors, logs them at error level, then re-throws', async () => {
|
|
39
|
+
const { logger, messages } = createCapturingLogger();
|
|
40
|
+
|
|
41
|
+
const wrapped = wrapWithLogging(
|
|
42
|
+
{
|
|
43
|
+
async fail(): Promise<void> {
|
|
44
|
+
throw new Error('boom');
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
logger,
|
|
48
|
+
'idp',
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
await expect(wrapped.fail()).rejects.toThrow('boom');
|
|
52
|
+
|
|
53
|
+
expect(messages).toEqual([
|
|
54
|
+
{ level: 'info', message: '→ idp.fail' },
|
|
55
|
+
{ level: 'error', message: '✗ idp.fail: boom' },
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('does NOT log payload arguments — method name only', async () => {
|
|
60
|
+
const { logger, messages } = createCapturingLogger();
|
|
61
|
+
|
|
62
|
+
const wrapped = wrapWithLogging(
|
|
63
|
+
{
|
|
64
|
+
async create_user(req: { username: string; password: string }): Promise<void> {
|
|
65
|
+
// Method body — payload should never appear in any log line.
|
|
66
|
+
void req;
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
logger,
|
|
70
|
+
'idp',
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
await wrapped.create_user({ username: 'alice', password: 'super-secret-token' });
|
|
74
|
+
|
|
75
|
+
// Confirm the secret password never appears in any log message.
|
|
76
|
+
for (const m of messages) {
|
|
77
|
+
expect(m.message).not.toContain('alice');
|
|
78
|
+
expect(m.message).not.toContain('super-secret-token');
|
|
79
|
+
}
|
|
80
|
+
// And the only log lines are the arrow markers.
|
|
81
|
+
expect(messages.map((m) => m.message)).toEqual(['→ idp.create_user', '✓ idp.create_user']);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('does NOT log result values', async () => {
|
|
85
|
+
const { logger, messages } = createCapturingLogger();
|
|
86
|
+
|
|
87
|
+
const wrapped = wrapWithLogging(
|
|
88
|
+
{
|
|
89
|
+
async create_oidc_client(): Promise<{ client_id: string; client_secret: string }> {
|
|
90
|
+
return { client_id: 'abc', client_secret: 'this-is-a-secret-do-not-log' };
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
logger,
|
|
94
|
+
'idp',
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const result = await wrapped.create_oidc_client();
|
|
98
|
+
|
|
99
|
+
expect(result.client_secret).toBe('this-is-a-secret-do-not-log');
|
|
100
|
+
for (const m of messages) {
|
|
101
|
+
expect(m.message).not.toContain('this-is-a-secret-do-not-log');
|
|
102
|
+
expect(m.message).not.toContain('client_id');
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('preserves non-function properties unchanged', () => {
|
|
107
|
+
const { logger } = createCapturingLogger();
|
|
108
|
+
|
|
109
|
+
const original = {
|
|
110
|
+
version: '1.0.0',
|
|
111
|
+
capability: 'idp' as const,
|
|
112
|
+
async noop() {},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const wrapped = wrapWithLogging(original, logger, 'idp');
|
|
116
|
+
|
|
117
|
+
expect(wrapped.version).toBe('1.0.0');
|
|
118
|
+
expect(wrapped.capability).toBe('idp');
|
|
119
|
+
expect(typeof wrapped.noop).toBe('function');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('does not mutate the original methods object', async () => {
|
|
123
|
+
const { logger } = createCapturingLogger();
|
|
124
|
+
|
|
125
|
+
let originalCalls = 0;
|
|
126
|
+
const original = {
|
|
127
|
+
async tick() {
|
|
128
|
+
originalCalls++;
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const wrapped = wrapWithLogging(original, logger, 'cap');
|
|
133
|
+
|
|
134
|
+
// Calling the original should NOT trip the wrapper logger.
|
|
135
|
+
await original.tick();
|
|
136
|
+
expect(originalCalls).toBe(1);
|
|
137
|
+
|
|
138
|
+
// The wrapped reference must be a different function.
|
|
139
|
+
expect(wrapped.tick).not.toBe(original.tick);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('multiple methods on the same object are each wrapped independently', async () => {
|
|
143
|
+
const { logger, messages } = createCapturingLogger();
|
|
144
|
+
|
|
145
|
+
const wrapped = wrapWithLogging(
|
|
146
|
+
{
|
|
147
|
+
async a(): Promise<void> {},
|
|
148
|
+
async b(): Promise<void> {},
|
|
149
|
+
async c(): Promise<void> {},
|
|
150
|
+
},
|
|
151
|
+
logger,
|
|
152
|
+
'cap',
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
await wrapped.a();
|
|
156
|
+
await wrapped.b();
|
|
157
|
+
await wrapped.c();
|
|
158
|
+
|
|
159
|
+
const messageTexts = messages.map((m) => m.message);
|
|
160
|
+
expect(messageTexts).toEqual([
|
|
161
|
+
'→ cap.a',
|
|
162
|
+
'✓ cap.a',
|
|
163
|
+
'→ cap.b',
|
|
164
|
+
'✓ cap.b',
|
|
165
|
+
'→ cap.c',
|
|
166
|
+
'✓ cap.c',
|
|
167
|
+
]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('handles non-Error throws (string, plain object) by stringifying them', async () => {
|
|
171
|
+
const { logger, messages } = createCapturingLogger();
|
|
172
|
+
|
|
173
|
+
const wrapped = wrapWithLogging(
|
|
174
|
+
{
|
|
175
|
+
async throwString(): Promise<void> {
|
|
176
|
+
throw 'literal string';
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
logger,
|
|
180
|
+
'cap',
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
await expect(wrapped.throwString()).rejects.toBe('literal string');
|
|
184
|
+
|
|
185
|
+
expect(messages).toContainEqual({
|
|
186
|
+
level: 'error',
|
|
187
|
+
message: '✗ cap.throwString: literal string',
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('preserves `this` binding inside wrapped methods', async () => {
|
|
192
|
+
// The wrapper always returns an async function regardless of whether
|
|
193
|
+
// the original method was sync or async. This is fine in practice
|
|
194
|
+
// because every real capability interface declares its methods as
|
|
195
|
+
// async (`Promise<T>` return types) — see PublicWebCapability,
|
|
196
|
+
// IdpCapability, etc. Sync capability methods would be a type-system
|
|
197
|
+
// lie that the existing interfaces don't allow.
|
|
198
|
+
const { logger } = createCapturingLogger();
|
|
199
|
+
|
|
200
|
+
const original = {
|
|
201
|
+
_state: 'initial',
|
|
202
|
+
async setState(this: { _state: string }, next: string): Promise<void> {
|
|
203
|
+
this._state = next;
|
|
204
|
+
},
|
|
205
|
+
async getState(this: { _state: string }): Promise<string> {
|
|
206
|
+
return this._state;
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const wrapped = wrapWithLogging(original, logger, 'cap');
|
|
211
|
+
|
|
212
|
+
await wrapped.setState('updated');
|
|
213
|
+
// The wrapped object shares state with the original via `this`
|
|
214
|
+
// because both methods operate on the same wrapped instance.
|
|
215
|
+
expect(await wrapped.getState()).toBe('updated');
|
|
216
|
+
});
|
|
217
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for zone-scoped capability lookup
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
6
|
+
import type { DbClient } from '../db/client';
|
|
7
|
+
import { cleanupTestDatabase, setupTestDatabase } from '../test-utils/database';
|
|
8
|
+
import { findAllCapabilityProviders, findCapabilityProvider } from './lookup';
|
|
9
|
+
|
|
10
|
+
describe('Zone-Scoped Capability Lookup', () => {
|
|
11
|
+
let db: DbClient;
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
db = await setupTestDatabase();
|
|
15
|
+
|
|
16
|
+
// Insert test modules
|
|
17
|
+
db.$client.run(
|
|
18
|
+
`INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('greenwave', 'GreenWave', '1.0.0', '/path', '{}')`,
|
|
19
|
+
);
|
|
20
|
+
db.$client.run(
|
|
21
|
+
`INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('iptables', 'iptables', '1.0.0', '/path', '{}')`,
|
|
22
|
+
);
|
|
23
|
+
db.$client.run(
|
|
24
|
+
`INSERT INTO modules (id, name, version, source_path, manifest_data) VALUES ('gmail', 'Gmail', '1.0.0', '/path', '{}')`,
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
await cleanupTestDatabase(db);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('findCapabilityProvider', () => {
|
|
33
|
+
test('returns null when no providers exist', () => {
|
|
34
|
+
const result = findCapabilityProvider('firewall', db);
|
|
35
|
+
expect(result).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('returns zone-agnostic provider without zone filter', () => {
|
|
39
|
+
db.$client.run(
|
|
40
|
+
`INSERT INTO capabilities (module_id, capability_name, version, data, zones) VALUES ('gmail', 'email_reader', '1.0.0', '{}', NULL)`,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const result = findCapabilityProvider('email_reader', db);
|
|
44
|
+
expect(result).not.toBeNull();
|
|
45
|
+
expect(result?.moduleId).toBe('gmail');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('returns zone-agnostic provider when zone is specified but no zone-scoped match', () => {
|
|
49
|
+
db.$client.run(
|
|
50
|
+
`INSERT INTO capabilities (module_id, capability_name, version, data, zones) VALUES ('gmail', 'email_reader', '1.0.0', '{}', NULL)`,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const result = findCapabilityProvider('email_reader', db, 'dmz');
|
|
54
|
+
expect(result).not.toBeNull();
|
|
55
|
+
expect(result?.moduleId).toBe('gmail');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('returns zone-scoped provider when zone matches', () => {
|
|
59
|
+
db.$client.run(
|
|
60
|
+
`INSERT INTO capabilities (module_id, capability_name, version, data, zones) VALUES ('iptables', 'firewall', '1.0.0', '{}', '["dmz","app","secure"]')`,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const result = findCapabilityProvider('firewall', db, 'dmz');
|
|
64
|
+
expect(result).not.toBeNull();
|
|
65
|
+
expect(result?.moduleId).toBe('iptables');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('returns null when zone does not match any provider', () => {
|
|
69
|
+
db.$client.run(
|
|
70
|
+
`INSERT INTO capabilities (module_id, capability_name, version, data, zones) VALUES ('iptables', 'firewall', '1.0.0', '{}', '["dmz"]')`,
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
const result = findCapabilityProvider('firewall', db, 'external');
|
|
74
|
+
expect(result).toBeNull();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('selects correct provider from multiple zone-scoped providers', () => {
|
|
78
|
+
db.$client.run(
|
|
79
|
+
`INSERT INTO capabilities (module_id, capability_name, version, data, zones) VALUES ('greenwave', 'firewall', '1.0.0', '{"has_external":true}', '["internal"]')`,
|
|
80
|
+
);
|
|
81
|
+
db.$client.run(
|
|
82
|
+
`INSERT INTO capabilities (module_id, capability_name, version, data, zones) VALUES ('iptables', 'firewall', '1.0.0', '{}', '["dmz","app","secure"]')`,
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const dmzResult = findCapabilityProvider('firewall', db, 'dmz');
|
|
86
|
+
expect(dmzResult?.moduleId).toBe('iptables');
|
|
87
|
+
|
|
88
|
+
const internalResult = findCapabilityProvider('firewall', db, 'internal');
|
|
89
|
+
expect(internalResult?.moduleId).toBe('greenwave');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('returns first provider when no zone specified and multiple exist', () => {
|
|
93
|
+
db.$client.run(
|
|
94
|
+
`INSERT INTO capabilities (module_id, capability_name, version, data, zones) VALUES ('greenwave', 'firewall', '1.0.0', '{}', '["internal"]')`,
|
|
95
|
+
);
|
|
96
|
+
db.$client.run(
|
|
97
|
+
`INSERT INTO capabilities (module_id, capability_name, version, data, zones) VALUES ('iptables', 'firewall', '1.0.0', '{}', '["dmz"]')`,
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const result = findCapabilityProvider('firewall', db);
|
|
101
|
+
expect(result).not.toBeNull();
|
|
102
|
+
// Returns first one found (order depends on DB insert order)
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('includes zones in returned info', () => {
|
|
106
|
+
db.$client.run(
|
|
107
|
+
`INSERT INTO capabilities (module_id, capability_name, version, data, zones) VALUES ('iptables', 'firewall', '1.0.0', '{"nat_ip":"192.168.0.253"}', '["dmz","app"]')`,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const result = findCapabilityProvider('firewall', db, 'dmz');
|
|
111
|
+
expect(result?.zones).toEqual(['dmz', 'app']);
|
|
112
|
+
expect(result?.data).toEqual({ nat_ip: '192.168.0.253' });
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe('findAllCapabilityProviders', () => {
|
|
117
|
+
test('returns empty array when no providers exist', () => {
|
|
118
|
+
const results = findAllCapabilityProviders('firewall', db);
|
|
119
|
+
expect(results).toEqual([]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('returns all providers of a capability', () => {
|
|
123
|
+
db.$client.run(
|
|
124
|
+
`INSERT INTO capabilities (module_id, capability_name, version, data, zones) VALUES ('greenwave', 'firewall', '1.0.0', '{}', '["internal"]')`,
|
|
125
|
+
);
|
|
126
|
+
db.$client.run(
|
|
127
|
+
`INSERT INTO capabilities (module_id, capability_name, version, data, zones) VALUES ('iptables', 'firewall', '1.0.0', '{}', '["dmz"]')`,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const results = findAllCapabilityProviders('firewall', db);
|
|
131
|
+
expect(results).toHaveLength(2);
|
|
132
|
+
const moduleIds = results.map((r) => r.moduleId).sort();
|
|
133
|
+
expect(moduleIds).toEqual(['greenwave', 'iptables']);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('does not return providers of different capabilities', () => {
|
|
137
|
+
db.$client.run(
|
|
138
|
+
`INSERT INTO capabilities (module_id, capability_name, version, data, zones) VALUES ('greenwave', 'firewall', '1.0.0', '{}', '["internal"]')`,
|
|
139
|
+
);
|
|
140
|
+
db.$client.run(
|
|
141
|
+
`INSERT INTO capabilities (module_id, capability_name, version, data, zones) VALUES ('gmail', 'email_reader', '1.0.0', '{}', NULL)`,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const firewalls = findAllCapabilityProviders('firewall', db);
|
|
145
|
+
expect(firewalls).toHaveLength(1);
|
|
146
|
+
expect(firewalls[0].moduleId).toBe('greenwave');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability Lookup
|
|
3
|
+
*
|
|
4
|
+
* Zone-aware capability provider lookup. Supports both zone-scoped
|
|
5
|
+
* capabilities (like firewall) and zone-agnostic ones (like dns_registrar).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { eq } from 'drizzle-orm';
|
|
9
|
+
import type { DbClient } from '../db/client';
|
|
10
|
+
import { capabilities } from '../db/schema';
|
|
11
|
+
|
|
12
|
+
export interface CapabilityProviderInfo {
|
|
13
|
+
id: number;
|
|
14
|
+
moduleId: string;
|
|
15
|
+
capabilityName: string;
|
|
16
|
+
version: string;
|
|
17
|
+
data: Record<string, unknown>;
|
|
18
|
+
zones: string[] | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Find a capability provider, optionally filtered by zone.
|
|
23
|
+
*
|
|
24
|
+
* - If zone is provided: prefer providers whose zones include that zone.
|
|
25
|
+
* Falls back to zone-agnostic providers (zones is null).
|
|
26
|
+
* - If zone is not provided: return the first matching provider.
|
|
27
|
+
*
|
|
28
|
+
* @param name - Capability name (e.g., "firewall", "dns_registrar")
|
|
29
|
+
* @param zone - Optional zone filter (e.g., "dmz", "internal")
|
|
30
|
+
* @param db - Database client
|
|
31
|
+
*/
|
|
32
|
+
export function findCapabilityProvider(
|
|
33
|
+
name: string,
|
|
34
|
+
db: DbClient,
|
|
35
|
+
zone?: string,
|
|
36
|
+
): CapabilityProviderInfo | null {
|
|
37
|
+
const all = db.select().from(capabilities).where(eq(capabilities.capabilityName, name)).all();
|
|
38
|
+
|
|
39
|
+
if (all.length === 0) return null;
|
|
40
|
+
|
|
41
|
+
if (zone) {
|
|
42
|
+
// First: try to find a provider that explicitly covers this zone
|
|
43
|
+
const zoneMatch = all.find((c) => {
|
|
44
|
+
const zones = c.zones as string[] | null;
|
|
45
|
+
return zones?.includes(zone);
|
|
46
|
+
});
|
|
47
|
+
if (zoneMatch) return toInfo(zoneMatch);
|
|
48
|
+
|
|
49
|
+
// Second: fall back to zone-agnostic provider (zones is null)
|
|
50
|
+
const agnostic = all.find((c) => c.zones === null || c.zones === undefined);
|
|
51
|
+
if (agnostic) return toInfo(agnostic);
|
|
52
|
+
|
|
53
|
+
// No match for this zone
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// No zone specified: return first provider
|
|
58
|
+
return toInfo(all[0]);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Find all providers of a capability (across all zones)
|
|
63
|
+
*/
|
|
64
|
+
export function findAllCapabilityProviders(name: string, db: DbClient): CapabilityProviderInfo[] {
|
|
65
|
+
return db
|
|
66
|
+
.select()
|
|
67
|
+
.from(capabilities)
|
|
68
|
+
.where(eq(capabilities.capabilityName, name))
|
|
69
|
+
.all()
|
|
70
|
+
.map(toInfo);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function toInfo(row: {
|
|
74
|
+
id: number;
|
|
75
|
+
moduleId: string;
|
|
76
|
+
capabilityName: string;
|
|
77
|
+
version: string;
|
|
78
|
+
data: Record<string, unknown>;
|
|
79
|
+
zones: string[] | null;
|
|
80
|
+
}): CapabilityProviderInfo {
|
|
81
|
+
return {
|
|
82
|
+
id: row.id,
|
|
83
|
+
moduleId: row.moduleId,
|
|
84
|
+
capabilityName: row.capabilityName,
|
|
85
|
+
version: row.version,
|
|
86
|
+
data: row.data as Record<string, unknown>,
|
|
87
|
+
zones: row.zones as string[] | null,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the pure helpers exported alongside the public_web
|
|
3
|
+
* capability factory: client-config rendering and request validation
|
|
4
|
+
* for the higher-level operations introduced in HOOK_API_V2 Phase 4
|
|
5
|
+
* (D4).
|
|
6
|
+
*
|
|
7
|
+
* The factory itself opens SSH connections and shells out to tar, so
|
|
8
|
+
* it's not unit-testable in isolation. These helpers are the seam
|
|
9
|
+
* where the interesting logic lives.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, test } from 'bun:test';
|
|
13
|
+
import {
|
|
14
|
+
type PublishStaticSiteRequest,
|
|
15
|
+
type RegisterReverseProxyRequest,
|
|
16
|
+
generateClientConfigJs,
|
|
17
|
+
validatePublishStaticSiteRequest,
|
|
18
|
+
validateRegisterReverseProxyRequest,
|
|
19
|
+
} from '@celilo/capabilities';
|
|
20
|
+
|
|
21
|
+
describe('generateClientConfigJs', () => {
|
|
22
|
+
test('renders an empty object as a literal {}', () => {
|
|
23
|
+
const out = generateClientConfigJs({});
|
|
24
|
+
expect(out).toContain('window.__MODULE_CONFIG__ = {};');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('emits the do-not-edit banner', () => {
|
|
28
|
+
const out = generateClientConfigJs({});
|
|
29
|
+
expect(out.split('\n')[0]).toContain('Generated by Celilo');
|
|
30
|
+
expect(out.split('\n')[0]).toContain('do not edit');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('renders a single string field', () => {
|
|
34
|
+
const out = generateClientConfigJs({ CLIENT_ID: 'lunacycle-web' });
|
|
35
|
+
expect(out).toContain('"CLIENT_ID":"lunacycle-web"');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('renders multiple fields in one object', () => {
|
|
39
|
+
const out = generateClientConfigJs({
|
|
40
|
+
AUTHENTIK_URL: 'https://auth.example.com',
|
|
41
|
+
CLIENT_ID: 'lunacycle-web',
|
|
42
|
+
});
|
|
43
|
+
expect(out).toContain('"AUTHENTIK_URL":"https://auth.example.com"');
|
|
44
|
+
expect(out).toContain('"CLIENT_ID":"lunacycle-web"');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('round-trips numbers as JSON numbers (not strings)', () => {
|
|
48
|
+
const out = generateClientConfigJs({ TIMEOUT_MS: 5000, RETRIES: 3 });
|
|
49
|
+
expect(out).toContain('"TIMEOUT_MS":5000');
|
|
50
|
+
expect(out).toContain('"RETRIES":3');
|
|
51
|
+
expect(out).not.toContain('"5000"');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('round-trips booleans as JSON booleans', () => {
|
|
55
|
+
const out = generateClientConfigJs({ DEBUG: true, PROD: false });
|
|
56
|
+
expect(out).toContain('"DEBUG":true');
|
|
57
|
+
expect(out).toContain('"PROD":false');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('escapes double quotes inside string values', () => {
|
|
61
|
+
const out = generateClientConfigJs({ MOTD: 'hello "world"' });
|
|
62
|
+
expect(out).toContain('"MOTD":"hello \\"world\\""');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('escapes backslashes inside string values', () => {
|
|
66
|
+
const out = generateClientConfigJs({ PATH: 'C:\\Users\\test' });
|
|
67
|
+
// JSON escapes each backslash, so the output has 2x doubled backslashes.
|
|
68
|
+
expect(out).toContain('"PATH":"C:\\\\Users\\\\test"');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('escapes newlines and other control characters', () => {
|
|
72
|
+
const out = generateClientConfigJs({ MULTI: 'a\nb\tc' });
|
|
73
|
+
expect(out).toContain('"MULTI":"a\\nb\\tc"');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('output is parseable as JavaScript (banner + assignment)', () => {
|
|
77
|
+
const out = generateClientConfigJs({ FOO: 'bar', N: 42 });
|
|
78
|
+
// The trailing newline keeps the file POSIX-clean.
|
|
79
|
+
expect(out.endsWith('\n')).toBe(true);
|
|
80
|
+
// Two non-empty lines: banner + assignment.
|
|
81
|
+
const lines = out.trim().split('\n');
|
|
82
|
+
expect(lines).toHaveLength(2);
|
|
83
|
+
expect(lines[1]).toMatch(/^window\.__MODULE_CONFIG__ = \{.*\};$/);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
function basePublishRequest(
|
|
88
|
+
overrides: Partial<PublishStaticSiteRequest> = {},
|
|
89
|
+
): PublishStaticSiteRequest {
|
|
90
|
+
return {
|
|
91
|
+
path: '/lunacycle',
|
|
92
|
+
sourceDir: '/tmp/build/dist',
|
|
93
|
+
...overrides,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
describe('validatePublishStaticSiteRequest', () => {
|
|
98
|
+
test('accepts a complete valid request', () => {
|
|
99
|
+
const result = validatePublishStaticSiteRequest(basePublishRequest());
|
|
100
|
+
expect(result.valid).toBe(true);
|
|
101
|
+
expect(result.errors).toEqual([]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('accepts a request with optional clientConfig and subdomain', () => {
|
|
105
|
+
const result = validatePublishStaticSiteRequest(
|
|
106
|
+
basePublishRequest({
|
|
107
|
+
clientConfig: { FOO: 'bar' },
|
|
108
|
+
subdomain: 'app',
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
expect(result.valid).toBe(true);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('rejects empty path', () => {
|
|
115
|
+
const result = validatePublishStaticSiteRequest(basePublishRequest({ path: '' }));
|
|
116
|
+
expect(result.valid).toBe(false);
|
|
117
|
+
expect(result.errors).toContain('Path is required');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('rejects path that does not start with /', () => {
|
|
121
|
+
const result = validatePublishStaticSiteRequest(basePublishRequest({ path: 'lunacycle' }));
|
|
122
|
+
expect(result.valid).toBe(false);
|
|
123
|
+
expect(result.errors.some((e) => e.includes('must start with /'))).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test('rejects path with trailing slash', () => {
|
|
127
|
+
const result = validatePublishStaticSiteRequest(basePublishRequest({ path: '/lunacycle/' }));
|
|
128
|
+
expect(result.valid).toBe(false);
|
|
129
|
+
expect(result.errors.some((e) => e.includes('trailing slash'))).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('accepts root path "/"', () => {
|
|
133
|
+
const result = validatePublishStaticSiteRequest(basePublishRequest({ path: '/' }));
|
|
134
|
+
expect(result.valid).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('rejects empty sourceDir', () => {
|
|
138
|
+
const result = validatePublishStaticSiteRequest(basePublishRequest({ sourceDir: '' }));
|
|
139
|
+
expect(result.valid).toBe(false);
|
|
140
|
+
expect(result.errors).toContain('sourceDir is required');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('reports multiple errors at once', () => {
|
|
144
|
+
const result = validatePublishStaticSiteRequest({
|
|
145
|
+
path: '',
|
|
146
|
+
sourceDir: '',
|
|
147
|
+
});
|
|
148
|
+
expect(result.valid).toBe(false);
|
|
149
|
+
expect(result.errors.length).toBeGreaterThanOrEqual(2);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
function baseProxyRequest(
|
|
154
|
+
overrides: Partial<RegisterReverseProxyRequest> = {},
|
|
155
|
+
): RegisterReverseProxyRequest {
|
|
156
|
+
return {
|
|
157
|
+
path: '/lunacycle/api',
|
|
158
|
+
targetHost: '10.0.20.42',
|
|
159
|
+
targetPort: 8080,
|
|
160
|
+
...overrides,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
describe('validateRegisterReverseProxyRequest', () => {
|
|
165
|
+
test('accepts a complete valid request', () => {
|
|
166
|
+
const result = validateRegisterReverseProxyRequest(baseProxyRequest());
|
|
167
|
+
expect(result.valid).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('accepts websocket flag', () => {
|
|
171
|
+
const result = validateRegisterReverseProxyRequest(baseProxyRequest({ websocket: true }));
|
|
172
|
+
expect(result.valid).toBe(true);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test('rejects missing targetHost', () => {
|
|
176
|
+
const result = validateRegisterReverseProxyRequest(baseProxyRequest({ targetHost: '' }));
|
|
177
|
+
expect(result.valid).toBe(false);
|
|
178
|
+
expect(result.errors).toContain('targetHost is required');
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('rejects port 0', () => {
|
|
182
|
+
const result = validateRegisterReverseProxyRequest(baseProxyRequest({ targetPort: 0 }));
|
|
183
|
+
expect(result.valid).toBe(false);
|
|
184
|
+
expect(result.errors.some((e) => e.includes('between 1 and 65535'))).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test('rejects port above 65535', () => {
|
|
188
|
+
const result = validateRegisterReverseProxyRequest(baseProxyRequest({ targetPort: 70000 }));
|
|
189
|
+
expect(result.valid).toBe(false);
|
|
190
|
+
expect(result.errors.some((e) => e.includes('between 1 and 65535'))).toBe(true);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('rejects missing path', () => {
|
|
194
|
+
const result = validateRegisterReverseProxyRequest(baseProxyRequest({ path: '' }));
|
|
195
|
+
expect(result.valid).toBe(false);
|
|
196
|
+
expect(result.errors).toContain('Path is required');
|
|
197
|
+
});
|
|
198
|
+
});
|