@serfab/cadre-host 0.10.0 → 0.12.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 +406 -296
- package/dist/bin/host.d.ts +11 -7
- package/dist/bin/host.js +40 -12
- package/dist/bin/host.js.map +1 -1
- package/dist/installer/config.d.ts +4 -4
- package/dist/installer/config.js +5 -39
- package/dist/installer/config.js.map +1 -1
- package/dist/installer/identity.d.ts +8 -1
- package/dist/installer/identity.js +8 -1
- package/dist/installer/identity.js.map +1 -1
- package/dist/orchestrator/host-process-orchestrator.d.ts +2 -2
- package/dist/orchestrator/host-process-orchestrator.js +9 -4
- package/dist/orchestrator/host-process-orchestrator.js.map +1 -1
- package/dist/server/settings-store.d.ts +1 -1
- package/dist/server/settings-store.js +5 -1
- package/dist/server/settings-store.js.map +1 -1
- package/dist/server/static.js +38 -38
- package/dist/ui/assets/index-C-iks41M.js.map +1 -1
- package/dist/ui/index.html +12 -12
- package/package.json +4 -4
- package/service/cadre-host.service.tmpl +16 -16
- package/service/com.serfab.cadre-host.plist.tmpl +35 -35
- package/service/install-service.ps1 +98 -98
- package/service/uninstall-service.ps1 +19 -19
- package/src/__tests__/build-targets.test.ts +26 -26
- package/src/__tests__/cli-invite.smoke.test.ts +192 -192
- package/src/__tests__/cli-nat.smoke.test.ts +212 -212
- package/src/__tests__/cli.smoke.test.ts +51 -46
- package/src/__tests__/global-setup.ts +56 -51
- package/src/__tests__/host-owner.smoke.test.ts +123 -123
- package/src/__tests__/orchestrator-env-scrub.test.ts +140 -140
- package/src/__tests__/orchestrator-node-identity.test.ts +5 -5
- package/src/__tests__/orchestrator-owner.test.ts +198 -198
- package/src/__tests__/orchestrator-pin-keys.test.ts +153 -153
- package/src/__tests__/orchestrator-push.test.ts +145 -145
- package/src/__tests__/orchestrator.test.ts +951 -951
- package/src/auth/__tests__/duration.test.ts +28 -28
- package/src/auth/__tests__/self-label.test.ts +85 -85
- package/src/auth/__tests__/trust-circle-store.test.ts +115 -115
- package/src/auth/__tests__/trust-circle.test.ts +445 -445
- package/src/auth/duration.ts +33 -33
- package/src/auth/index.ts +21 -21
- package/src/auth/self-label.ts +74 -74
- package/src/auth/trust-circle-store.ts +155 -155
- package/src/auth/types.ts +99 -99
- package/src/bin/__tests__/cli-reference.test.ts +167 -0
- package/src/bin/host.ts +1326 -1299
- package/src/donation/__tests__/donation-service.test.ts +1068 -1068
- package/src/donation/__tests__/donation-supervisor.test.ts +510 -510
- package/src/donation/__tests__/fake-orchestrator.test.ts +168 -168
- package/src/donation/__tests__/fake-orchestrator.ts +225 -225
- package/src/donation/__tests__/grant-service.test.ts +213 -213
- package/src/donation/__tests__/grant-store.test.ts +113 -113
- package/src/donation/donation-service.ts +962 -962
- package/src/donation/donation-store.ts +171 -171
- package/src/donation/donation-supervisor.ts +388 -388
- package/src/donation/grant-service.ts +200 -200
- package/src/donation/grant-store.ts +152 -152
- package/src/donation/index.ts +64 -64
- package/src/donation/types.ts +243 -243
- package/src/installer/__tests__/config.test.ts +134 -112
- package/src/installer/__tests__/identity.test.ts +34 -34
- package/src/installer/__tests__/paths.test.ts +47 -47
- package/src/installer/__tests__/service-host.test.ts +62 -62
- package/src/installer/__tests__/wizard.test.ts +64 -64
- package/src/installer/config.ts +5 -43
- package/src/installer/identity.ts +56 -49
- package/src/installer/paths.ts +109 -109
- package/src/installer/platform.ts +27 -27
- package/src/installer/service-host/index.ts +21 -21
- package/src/installer/service-host/template.ts +21 -21
- package/src/installer/wizard.ts +103 -103
- package/src/nat/__tests__/address-resolver.test.ts +82 -82
- package/src/nat/__tests__/ddns-duckdns.test.ts +83 -83
- package/src/nat/__tests__/ddns-updater.test.ts +253 -253
- package/src/nat/__tests__/external-ip.test.ts +124 -124
- package/src/nat/__tests__/nat-service.test.ts +18 -0
- package/src/nat/__tests__/nat-store.test.ts +93 -93
- package/src/nat/__tests__/port-mapper.test.ts +191 -191
- package/src/nat/__tests__/reachability.test.ts +24 -24
- package/src/nat/__tests__/secrets.test.ts +178 -178
- package/src/nat/address-resolver.ts +51 -51
- package/src/nat/ddns/duckdns.ts +76 -76
- package/src/nat/ddns/index.ts +55 -55
- package/src/nat/ddns/updater.ts +243 -243
- package/src/nat/external-ip.ts +154 -154
- package/src/nat/index.ts +71 -71
- package/src/nat/nat-service.ts +501 -501
- package/src/nat/nat-store.ts +154 -154
- package/src/nat/port-mapper.ts +329 -329
- package/src/nat/reachability.ts +34 -34
- package/src/nat/secrets/file-store.ts +130 -130
- package/src/nat/secrets/index.ts +112 -112
- package/src/nat/secrets/keytar-store.ts +67 -67
- package/src/nat/types.ts +159 -159
- package/src/orchestrator/__tests__/node-identity.test.ts +62 -62
- package/src/orchestrator/host-process-orchestrator.ts +9 -4
- package/src/orchestrator/index.ts +10 -10
- package/src/orchestrator/log-rotator.ts +53 -53
- package/src/orchestrator/node-identity.ts +40 -40
- package/src/orchestrator/pid-liveness.ts +24 -24
- package/src/orchestrator/port-allocator.ts +91 -91
- package/src/orchestrator/state-store.ts +90 -90
- package/src/orchestrator/types.ts +141 -141
- package/src/owner/__tests__/owner-node-client.test.ts +194 -194
- package/src/owner/index.ts +12 -12
- package/src/owner/owner-node-client.ts +259 -259
- package/src/push/__tests__/push.test.ts +90 -90
- package/src/push/index.ts +146 -146
- package/src/server/__tests__/bus.test.ts +38 -38
- package/src/server/__tests__/error-handler.test.ts +118 -118
- package/src/server/__tests__/grants-admin-route.test.ts +73 -73
- package/src/server/__tests__/grants-route.test.ts +263 -263
- package/src/server/__tests__/nodes-route.test.ts +259 -259
- package/src/server/__tests__/origin-guard.test.ts +72 -72
- package/src/server/__tests__/publishers.test.ts +216 -216
- package/src/server/__tests__/server.smoke.test.ts +253 -253
- package/src/server/__tests__/settings-route.test.ts +195 -194
- package/src/server/__tests__/sse-route.test.ts +119 -119
- package/src/server/__tests__/static.test.ts +145 -145
- package/src/server/__tests__/status-route.test.ts +134 -134
- package/src/server/__tests__/strands-route.test.ts +233 -233
- package/src/server/error-handler.ts +178 -178
- package/src/server/events/bus.ts +42 -42
- package/src/server/events/sse-route.ts +81 -81
- package/src/server/events/types.ts +23 -23
- package/src/server/index.ts +267 -267
- package/src/server/origin-guard.ts +84 -84
- package/src/server/routes/grants-admin.ts +41 -41
- package/src/server/routes/grants.ts +210 -210
- package/src/server/routes/nat.ts +63 -63
- package/src/server/routes/nodes.ts +170 -170
- package/src/server/routes/settings.ts +123 -123
- package/src/server/routes/status.ts +118 -118
- package/src/server/routes/strands.ts +54 -54
- package/src/server/routes/trust-circle.ts +49 -49
- package/src/server/routes/update.ts +43 -43
- package/src/server/server.ts +122 -122
- package/src/server/settings-store.ts +5 -1
- package/src/server/static.ts +207 -207
- package/src/strands/__tests__/strand-service.test.ts +165 -165
- package/src/strands/index.ts +14 -14
- package/src/strands/strand-service.ts +135 -135
- package/src/strands/types.ts +102 -102
- package/src/update/__tests__/apply.test.ts +158 -158
- package/src/update/__tests__/manifest.test.ts +128 -128
- package/src/update/__tests__/release-key.test.ts +42 -42
- package/src/update/__tests__/sign.test.ts +78 -78
- package/src/update/__tests__/store.test.ts +81 -81
- package/src/update/__tests__/update-service.integration.test.ts +256 -256
- package/src/update/__tests__/version.test.ts +31 -31
- package/src/update/apply.ts +190 -190
- package/src/update/index.ts +341 -341
- package/src/update/release-key.ts +71 -71
- package/src/update/sign.ts +74 -74
- package/src/update/store.ts +101 -101
- package/src/update/types.ts +109 -109
- package/src/update/version.ts +80 -80
|
@@ -1,118 +1,118 @@
|
|
|
1
|
-
import Fastify, { type FastifyRequest } from 'fastify';
|
|
2
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
3
|
-
|
|
4
|
-
import { TrustCircleError } from '../../auth/types.js';
|
|
5
|
-
import { NatError } from '../../nat/types.js';
|
|
6
|
-
import { UpdateErrorException } from '../../update/types.js';
|
|
7
|
-
import { GrantError } from '../../donation/types.js';
|
|
8
|
-
import { registerErrorHandler } from '../error-handler.js';
|
|
9
|
-
|
|
10
|
-
describe('error handler', () => {
|
|
11
|
-
let app: ReturnType<typeof Fastify>;
|
|
12
|
-
|
|
13
|
-
beforeEach(async () => {
|
|
14
|
-
app = Fastify();
|
|
15
|
-
registerErrorHandler(app);
|
|
16
|
-
app.get('/trust/:code', async (req: FastifyRequest) => {
|
|
17
|
-
const { code } = req.params as { code: string };
|
|
18
|
-
throw new TrustCircleError(code as never, `trust err: ${code}`);
|
|
19
|
-
});
|
|
20
|
-
app.get('/nat/:code', async (req: FastifyRequest) => {
|
|
21
|
-
const { code } = req.params as { code: string };
|
|
22
|
-
throw new NatError(code as never, `nat err: ${code}`);
|
|
23
|
-
});
|
|
24
|
-
app.get('/update/:code', async (req: FastifyRequest) => {
|
|
25
|
-
const { code } = req.params as { code: string };
|
|
26
|
-
throw new UpdateErrorException(code as never, `update err: ${code}`);
|
|
27
|
-
});
|
|
28
|
-
app.get('/grant/:code', async (req: FastifyRequest) => {
|
|
29
|
-
const { code } = req.params as { code: string };
|
|
30
|
-
throw new GrantError(code as never, `grant err: ${code}`);
|
|
31
|
-
});
|
|
32
|
-
app.get('/unknown', async () => {
|
|
33
|
-
throw new Error('mystery');
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
afterEach(async () => {
|
|
38
|
-
await app.close();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('TrustCircleError invalid_label → 400', async () => {
|
|
42
|
-
const res = await app.inject({ method: 'GET', url: '/trust/invalid_label' });
|
|
43
|
-
expect(res.statusCode).toBe(400);
|
|
44
|
-
const body = res.json() as { ok: boolean; error: { code: string; message: string } };
|
|
45
|
-
expect(body).toEqual({
|
|
46
|
-
ok: false,
|
|
47
|
-
error: { code: 'invalid_label', message: 'trust err: invalid_label' },
|
|
48
|
-
});
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('TrustCircleError not_found → 404', async () => {
|
|
52
|
-
const res = await app.inject({ method: 'GET', url: '/trust/not_found' });
|
|
53
|
-
expect(res.statusCode).toBe(404);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('TrustCircleError already_redeemed → 404', async () => {
|
|
57
|
-
const res = await app.inject({ method: 'GET', url: '/trust/already_redeemed' });
|
|
58
|
-
expect(res.statusCode).toBe(404);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('TrustCircleError expired → 410', async () => {
|
|
62
|
-
const res = await app.inject({ method: 'GET', url: '/trust/expired' });
|
|
63
|
-
expect(res.statusCode).toBe(410);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('TrustCircleError storage_error → 500', async () => {
|
|
67
|
-
const res = await app.inject({ method: 'GET', url: '/trust/storage_error' });
|
|
68
|
-
expect(res.statusCode).toBe(500);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('NatError invalid_config → 400', async () => {
|
|
72
|
-
const res = await app.inject({ method: 'GET', url: '/nat/invalid_config' });
|
|
73
|
-
expect(res.statusCode).toBe(400);
|
|
74
|
-
const body = res.json() as { error: { code: string } };
|
|
75
|
-
expect(body.error.code).toBe('invalid_config');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('NatError secrets_unavailable → 500', async () => {
|
|
79
|
-
const res = await app.inject({ method: 'GET', url: '/nat/secrets_unavailable' });
|
|
80
|
-
expect(res.statusCode).toBe(500);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('UpdateErrorException apply_in_progress → 409', async () => {
|
|
84
|
-
const res = await app.inject({ method: 'GET', url: '/update/apply_in_progress' });
|
|
85
|
-
expect(res.statusCode).toBe(409);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('UpdateErrorException no_update_available → 400', async () => {
|
|
89
|
-
const res = await app.inject({ method: 'GET', url: '/update/no_update_available' });
|
|
90
|
-
expect(res.statusCode).toBe(400);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('GrantError invalid_max_nodes → 400', async () => {
|
|
94
|
-
const res = await app.inject({ method: 'GET', url: '/grant/invalid_max_nodes' });
|
|
95
|
-
expect(res.statusCode).toBe(400);
|
|
96
|
-
const body = res.json() as { error: { code: string } };
|
|
97
|
-
expect(body.error.code).toBe('invalid_max_nodes');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('GrantError not_found → 404', async () => {
|
|
101
|
-
const res = await app.inject({ method: 'GET', url: '/grant/not_found' });
|
|
102
|
-
expect(res.statusCode).toBe(404);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('GrantError storage_error → 500', async () => {
|
|
106
|
-
const res = await app.inject({ method: 'GET', url: '/grant/storage_error' });
|
|
107
|
-
expect(res.statusCode).toBe(500);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('unknown Error → 500 internal', async () => {
|
|
111
|
-
const res = await app.inject({ method: 'GET', url: '/unknown' });
|
|
112
|
-
expect(res.statusCode).toBe(500);
|
|
113
|
-
const body = res.json() as { ok: boolean; error: { code: string; message: string } };
|
|
114
|
-
expect(body.ok).toBe(false);
|
|
115
|
-
expect(body.error.code).toBe('internal');
|
|
116
|
-
expect(body.error.message).toBe('mystery');
|
|
117
|
-
});
|
|
118
|
-
});
|
|
1
|
+
import Fastify, { type FastifyRequest } from 'fastify';
|
|
2
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { TrustCircleError } from '../../auth/types.js';
|
|
5
|
+
import { NatError } from '../../nat/types.js';
|
|
6
|
+
import { UpdateErrorException } from '../../update/types.js';
|
|
7
|
+
import { GrantError } from '../../donation/types.js';
|
|
8
|
+
import { registerErrorHandler } from '../error-handler.js';
|
|
9
|
+
|
|
10
|
+
describe('error handler', () => {
|
|
11
|
+
let app: ReturnType<typeof Fastify>;
|
|
12
|
+
|
|
13
|
+
beforeEach(async () => {
|
|
14
|
+
app = Fastify();
|
|
15
|
+
registerErrorHandler(app);
|
|
16
|
+
app.get('/trust/:code', async (req: FastifyRequest) => {
|
|
17
|
+
const { code } = req.params as { code: string };
|
|
18
|
+
throw new TrustCircleError(code as never, `trust err: ${code}`);
|
|
19
|
+
});
|
|
20
|
+
app.get('/nat/:code', async (req: FastifyRequest) => {
|
|
21
|
+
const { code } = req.params as { code: string };
|
|
22
|
+
throw new NatError(code as never, `nat err: ${code}`);
|
|
23
|
+
});
|
|
24
|
+
app.get('/update/:code', async (req: FastifyRequest) => {
|
|
25
|
+
const { code } = req.params as { code: string };
|
|
26
|
+
throw new UpdateErrorException(code as never, `update err: ${code}`);
|
|
27
|
+
});
|
|
28
|
+
app.get('/grant/:code', async (req: FastifyRequest) => {
|
|
29
|
+
const { code } = req.params as { code: string };
|
|
30
|
+
throw new GrantError(code as never, `grant err: ${code}`);
|
|
31
|
+
});
|
|
32
|
+
app.get('/unknown', async () => {
|
|
33
|
+
throw new Error('mystery');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
afterEach(async () => {
|
|
38
|
+
await app.close();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('TrustCircleError invalid_label → 400', async () => {
|
|
42
|
+
const res = await app.inject({ method: 'GET', url: '/trust/invalid_label' });
|
|
43
|
+
expect(res.statusCode).toBe(400);
|
|
44
|
+
const body = res.json() as { ok: boolean; error: { code: string; message: string } };
|
|
45
|
+
expect(body).toEqual({
|
|
46
|
+
ok: false,
|
|
47
|
+
error: { code: 'invalid_label', message: 'trust err: invalid_label' },
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('TrustCircleError not_found → 404', async () => {
|
|
52
|
+
const res = await app.inject({ method: 'GET', url: '/trust/not_found' });
|
|
53
|
+
expect(res.statusCode).toBe(404);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('TrustCircleError already_redeemed → 404', async () => {
|
|
57
|
+
const res = await app.inject({ method: 'GET', url: '/trust/already_redeemed' });
|
|
58
|
+
expect(res.statusCode).toBe(404);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('TrustCircleError expired → 410', async () => {
|
|
62
|
+
const res = await app.inject({ method: 'GET', url: '/trust/expired' });
|
|
63
|
+
expect(res.statusCode).toBe(410);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('TrustCircleError storage_error → 500', async () => {
|
|
67
|
+
const res = await app.inject({ method: 'GET', url: '/trust/storage_error' });
|
|
68
|
+
expect(res.statusCode).toBe(500);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('NatError invalid_config → 400', async () => {
|
|
72
|
+
const res = await app.inject({ method: 'GET', url: '/nat/invalid_config' });
|
|
73
|
+
expect(res.statusCode).toBe(400);
|
|
74
|
+
const body = res.json() as { error: { code: string } };
|
|
75
|
+
expect(body.error.code).toBe('invalid_config');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('NatError secrets_unavailable → 500', async () => {
|
|
79
|
+
const res = await app.inject({ method: 'GET', url: '/nat/secrets_unavailable' });
|
|
80
|
+
expect(res.statusCode).toBe(500);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('UpdateErrorException apply_in_progress → 409', async () => {
|
|
84
|
+
const res = await app.inject({ method: 'GET', url: '/update/apply_in_progress' });
|
|
85
|
+
expect(res.statusCode).toBe(409);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('UpdateErrorException no_update_available → 400', async () => {
|
|
89
|
+
const res = await app.inject({ method: 'GET', url: '/update/no_update_available' });
|
|
90
|
+
expect(res.statusCode).toBe(400);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('GrantError invalid_max_nodes → 400', async () => {
|
|
94
|
+
const res = await app.inject({ method: 'GET', url: '/grant/invalid_max_nodes' });
|
|
95
|
+
expect(res.statusCode).toBe(400);
|
|
96
|
+
const body = res.json() as { error: { code: string } };
|
|
97
|
+
expect(body.error.code).toBe('invalid_max_nodes');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('GrantError not_found → 404', async () => {
|
|
101
|
+
const res = await app.inject({ method: 'GET', url: '/grant/not_found' });
|
|
102
|
+
expect(res.statusCode).toBe(404);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('GrantError storage_error → 500', async () => {
|
|
106
|
+
const res = await app.inject({ method: 'GET', url: '/grant/storage_error' });
|
|
107
|
+
expect(res.statusCode).toBe(500);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('unknown Error → 500 internal', async () => {
|
|
111
|
+
const res = await app.inject({ method: 'GET', url: '/unknown' });
|
|
112
|
+
expect(res.statusCode).toBe(500);
|
|
113
|
+
const body = res.json() as { ok: boolean; error: { code: string; message: string } };
|
|
114
|
+
expect(body.ok).toBe(false);
|
|
115
|
+
expect(body.error.code).toBe('internal');
|
|
116
|
+
expect(body.error.message).toBe('mystery');
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -1,73 +1,73 @@
|
|
|
1
|
-
import Fastify from 'fastify';
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
-
import { mkdtempSync, rmSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { join } from 'node:path';
|
|
6
|
-
|
|
7
|
-
import { registerErrorHandler } from '../error-handler.js';
|
|
8
|
-
import { registerGrantsAdminRoutes } from '../routes/grants-admin.js';
|
|
9
|
-
import { GrantService, GrantStore, createGrantAdminHandlers } from '../../donation/index.js';
|
|
10
|
-
import type { Grant } from '../../donation/index.js';
|
|
11
|
-
|
|
12
|
-
let tmpRoot: string;
|
|
13
|
-
let app: ReturnType<typeof Fastify>;
|
|
14
|
-
|
|
15
|
-
beforeEach(() => {
|
|
16
|
-
tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-grants-route-'));
|
|
17
|
-
const service = new GrantService({ store: new GrantStore(tmpRoot) });
|
|
18
|
-
app = Fastify();
|
|
19
|
-
registerErrorHandler(app);
|
|
20
|
-
registerGrantsAdminRoutes(app, { handlers: createGrantAdminHandlers(service) });
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
afterEach(async () => {
|
|
24
|
-
await app.close();
|
|
25
|
-
try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe('/grants-admin routes', () => {
|
|
29
|
-
it('POST issues a grant, GET lists it, DELETE revokes it', async () => {
|
|
30
|
-
const post = await app.inject({
|
|
31
|
-
method: 'POST',
|
|
32
|
-
url: '/grants-admin',
|
|
33
|
-
payload: { label: "Alice's cadre", maxNodes: 2 },
|
|
34
|
-
});
|
|
35
|
-
expect(post.statusCode).toBe(200);
|
|
36
|
-
const created = (post.json() as { grant: Grant }).grant;
|
|
37
|
-
expect(created.token).toBeTruthy();
|
|
38
|
-
expect(created.maxNodes).toBe(2);
|
|
39
|
-
|
|
40
|
-
const list = await app.inject({ method: 'GET', url: '/grants-admin' });
|
|
41
|
-
expect(list.statusCode).toBe(200);
|
|
42
|
-
expect((list.json() as { grants: Grant[] }).grants.map(g => g.token)).toContain(created.token);
|
|
43
|
-
|
|
44
|
-
const del = await app.inject({
|
|
45
|
-
method: 'DELETE',
|
|
46
|
-
url: `/grants-admin/${encodeURIComponent(created.token)}`,
|
|
47
|
-
});
|
|
48
|
-
expect(del.statusCode).toBe(200);
|
|
49
|
-
|
|
50
|
-
// Revoked → still listed, now carries revokedAt.
|
|
51
|
-
const after = await app.inject({ method: 'GET', url: '/grants-admin' });
|
|
52
|
-
const row = (after.json() as { grants: Grant[] }).grants.find(g => g.token === created.token);
|
|
53
|
-
expect(row?.revokedAt).toBeTruthy();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('POST with an empty label → 400 invalid_label', async () => {
|
|
57
|
-
const res = await app.inject({ method: 'POST', url: '/grants-admin', payload: { label: ' ' } });
|
|
58
|
-
expect(res.statusCode).toBe(400);
|
|
59
|
-
expect((res.json() as { error: { code: string } }).error.code).toBe('invalid_label');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('POST with a bad maxNodes → 400 invalid_max_nodes', async () => {
|
|
63
|
-
const res = await app.inject({ method: 'POST', url: '/grants-admin', payload: { label: 'X', maxNodes: 0 } });
|
|
64
|
-
expect(res.statusCode).toBe(400);
|
|
65
|
-
expect((res.json() as { error: { code: string } }).error.code).toBe('invalid_max_nodes');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('DELETE of an unknown token → 404 not_found', async () => {
|
|
69
|
-
const res = await app.inject({ method: 'DELETE', url: '/grants-admin/nope' });
|
|
70
|
-
expect(res.statusCode).toBe(404);
|
|
71
|
-
expect((res.json() as { error: { code: string } }).error.code).toBe('not_found');
|
|
72
|
-
});
|
|
73
|
-
});
|
|
1
|
+
import Fastify from 'fastify';
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
|
|
7
|
+
import { registerErrorHandler } from '../error-handler.js';
|
|
8
|
+
import { registerGrantsAdminRoutes } from '../routes/grants-admin.js';
|
|
9
|
+
import { GrantService, GrantStore, createGrantAdminHandlers } from '../../donation/index.js';
|
|
10
|
+
import type { Grant } from '../../donation/index.js';
|
|
11
|
+
|
|
12
|
+
let tmpRoot: string;
|
|
13
|
+
let app: ReturnType<typeof Fastify>;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tmpRoot = mkdtempSync(join(tmpdir(), 'cadre-host-grants-route-'));
|
|
17
|
+
const service = new GrantService({ store: new GrantStore(tmpRoot) });
|
|
18
|
+
app = Fastify();
|
|
19
|
+
registerErrorHandler(app);
|
|
20
|
+
registerGrantsAdminRoutes(app, { handlers: createGrantAdminHandlers(service) });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(async () => {
|
|
24
|
+
await app.close();
|
|
25
|
+
try { rmSync(tmpRoot, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe('/grants-admin routes', () => {
|
|
29
|
+
it('POST issues a grant, GET lists it, DELETE revokes it', async () => {
|
|
30
|
+
const post = await app.inject({
|
|
31
|
+
method: 'POST',
|
|
32
|
+
url: '/grants-admin',
|
|
33
|
+
payload: { label: "Alice's cadre", maxNodes: 2 },
|
|
34
|
+
});
|
|
35
|
+
expect(post.statusCode).toBe(200);
|
|
36
|
+
const created = (post.json() as { grant: Grant }).grant;
|
|
37
|
+
expect(created.token).toBeTruthy();
|
|
38
|
+
expect(created.maxNodes).toBe(2);
|
|
39
|
+
|
|
40
|
+
const list = await app.inject({ method: 'GET', url: '/grants-admin' });
|
|
41
|
+
expect(list.statusCode).toBe(200);
|
|
42
|
+
expect((list.json() as { grants: Grant[] }).grants.map(g => g.token)).toContain(created.token);
|
|
43
|
+
|
|
44
|
+
const del = await app.inject({
|
|
45
|
+
method: 'DELETE',
|
|
46
|
+
url: `/grants-admin/${encodeURIComponent(created.token)}`,
|
|
47
|
+
});
|
|
48
|
+
expect(del.statusCode).toBe(200);
|
|
49
|
+
|
|
50
|
+
// Revoked → still listed, now carries revokedAt.
|
|
51
|
+
const after = await app.inject({ method: 'GET', url: '/grants-admin' });
|
|
52
|
+
const row = (after.json() as { grants: Grant[] }).grants.find(g => g.token === created.token);
|
|
53
|
+
expect(row?.revokedAt).toBeTruthy();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('POST with an empty label → 400 invalid_label', async () => {
|
|
57
|
+
const res = await app.inject({ method: 'POST', url: '/grants-admin', payload: { label: ' ' } });
|
|
58
|
+
expect(res.statusCode).toBe(400);
|
|
59
|
+
expect((res.json() as { error: { code: string } }).error.code).toBe('invalid_label');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('POST with a bad maxNodes → 400 invalid_max_nodes', async () => {
|
|
63
|
+
const res = await app.inject({ method: 'POST', url: '/grants-admin', payload: { label: 'X', maxNodes: 0 } });
|
|
64
|
+
expect(res.statusCode).toBe(400);
|
|
65
|
+
expect((res.json() as { error: { code: string } }).error.code).toBe('invalid_max_nodes');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('DELETE of an unknown token → 404 not_found', async () => {
|
|
69
|
+
const res = await app.inject({ method: 'DELETE', url: '/grants-admin/nope' });
|
|
70
|
+
expect(res.statusCode).toBe(404);
|
|
71
|
+
expect((res.json() as { error: { code: string } }).error.code).toBe('not_found');
|
|
72
|
+
});
|
|
73
|
+
});
|