@shipfox/api-server 4.0.0 → 6.0.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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +116 -0
- package/README.md +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modules.d.ts +5 -1
- package/dist/modules.d.ts.map +1 -1
- package/dist/modules.js +124 -53
- package/dist/modules.js.map +1 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +61 -23
- package/dist/server.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +44 -45
- package/src/index.ts +1 -1
- package/src/modules.test.ts +278 -47
- package/src/modules.ts +142 -81
- package/src/server.test.ts +126 -34
- package/src/server.ts +61 -20
- package/tsconfig.build.tsbuildinfo +1 -1
package/src/modules.ts
CHANGED
|
@@ -1,107 +1,168 @@
|
|
|
1
1
|
import {annotationsModule} from '@shipfox/annotations';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import {annotationsInterModuleContract} from '@shipfox/annotations-dto/inter-module';
|
|
3
|
+
import {createAgentModule} from '@shipfox/api-agent';
|
|
4
|
+
import {agentInterModuleContract} from '@shipfox/api-agent-dto/inter-module';
|
|
5
|
+
import {createAuthModule} from '@shipfox/api-auth';
|
|
6
|
+
import {config as authConfig} from '@shipfox/api-auth/config';
|
|
7
|
+
import {authInterModuleContract} from '@shipfox/api-auth-dto/inter-module';
|
|
4
8
|
import {createDefinitionsModule} from '@shipfox/api-definitions';
|
|
9
|
+
import {definitionsInterModuleContract} from '@shipfox/api-definitions-dto/inter-module';
|
|
5
10
|
import {dispatcherModule} from '@shipfox/api-dispatcher';
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
createWorkspaceConnectionSnapshotLoader,
|
|
11
|
-
getIntegrationConnectionById,
|
|
12
|
-
} from '@shipfox/api-integration-core';
|
|
13
|
-
import {logsModule} from '@shipfox/api-logs';
|
|
11
|
+
import {emailChallengesModule} from '@shipfox/api-email-challenges';
|
|
12
|
+
import {createIntegrationsContext, type WebhookDeliverySource} from '@shipfox/api-integration-core';
|
|
13
|
+
import {integrationsInterModuleContract} from '@shipfox/api-integration-core-dto';
|
|
14
|
+
import {createLogsModule} from '@shipfox/api-logs';
|
|
14
15
|
import {createProjectsModule} from '@shipfox/api-projects';
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
} from '@shipfox/api-workflows';
|
|
16
|
+
import {projectsInterModuleContract} from '@shipfox/api-projects-dto';
|
|
17
|
+
import {createRunnersModule} from '@shipfox/api-runners';
|
|
18
|
+
import {runnersInterModuleContract} from '@shipfox/api-runners-dto/inter-module';
|
|
19
|
+
import {createSecretsModule} from '@shipfox/api-secrets';
|
|
20
|
+
import {secretsInterModuleContract} from '@shipfox/api-secrets-dto/inter-module';
|
|
21
|
+
import {createTriggersModule} from '@shipfox/api-triggers';
|
|
22
|
+
import {createWorkflowsModule} from '@shipfox/api-workflows';
|
|
23
|
+
import {workflowsInterModuleContract} from '@shipfox/api-workflows-dto/inter-module';
|
|
24
24
|
import {workspacesModule} from '@shipfox/api-workspaces';
|
|
25
|
+
import {workspacesInterModuleContract} from '@shipfox/api-workspaces-dto/inter-module';
|
|
26
|
+
import {durationToSeconds} from '@shipfox/node-jwt';
|
|
25
27
|
import type {ShipfoxModule} from '@shipfox/node-module';
|
|
28
|
+
import {
|
|
29
|
+
createInMemoryInterModuleTransport,
|
|
30
|
+
registerInterModulePresentations,
|
|
31
|
+
} from '@shipfox/node-module/inter-module';
|
|
32
|
+
|
|
33
|
+
export interface DefaultModulesOptions {
|
|
34
|
+
webhookDeliverySource?: WebhookDeliverySource | undefined;
|
|
35
|
+
}
|
|
26
36
|
|
|
27
|
-
export async function defaultModules(
|
|
37
|
+
export async function defaultModules(
|
|
38
|
+
options: DefaultModulesOptions = {},
|
|
39
|
+
): Promise<ShipfoxModule[]> {
|
|
40
|
+
const interModuleTransport = createInMemoryInterModuleTransport();
|
|
41
|
+
const workflowsClient = interModuleTransport.createClient(workflowsInterModuleContract);
|
|
42
|
+
const authClient = interModuleTransport.createClient(authInterModuleContract);
|
|
43
|
+
const agentClient = interModuleTransport.createClient(agentInterModuleContract);
|
|
44
|
+
const runnersClient = interModuleTransport.createClient(runnersInterModuleContract);
|
|
45
|
+
const projectsClient = interModuleTransport.createClient(projectsInterModuleContract);
|
|
46
|
+
const definitionsClient = interModuleTransport.createClient(definitionsInterModuleContract);
|
|
47
|
+
const annotationsClient = interModuleTransport.createClient(annotationsInterModuleContract);
|
|
48
|
+
const secretsClient = interModuleTransport.createClient(secretsInterModuleContract);
|
|
49
|
+
const workspacesClient = interModuleTransport.createClient(workspacesInterModuleContract);
|
|
50
|
+
const integrationsClient = interModuleTransport.createClient(integrationsInterModuleContract);
|
|
28
51
|
const integrations = await createIntegrationsContext({
|
|
52
|
+
workspaces: workspacesClient,
|
|
29
53
|
secrets: {
|
|
30
|
-
deleteSecrets,
|
|
54
|
+
deleteSecrets: async (params) => (await secretsClient.deleteSecrets(params)).deleted,
|
|
31
55
|
linear: {
|
|
32
|
-
getSecret: (params) =>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
...
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
getSecret: async (params) =>
|
|
57
|
+
(
|
|
58
|
+
await secretsClient.getSecret({
|
|
59
|
+
...params,
|
|
60
|
+
namespace: `system/integrations/linear/${params.namespace}`,
|
|
61
|
+
})
|
|
62
|
+
).value,
|
|
63
|
+
setSecrets: async (params) => {
|
|
64
|
+
const {editedBy, ...secretParams} = params;
|
|
65
|
+
await secretsClient.setSecrets({
|
|
66
|
+
...secretParams,
|
|
67
|
+
namespace: `system/integrations/linear/${secretParams.namespace}`,
|
|
68
|
+
...(editedBy === undefined ? {} : {editedBy}),
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
deleteSecrets: async (params) =>
|
|
72
|
+
(
|
|
73
|
+
await secretsClient.deleteSecrets({
|
|
74
|
+
...params,
|
|
75
|
+
namespace: `system/integrations/linear/${params.namespace}`,
|
|
76
|
+
})
|
|
77
|
+
).deleted,
|
|
78
|
+
},
|
|
79
|
+
jira: {
|
|
80
|
+
getSecret: async (params) =>
|
|
81
|
+
(
|
|
82
|
+
await secretsClient.getSecret({
|
|
83
|
+
...params,
|
|
84
|
+
namespace: `system/integrations/jira/${params.namespace}`,
|
|
85
|
+
})
|
|
86
|
+
).value,
|
|
87
|
+
setSecrets: async (params) => {
|
|
88
|
+
const {editedBy, ...secretParams} = params;
|
|
89
|
+
await secretsClient.setSecrets({
|
|
90
|
+
...secretParams,
|
|
91
|
+
namespace: `system/integrations/jira/${secretParams.namespace}`,
|
|
92
|
+
...(editedBy === undefined ? {} : {editedBy}),
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
deleteSecrets: async (params) =>
|
|
96
|
+
(
|
|
97
|
+
await secretsClient.deleteSecrets({
|
|
98
|
+
...params,
|
|
99
|
+
namespace: `system/integrations/jira/${params.namespace}`,
|
|
100
|
+
})
|
|
101
|
+
).deleted,
|
|
47
102
|
},
|
|
48
103
|
slack: {
|
|
49
|
-
getSecret: (params) =>
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
...
|
|
62
|
-
|
|
63
|
-
|
|
104
|
+
getSecret: async (params) =>
|
|
105
|
+
(
|
|
106
|
+
await secretsClient.getSecret({
|
|
107
|
+
...params,
|
|
108
|
+
namespace: `system/integrations/slack/${params.namespace}`,
|
|
109
|
+
})
|
|
110
|
+
).value,
|
|
111
|
+
setSecrets: async (params) => {
|
|
112
|
+
const {editedBy, ...secretParams} = params;
|
|
113
|
+
await secretsClient.setSecrets({
|
|
114
|
+
...secretParams,
|
|
115
|
+
namespace: `system/integrations/slack/${secretParams.namespace}`,
|
|
116
|
+
...(editedBy === undefined ? {} : {editedBy}),
|
|
117
|
+
});
|
|
118
|
+
},
|
|
119
|
+
deleteSecrets: async (params) =>
|
|
120
|
+
(
|
|
121
|
+
await secretsClient.deleteSecrets({
|
|
122
|
+
...params,
|
|
123
|
+
namespace: `system/integrations/slack/${params.namespace}`,
|
|
124
|
+
})
|
|
125
|
+
).deleted,
|
|
64
126
|
},
|
|
65
127
|
},
|
|
66
|
-
agentTools: {
|
|
67
|
-
|
|
68
|
-
const [agentToolSelectionCatalogs, agentToolCatalogs] = await Promise.all([
|
|
69
|
-
buildAgentToolSelectionCatalogs(integrations.registry),
|
|
70
|
-
buildAgentToolCatalogs(integrations.registry),
|
|
71
|
-
]);
|
|
72
|
-
const loadWorkspaceConnectionSnapshot = createWorkspaceConnectionSnapshotLoader(
|
|
73
|
-
integrations.registry,
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
// The checkout-token route resolves intents and mints credentials through the
|
|
77
|
-
// source-control service; wire it into the workflows module before serving.
|
|
78
|
-
setSourceControl(integrations.sourceControl);
|
|
79
|
-
setAgentToolMaterializationServices({
|
|
80
|
-
catalogs: agentToolCatalogs,
|
|
81
|
-
loadWorkspaceConnectionSnapshot,
|
|
82
|
-
getIntegrationConnectionById,
|
|
128
|
+
agentTools: {workflows: workflowsClient},
|
|
129
|
+
webhookDeliverySource: options.webhookDeliverySource,
|
|
83
130
|
});
|
|
84
|
-
const projectsModule = createProjectsModule({
|
|
131
|
+
const projectsModule = createProjectsModule({integrations: integrationsClient});
|
|
85
132
|
const definitionsModule = createDefinitionsModule({
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
loadWorkspaceConnectionSnapshot,
|
|
89
|
-
getIntegrationConnectionById,
|
|
133
|
+
projects: projectsClient,
|
|
134
|
+
integrations: integrationsClient,
|
|
90
135
|
});
|
|
91
136
|
|
|
92
|
-
|
|
93
|
-
|
|
137
|
+
const modules = [
|
|
138
|
+
emailChallengesModule,
|
|
139
|
+
createAuthModule({workspaces: workspacesClient}),
|
|
94
140
|
workspacesModule,
|
|
95
|
-
|
|
96
|
-
|
|
141
|
+
createSecretsModule(projectsClient),
|
|
142
|
+
createAgentModule({secrets: secretsClient}),
|
|
97
143
|
integrations.module,
|
|
98
144
|
projectsModule,
|
|
99
145
|
definitionsModule,
|
|
100
|
-
|
|
146
|
+
createWorkflowsModule({
|
|
147
|
+
annotations: annotationsClient,
|
|
148
|
+
agent: agentClient,
|
|
149
|
+
definitions: definitionsClient,
|
|
150
|
+
auth: authClient,
|
|
151
|
+
projects: projectsClient,
|
|
152
|
+
runners: runnersClient,
|
|
153
|
+
secrets: secretsClient,
|
|
154
|
+
integrations: integrationsClient,
|
|
155
|
+
}),
|
|
101
156
|
annotationsModule,
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
157
|
+
createRunnersModule({auth: authClient}),
|
|
158
|
+
createLogsModule({
|
|
159
|
+
workflows: workflowsClient,
|
|
160
|
+
jobLeaseTokenTtlSeconds: durationToSeconds(authConfig.AUTH_JOB_LEASE_TOKEN_EXPIRES_IN),
|
|
161
|
+
}),
|
|
162
|
+
createTriggersModule({workflows: workflowsClient}),
|
|
105
163
|
dispatcherModule,
|
|
106
164
|
];
|
|
165
|
+
registerInterModulePresentations({transport: interModuleTransport, modules});
|
|
166
|
+
interModuleTransport.seal();
|
|
167
|
+
return modules;
|
|
107
168
|
}
|
package/src/server.test.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
ModuleService,
|
|
3
|
+
ModuleServicesHandle,
|
|
2
4
|
ModuleWorker,
|
|
3
5
|
ModuleWorkersHandle,
|
|
4
6
|
ShipfoxModule,
|
|
@@ -8,6 +10,7 @@ import {createServer, runServer, type ServerHandle} from './server.js';
|
|
|
8
10
|
|
|
9
11
|
const mocks = vi.hoisted(() => {
|
|
10
12
|
const workersHandle = {stop: vi.fn()};
|
|
13
|
+
const servicesHandle = {stop: vi.fn()};
|
|
11
14
|
return {
|
|
12
15
|
captureException: vi.fn(),
|
|
13
16
|
closeApp: vi.fn(),
|
|
@@ -23,13 +26,13 @@ const mocks = vi.hoisted(() => {
|
|
|
23
26
|
logger: {error: vi.fn(), info: vi.fn()},
|
|
24
27
|
parseApiTrustProxy: vi.fn(),
|
|
25
28
|
registerModuleMetrics: vi.fn(),
|
|
26
|
-
resetPublishers: vi.fn(),
|
|
27
|
-
resetSubscribers: vi.fn(),
|
|
28
29
|
runModuleStartupTasks: vi.fn(),
|
|
29
30
|
shutdownServiceMetrics: vi.fn(),
|
|
31
|
+
startModuleServices: vi.fn(),
|
|
30
32
|
startModuleWorkers: vi.fn(),
|
|
31
33
|
startServiceMetrics: vi.fn(),
|
|
32
34
|
workersHandle,
|
|
35
|
+
servicesHandle,
|
|
33
36
|
apiConfig: {
|
|
34
37
|
API_PORT: undefined as number | undefined,
|
|
35
38
|
API_TRUST_PROXY: 'false',
|
|
@@ -51,9 +54,8 @@ vi.mock('@shipfox/node-module', () => ({
|
|
|
51
54
|
aggregateLoginMethods: mocks.aggregateLoginMethods,
|
|
52
55
|
initializeModules: mocks.initializeModules,
|
|
53
56
|
registerModuleMetrics: mocks.registerModuleMetrics,
|
|
54
|
-
resetPublishers: mocks.resetPublishers,
|
|
55
|
-
resetSubscribers: mocks.resetSubscribers,
|
|
56
57
|
runModuleStartupTasks: mocks.runModuleStartupTasks,
|
|
58
|
+
startModuleServices: mocks.startModuleServices,
|
|
57
59
|
startModuleWorkers: mocks.startModuleWorkers,
|
|
58
60
|
}));
|
|
59
61
|
vi.mock('@shipfox/node-opentelemetry', () => ({
|
|
@@ -87,12 +89,28 @@ function worker(taskQueue = 'runtime-queue'): ModuleWorker {
|
|
|
87
89
|
};
|
|
88
90
|
}
|
|
89
91
|
|
|
92
|
+
function service(name = 'runtime-service'): ModuleService {
|
|
93
|
+
return {name, shutdownTimeoutMs: 10_000, start: vi.fn()};
|
|
94
|
+
}
|
|
95
|
+
|
|
90
96
|
function lastStartModuleWorkersOptions(): StartModuleWorkersOptions {
|
|
91
97
|
const options = mocks.startModuleWorkers.mock.calls.at(-1)?.[0];
|
|
92
98
|
if (!options) throw new Error('startModuleWorkers was not called');
|
|
93
99
|
return options as StartModuleWorkersOptions;
|
|
94
100
|
}
|
|
95
101
|
|
|
102
|
+
function lastStartModuleServicesOptions(): {
|
|
103
|
+
services: ModuleService[];
|
|
104
|
+
onServiceFailure?: (error: unknown, service: ModuleService) => void | Promise<void>;
|
|
105
|
+
} {
|
|
106
|
+
const options = mocks.startModuleServices.mock.calls.at(-1)?.[0];
|
|
107
|
+
if (!options) throw new Error('startModuleServices was not called');
|
|
108
|
+
return options as {
|
|
109
|
+
services: ModuleService[];
|
|
110
|
+
onServiceFailure?: (error: unknown, service: ModuleService) => void | Promise<void>;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
96
114
|
function resetMocks(): void {
|
|
97
115
|
vi.useRealTimers();
|
|
98
116
|
mocks.captureException.mockReset();
|
|
@@ -110,13 +128,13 @@ function resetMocks(): void {
|
|
|
110
128
|
mocks.logger.info.mockReset();
|
|
111
129
|
mocks.parseApiTrustProxy.mockReset();
|
|
112
130
|
mocks.registerModuleMetrics.mockReset();
|
|
113
|
-
mocks.resetPublishers.mockReset();
|
|
114
|
-
mocks.resetSubscribers.mockReset();
|
|
115
131
|
mocks.runModuleStartupTasks.mockReset();
|
|
116
132
|
mocks.shutdownServiceMetrics.mockReset();
|
|
133
|
+
mocks.startModuleServices.mockReset();
|
|
117
134
|
mocks.startModuleWorkers.mockReset();
|
|
118
135
|
mocks.startServiceMetrics.mockReset();
|
|
119
136
|
mocks.workersHandle.stop.mockReset();
|
|
137
|
+
mocks.servicesHandle.stop.mockReset();
|
|
120
138
|
mocks.apiConfig.API_PORT = undefined;
|
|
121
139
|
mocks.apiConfig.API_TRUST_PROXY = 'false';
|
|
122
140
|
mocks.apiConfig.E2E_ENABLED = false;
|
|
@@ -132,13 +150,16 @@ function resetMocks(): void {
|
|
|
132
150
|
routes: [],
|
|
133
151
|
e2eRoutes: [],
|
|
134
152
|
workers: [worker()],
|
|
153
|
+
services: [service()],
|
|
135
154
|
});
|
|
136
155
|
mocks.listen.mockResolvedValue('http://127.0.0.1:3000');
|
|
137
156
|
mocks.parseApiTrustProxy.mockReturnValue(false);
|
|
138
157
|
mocks.runModuleStartupTasks.mockResolvedValue(undefined);
|
|
139
158
|
mocks.shutdownServiceMetrics.mockResolvedValue(undefined);
|
|
140
159
|
mocks.startModuleWorkers.mockResolvedValue(mocks.workersHandle);
|
|
160
|
+
mocks.startModuleServices.mockResolvedValue(mocks.servicesHandle);
|
|
141
161
|
mocks.workersHandle.stop.mockResolvedValue(undefined);
|
|
162
|
+
mocks.servicesHandle.stop.mockResolvedValue(undefined);
|
|
142
163
|
}
|
|
143
164
|
|
|
144
165
|
const servers: ServerHandle[] = [];
|
|
@@ -173,8 +194,14 @@ describe('createServer', () => {
|
|
|
173
194
|
expect(mocks.aggregateLoginMethods).toHaveBeenCalledWith({modules});
|
|
174
195
|
expect(mocks.createPostgresClient).toHaveBeenCalledOnce();
|
|
175
196
|
expect(mocks.initializeModules).toHaveBeenCalledWith({modules});
|
|
176
|
-
expect(mocks.registerModuleMetrics).toHaveBeenCalledWith({
|
|
177
|
-
|
|
197
|
+
expect(mocks.registerModuleMetrics).toHaveBeenCalledWith({
|
|
198
|
+
modules,
|
|
199
|
+
context: {outboxRegistry: undefined},
|
|
200
|
+
});
|
|
201
|
+
expect(mocks.runModuleStartupTasks).toHaveBeenCalledWith({
|
|
202
|
+
modules,
|
|
203
|
+
context: {outboxRegistry: undefined},
|
|
204
|
+
});
|
|
178
205
|
expect(mocks.createApp).toHaveBeenCalledWith({
|
|
179
206
|
auth: [],
|
|
180
207
|
routes: [],
|
|
@@ -201,6 +228,7 @@ describe('createServer', () => {
|
|
|
201
228
|
await server.start();
|
|
202
229
|
|
|
203
230
|
expect(mocks.startModuleWorkers).toHaveBeenCalledOnce();
|
|
231
|
+
expect(mocks.startModuleServices).toHaveBeenCalledOnce();
|
|
204
232
|
expect(mocks.listen).toHaveBeenCalledOnce();
|
|
205
233
|
});
|
|
206
234
|
|
|
@@ -230,6 +258,35 @@ describe('createServer', () => {
|
|
|
230
258
|
);
|
|
231
259
|
});
|
|
232
260
|
|
|
261
|
+
it('starts module services after workers and before listening for HTTP requests', async () => {
|
|
262
|
+
const server = await createTestServer({modules: [module()]});
|
|
263
|
+
let resolveServices: ((handle: ModuleServicesHandle) => void) | undefined;
|
|
264
|
+
const servicesStarted = new Promise<void>((resolve) => {
|
|
265
|
+
mocks.startModuleServices.mockImplementationOnce(
|
|
266
|
+
() =>
|
|
267
|
+
new Promise<ModuleServicesHandle>((innerResolve) => {
|
|
268
|
+
resolveServices = innerResolve;
|
|
269
|
+
resolve();
|
|
270
|
+
}),
|
|
271
|
+
);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const start = server.start();
|
|
275
|
+
await servicesStarted;
|
|
276
|
+
|
|
277
|
+
expect(mocks.listen).not.toHaveBeenCalled();
|
|
278
|
+
expect(mocks.startModuleWorkers.mock.invocationCallOrder[0]).toBeLessThan(
|
|
279
|
+
mocks.startModuleServices.mock.invocationCallOrder[0] ?? 0,
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
resolveServices?.(mocks.servicesHandle);
|
|
283
|
+
await start;
|
|
284
|
+
|
|
285
|
+
expect(mocks.startModuleServices.mock.invocationCallOrder[0]).toBeLessThan(
|
|
286
|
+
mocks.listen.mock.invocationCallOrder[0] ?? 0,
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
233
290
|
it('passes API_PORT to the HTTP listener when configured', async () => {
|
|
234
291
|
mocks.apiConfig.API_PORT = 55_291;
|
|
235
292
|
const server = await createTestServer({modules: [module()]});
|
|
@@ -250,6 +307,20 @@ describe('createServer', () => {
|
|
|
250
307
|
expect(mocks.listen).not.toHaveBeenCalled();
|
|
251
308
|
});
|
|
252
309
|
|
|
310
|
+
it('does not listen when module service startup fails', async () => {
|
|
311
|
+
const failure = new Error('service configuration invalid');
|
|
312
|
+
mocks.startModuleServices.mockRejectedValueOnce(failure);
|
|
313
|
+
const server = await createTestServer({modules: [module()]});
|
|
314
|
+
|
|
315
|
+
const result = server.start();
|
|
316
|
+
|
|
317
|
+
await expect(result).rejects.toBe(failure);
|
|
318
|
+
expect(mocks.listen).not.toHaveBeenCalled();
|
|
319
|
+
|
|
320
|
+
await server.stop();
|
|
321
|
+
expect(mocks.workersHandle.stop).toHaveBeenCalledOnce();
|
|
322
|
+
});
|
|
323
|
+
|
|
253
324
|
it('releases service metrics and Postgres when boot fails', async () => {
|
|
254
325
|
const failure = new Error('migration failed');
|
|
255
326
|
mocks.initializeModules.mockRejectedValueOnce(failure);
|
|
@@ -260,8 +331,6 @@ describe('createServer', () => {
|
|
|
260
331
|
expect(mocks.shutdownServiceMetrics.mock.invocationCallOrder[0]).toBeLessThan(
|
|
261
332
|
mocks.closePostgresClient.mock.invocationCallOrder[0] ?? 0,
|
|
262
333
|
);
|
|
263
|
-
expect(mocks.resetPublishers).toHaveBeenCalledOnce();
|
|
264
|
-
expect(mocks.resetSubscribers).toHaveBeenCalledOnce();
|
|
265
334
|
});
|
|
266
335
|
|
|
267
336
|
it('rejects a second server while another server owns process resources', async () => {
|
|
@@ -287,8 +356,6 @@ describe('createServer', () => {
|
|
|
287
356
|
|
|
288
357
|
await expect(result).rejects.toBe(failure);
|
|
289
358
|
expect(mocks.closePostgresClient).toHaveBeenCalledOnce();
|
|
290
|
-
expect(mocks.resetPublishers).toHaveBeenCalledOnce();
|
|
291
|
-
expect(mocks.resetSubscribers).toHaveBeenCalledOnce();
|
|
292
359
|
expect(mocks.logger.error).toHaveBeenCalledWith(
|
|
293
360
|
{err: cleanupFailure, bootError: failure},
|
|
294
361
|
'Failed to clean up API server boot',
|
|
@@ -305,11 +372,10 @@ describe('createServer', () => {
|
|
|
305
372
|
await secondStop;
|
|
306
373
|
|
|
307
374
|
expect(mocks.closeApp).toHaveBeenCalledOnce();
|
|
375
|
+
expect(mocks.servicesHandle.stop).toHaveBeenCalledOnce();
|
|
308
376
|
expect(mocks.workersHandle.stop).toHaveBeenCalledOnce();
|
|
309
377
|
expect(mocks.shutdownServiceMetrics).toHaveBeenCalledOnce();
|
|
310
378
|
expect(mocks.closePostgresClient).toHaveBeenCalledOnce();
|
|
311
|
-
expect(mocks.resetPublishers).toHaveBeenCalledOnce();
|
|
312
|
-
expect(mocks.resetSubscribers).toHaveBeenCalledOnce();
|
|
313
379
|
expect(mocks.closeErrorMonitoring).toHaveBeenCalledWith(2_000);
|
|
314
380
|
});
|
|
315
381
|
|
|
@@ -319,11 +385,10 @@ describe('createServer', () => {
|
|
|
319
385
|
await server.stop();
|
|
320
386
|
|
|
321
387
|
expect(mocks.closeApp).toHaveBeenCalledOnce();
|
|
388
|
+
expect(mocks.servicesHandle.stop).not.toHaveBeenCalled();
|
|
322
389
|
expect(mocks.workersHandle.stop).not.toHaveBeenCalled();
|
|
323
390
|
expect(mocks.shutdownServiceMetrics).toHaveBeenCalledOnce();
|
|
324
391
|
expect(mocks.closePostgresClient).toHaveBeenCalledOnce();
|
|
325
|
-
expect(mocks.resetPublishers).toHaveBeenCalledOnce();
|
|
326
|
-
expect(mocks.resetSubscribers).toHaveBeenCalledOnce();
|
|
327
392
|
});
|
|
328
393
|
|
|
329
394
|
it('waits for worker startup before stopping', async () => {
|
|
@@ -351,11 +416,10 @@ describe('createServer', () => {
|
|
|
351
416
|
|
|
352
417
|
expect(mocks.listen).not.toHaveBeenCalled();
|
|
353
418
|
expect(mocks.closeApp).toHaveBeenCalledOnce();
|
|
419
|
+
expect(mocks.servicesHandle.stop).toHaveBeenCalledOnce();
|
|
354
420
|
expect(mocks.workersHandle.stop).toHaveBeenCalledOnce();
|
|
355
421
|
expect(mocks.shutdownServiceMetrics).toHaveBeenCalledOnce();
|
|
356
422
|
expect(mocks.closePostgresClient).toHaveBeenCalledOnce();
|
|
357
|
-
expect(mocks.resetPublishers).toHaveBeenCalledOnce();
|
|
358
|
-
expect(mocks.resetSubscribers).toHaveBeenCalledOnce();
|
|
359
423
|
expect(mocks.closeErrorMonitoring).toHaveBeenCalledWith(2_000);
|
|
360
424
|
});
|
|
361
425
|
|
|
@@ -366,6 +430,9 @@ describe('createServer', () => {
|
|
|
366
430
|
await server.stop();
|
|
367
431
|
|
|
368
432
|
expect(mocks.closeApp.mock.invocationCallOrder[0]).toBeLessThan(
|
|
433
|
+
mocks.servicesHandle.stop.mock.invocationCallOrder[0] ?? 0,
|
|
434
|
+
);
|
|
435
|
+
expect(mocks.servicesHandle.stop.mock.invocationCallOrder[0]).toBeLessThan(
|
|
369
436
|
mocks.workersHandle.stop.mock.invocationCallOrder[0] ?? 0,
|
|
370
437
|
);
|
|
371
438
|
expect(mocks.workersHandle.stop.mock.invocationCallOrder[0]).toBeLessThan(
|
|
@@ -375,9 +442,6 @@ describe('createServer', () => {
|
|
|
375
442
|
mocks.closePostgresClient.mock.invocationCallOrder[0] ?? 0,
|
|
376
443
|
);
|
|
377
444
|
expect(mocks.closePostgresClient.mock.invocationCallOrder[0]).toBeLessThan(
|
|
378
|
-
mocks.resetPublishers.mock.invocationCallOrder[0] ?? 0,
|
|
379
|
-
);
|
|
380
|
-
expect(mocks.resetSubscribers.mock.invocationCallOrder[0]).toBeLessThan(
|
|
381
445
|
mocks.closeErrorMonitoring.mock.invocationCallOrder[0] ?? 0,
|
|
382
446
|
);
|
|
383
447
|
});
|
|
@@ -389,22 +453,15 @@ describe('createServer', () => {
|
|
|
389
453
|
mocks.workersHandle.stop.mockRejectedValueOnce(new Error('stop workers failed'));
|
|
390
454
|
mocks.shutdownServiceMetrics.mockRejectedValueOnce(new Error('shutdown metrics failed'));
|
|
391
455
|
mocks.closePostgresClient.mockRejectedValueOnce(new Error('close Postgres failed'));
|
|
392
|
-
mocks.resetPublishers.mockImplementationOnce(() => {
|
|
393
|
-
throw new Error('reset publishers failed');
|
|
394
|
-
});
|
|
395
|
-
mocks.resetSubscribers.mockImplementationOnce(() => {
|
|
396
|
-
throw new Error('reset subscribers failed');
|
|
397
|
-
});
|
|
398
456
|
mocks.closeErrorMonitoring.mockRejectedValueOnce(new Error('close monitoring failed'));
|
|
399
457
|
|
|
400
458
|
const result = server.stop();
|
|
401
459
|
|
|
402
460
|
await expect(result).rejects.toBeInstanceOf(AggregateError);
|
|
461
|
+
expect(mocks.servicesHandle.stop).toHaveBeenCalledOnce();
|
|
403
462
|
expect(mocks.workersHandle.stop).toHaveBeenCalledOnce();
|
|
404
463
|
expect(mocks.shutdownServiceMetrics).toHaveBeenCalledOnce();
|
|
405
464
|
expect(mocks.closePostgresClient).toHaveBeenCalledOnce();
|
|
406
|
-
expect(mocks.resetPublishers).toHaveBeenCalledOnce();
|
|
407
|
-
expect(mocks.resetSubscribers).toHaveBeenCalledOnce();
|
|
408
465
|
expect(mocks.closeErrorMonitoring).toHaveBeenCalledWith(2_000);
|
|
409
466
|
});
|
|
410
467
|
|
|
@@ -443,6 +500,27 @@ describe('createServer', () => {
|
|
|
443
500
|
);
|
|
444
501
|
});
|
|
445
502
|
|
|
503
|
+
it('reports service failures after closing HTTP and error monitoring', async () => {
|
|
504
|
+
const onServiceFailure = vi.fn();
|
|
505
|
+
const server = await createTestServer({modules: [module()], onServiceFailure});
|
|
506
|
+
await server.start();
|
|
507
|
+
const failure = new Error('poller failed');
|
|
508
|
+
const failedService = service();
|
|
509
|
+
|
|
510
|
+
await lastStartModuleServicesOptions().onServiceFailure?.(failure, failedService);
|
|
511
|
+
|
|
512
|
+
expect(mocks.logger.error).toHaveBeenCalledWith(
|
|
513
|
+
{err: failure, service: 'runtime-service'},
|
|
514
|
+
'Module service stopped unexpectedly',
|
|
515
|
+
);
|
|
516
|
+
expect(mocks.captureException).toHaveBeenCalledWith(failure);
|
|
517
|
+
expect(mocks.closeApp).toHaveBeenCalledOnce();
|
|
518
|
+
expect(mocks.closeErrorMonitoring).toHaveBeenCalledWith(2_000);
|
|
519
|
+
expect(mocks.closeErrorMonitoring.mock.invocationCallOrder[0]).toBeLessThan(
|
|
520
|
+
onServiceFailure.mock.invocationCallOrder[0] ?? 0,
|
|
521
|
+
);
|
|
522
|
+
});
|
|
523
|
+
|
|
446
524
|
it('reports a timed-out HTTP close before invoking the worker failure hook', async () => {
|
|
447
525
|
vi.useFakeTimers();
|
|
448
526
|
const onWorkerFailure = vi.fn();
|
|
@@ -459,7 +537,7 @@ describe('createServer', () => {
|
|
|
459
537
|
|
|
460
538
|
expect(mocks.logger.error).toHaveBeenCalledWith(
|
|
461
539
|
{timeoutMs: 10_000},
|
|
462
|
-
'Timed out closing HTTP server after
|
|
540
|
+
'Timed out closing HTTP server after module runtime failure',
|
|
463
541
|
);
|
|
464
542
|
expect(onWorkerFailure).toHaveBeenCalledWith(
|
|
465
543
|
failure,
|
|
@@ -478,7 +556,7 @@ describe('createServer', () => {
|
|
|
478
556
|
|
|
479
557
|
expect(mocks.logger.error).toHaveBeenCalledWith(
|
|
480
558
|
{err: closeFailure},
|
|
481
|
-
'Failed to close HTTP server after
|
|
559
|
+
'Failed to close HTTP server after module runtime failure',
|
|
482
560
|
);
|
|
483
561
|
expect(mocks.closeErrorMonitoring).toHaveBeenCalledWith(2_000);
|
|
484
562
|
expect(mocks.closeErrorMonitoring.mock.invocationCallOrder[0]).toBeLessThan(
|
|
@@ -519,11 +597,10 @@ describe('runServer', () => {
|
|
|
519
597
|
await expect(result).rejects.toBe(failure);
|
|
520
598
|
expect(onStartupFailure).toHaveBeenCalledWith(failure);
|
|
521
599
|
expect(mocks.closeApp).toHaveBeenCalledOnce();
|
|
600
|
+
expect(mocks.servicesHandle.stop).toHaveBeenCalledOnce();
|
|
522
601
|
expect(mocks.workersHandle.stop).toHaveBeenCalledOnce();
|
|
523
602
|
expect(mocks.shutdownServiceMetrics).toHaveBeenCalledOnce();
|
|
524
603
|
expect(mocks.closePostgresClient).toHaveBeenCalledOnce();
|
|
525
|
-
expect(mocks.resetPublishers).toHaveBeenCalledOnce();
|
|
526
|
-
expect(mocks.resetSubscribers).toHaveBeenCalledOnce();
|
|
527
604
|
expect(mocks.closeErrorMonitoring).toHaveBeenCalledWith(2_000);
|
|
528
605
|
expect(onStartupFailure.mock.invocationCallOrder[0]).toBeLessThan(
|
|
529
606
|
mocks.closeErrorMonitoring.mock.invocationCallOrder[0] ?? 0,
|
|
@@ -557,10 +634,25 @@ describe('runServer', () => {
|
|
|
557
634
|
it('exits unsuccessfully when a module worker fails', async () => {
|
|
558
635
|
vi.spyOn(process, 'once').mockImplementation((() => process) as never);
|
|
559
636
|
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as never);
|
|
560
|
-
await runServer({modules: [module()]});
|
|
637
|
+
const handle = await runServer({modules: [module()]});
|
|
561
638
|
|
|
562
639
|
await lastStartModuleWorkersOptions().onWorkerFailure?.(new Error('poller failed'), worker());
|
|
563
640
|
|
|
564
641
|
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
642
|
+
await handle.stop();
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
it('exits unsuccessfully when a module service fails', async () => {
|
|
646
|
+
vi.spyOn(process, 'once').mockImplementation((() => process) as never);
|
|
647
|
+
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as never);
|
|
648
|
+
const handle = await runServer({modules: [module()]});
|
|
649
|
+
|
|
650
|
+
await lastStartModuleServicesOptions().onServiceFailure?.(
|
|
651
|
+
new Error('poller failed'),
|
|
652
|
+
service(),
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
656
|
+
await handle.stop();
|
|
565
657
|
});
|
|
566
658
|
});
|