@google-cloud/cloud-run-mcp 1.10.0 → 1.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/.github/workflows/npm-publish.yml +10 -6
- package/README.md +26 -0
- package/SECURITY.md +7 -0
- package/cloudbuild.yaml +37 -0
- package/example-sources-to-deploy/java/pom.xml +41 -0
- package/example-sources-to-deploy/java/src/main/java/com/example/helloworld/HelloworldApplication.java +21 -0
- package/example-sources-to-deploy/java/src/main/resources/application.properties +1 -0
- package/example-sources-to-deploy/nodejs/index.js +15 -0
- package/example-sources-to-deploy/nodejs/package.json +12 -0
- package/example-sources-to-deploy/python/pip-project/main.py +11 -0
- package/example-sources-to-deploy/python/pip-project/requirements.txt +2 -0
- package/example-sources-to-deploy/python/pyproject-project/main.py +11 -0
- package/example-sources-to-deploy/python/pyproject-project/pyproject.toml +10 -0
- package/lib/clients.js +142 -12
- package/lib/cloud-api/build.js +192 -28
- package/lib/cloud-api/run.js +62 -0
- package/lib/cloud-api/secrets.js +196 -0
- package/lib/cloud-api/storage.js +164 -16
- package/lib/deployment/compose.js +517 -0
- package/lib/deployment/constants.js +8 -0
- package/lib/deployment/deployer.js +283 -28
- package/lib/deployment/helpers.js +58 -1
- package/lib/deployment/universal-maker.js +21 -148
- package/lib/middleware/oauth.js +26 -15
- package/lib/util/archive.js +3 -3
- package/lib/util/artifacts.js +189 -0
- package/lib/util/helpers.js +109 -0
- package/mcp-server.js +83 -20
- package/package.json +7 -4
- package/skills/cloud-run/SKILL.md +109 -0
- package/test/local/archive.test.js +88 -0
- package/test/local/clients.test.js +307 -2
- package/test/local/cloud-api/build.test.js +330 -25
- package/test/local/cloud-api/run.test.js +129 -0
- package/test/local/cloud-api/secrets.test.js +291 -0
- package/test/local/cloud-api/storage.test.js +405 -0
- package/test/local/compose.test.js +618 -0
- package/test/local/deploy.test.js +408 -0
- package/test/local/deployment-helpers.test.js +100 -1
- package/test/local/mcp-server-streamable-http.test.js +132 -12
- package/test/local/middleware/oauth.test.js +106 -5
- package/test/local/test-utils.js +2 -18
- package/test/local/tools.test.js +43 -1
- package/test/local/universal-maker.test.js +23 -114
- package/test/local/util.test.js +361 -0
- package/test/need-gcp/deployer.test.js +1 -1
- package/test/need-gcp/test-helpers.js +5 -1
- package/test/need-gcp/workflows/deployment-workflows.test.js +172 -6
- package/tools/register-tools.js +98 -78
- /package/example-sources-to-deploy/{Dockerfile → golang/Dockerfile} +0 -0
- /package/example-sources-to-deploy/{go.mod → golang/go.mod} +0 -0
- /package/example-sources-to-deploy/{main.go → golang/main.go} +0 -0
|
@@ -17,6 +17,7 @@ describe('oauthMiddleware', () => {
|
|
|
17
17
|
};
|
|
18
18
|
res = {
|
|
19
19
|
headersSent: false,
|
|
20
|
+
set: mock.fn(),
|
|
20
21
|
status: mock.fn(() => res),
|
|
21
22
|
json: mock.fn(),
|
|
22
23
|
};
|
|
@@ -41,12 +42,10 @@ describe('oauthMiddleware', () => {
|
|
|
41
42
|
assert.strictEqual(res.status.mock.callCount(), 0);
|
|
42
43
|
});
|
|
43
44
|
|
|
44
|
-
it('should
|
|
45
|
+
it('should return 401 if Authorization header is missing even when method is not tools/call', async () => {
|
|
45
46
|
process.env.OAUTH_ENABLED = 'true';
|
|
46
47
|
req.body.method = 'other/method';
|
|
47
48
|
|
|
48
|
-
// We don't verify token if it's not a tool call (based on current implementation)
|
|
49
|
-
// So we don't need to mock successful verification here
|
|
50
49
|
const { oauthMiddleware } = await esmock(
|
|
51
50
|
'../../../lib/middleware/oauth.js',
|
|
52
51
|
{}
|
|
@@ -54,7 +53,61 @@ describe('oauthMiddleware', () => {
|
|
|
54
53
|
|
|
55
54
|
await oauthMiddleware(req, res, next);
|
|
56
55
|
|
|
57
|
-
assert.strictEqual(next.mock.callCount(),
|
|
56
|
+
assert.strictEqual(next.mock.callCount(), 0);
|
|
57
|
+
assert.strictEqual(res.status.mock.callCount(), 1);
|
|
58
|
+
assert.deepStrictEqual(res.status.mock.calls[0].arguments, [401]);
|
|
59
|
+
assert.strictEqual(res.json.mock.callCount(), 1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should return 401 if Authorization header is missing when req.body is a JSON array', async () => {
|
|
63
|
+
process.env.OAUTH_ENABLED = 'true';
|
|
64
|
+
req.body = [{ jsonrpc: '2.0', method: 'tools/call', id: 1 }];
|
|
65
|
+
|
|
66
|
+
const { oauthMiddleware } = await esmock(
|
|
67
|
+
'../../../lib/middleware/oauth.js',
|
|
68
|
+
{}
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
await oauthMiddleware(req, res, next);
|
|
72
|
+
|
|
73
|
+
assert.strictEqual(next.mock.callCount(), 0);
|
|
74
|
+
assert.strictEqual(res.status.mock.callCount(), 1);
|
|
75
|
+
assert.deepStrictEqual(res.status.mock.calls[0].arguments, [401]);
|
|
76
|
+
assert.strictEqual(res.json.mock.callCount(), 1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should return 401 if Authorization header is missing when req.body is undefined (e.g. GET /sse)', async () => {
|
|
80
|
+
process.env.OAUTH_ENABLED = 'true';
|
|
81
|
+
req.body = undefined;
|
|
82
|
+
|
|
83
|
+
const { oauthMiddleware } = await esmock(
|
|
84
|
+
'../../../lib/middleware/oauth.js',
|
|
85
|
+
{}
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
await oauthMiddleware(req, res, next);
|
|
89
|
+
|
|
90
|
+
assert.strictEqual(next.mock.callCount(), 0);
|
|
91
|
+
assert.strictEqual(res.status.mock.callCount(), 1);
|
|
92
|
+
assert.deepStrictEqual(res.status.mock.calls[0].arguments, [401]);
|
|
93
|
+
assert.strictEqual(res.json.mock.callCount(), 1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should return 401 if Authorization header has no token value', async () => {
|
|
97
|
+
process.env.OAUTH_ENABLED = 'true';
|
|
98
|
+
req.headers.authorization = 'Bearer';
|
|
99
|
+
|
|
100
|
+
const { oauthMiddleware } = await esmock(
|
|
101
|
+
'../../../lib/middleware/oauth.js',
|
|
102
|
+
{}
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
await oauthMiddleware(req, res, next);
|
|
106
|
+
|
|
107
|
+
assert.strictEqual(next.mock.callCount(), 0);
|
|
108
|
+
assert.strictEqual(res.status.mock.callCount(), 1);
|
|
109
|
+
assert.deepStrictEqual(res.status.mock.calls[0].arguments, [401]);
|
|
110
|
+
assert.strictEqual(res.json.mock.callCount(), 1);
|
|
58
111
|
});
|
|
59
112
|
|
|
60
113
|
it('should return 401 if Authorization header is missing for tool call', async () => {
|
|
@@ -74,8 +127,27 @@ describe('oauthMiddleware', () => {
|
|
|
74
127
|
assert.strictEqual(res.json.mock.callCount(), 1);
|
|
75
128
|
});
|
|
76
129
|
|
|
130
|
+
it('should return 401 if GOOGLE_OAUTH_AUDIENCE is missing when OAUTH_ENABLED is "true"', async () => {
|
|
131
|
+
process.env.OAUTH_ENABLED = 'true';
|
|
132
|
+
delete process.env.GOOGLE_OAUTH_AUDIENCE;
|
|
133
|
+
req.body.method = 'tools/call';
|
|
134
|
+
req.headers.authorization = 'Bearer valid-token';
|
|
135
|
+
|
|
136
|
+
const { oauthMiddleware } = await esmock(
|
|
137
|
+
'../../../lib/middleware/oauth.js',
|
|
138
|
+
{}
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
await oauthMiddleware(req, res, next);
|
|
142
|
+
|
|
143
|
+
assert.strictEqual(next.mock.callCount(), 0);
|
|
144
|
+
assert.strictEqual(res.status.mock.callCount(), 1);
|
|
145
|
+
assert.deepStrictEqual(res.status.mock.calls[0].arguments, [401]);
|
|
146
|
+
});
|
|
147
|
+
|
|
77
148
|
it('should verify token and call next() for valid tool call', async () => {
|
|
78
149
|
process.env.OAUTH_ENABLED = 'true';
|
|
150
|
+
process.env.GOOGLE_OAUTH_AUDIENCE = 'valid-audience';
|
|
79
151
|
req.body.method = 'tools/call';
|
|
80
152
|
req.headers.authorization = 'Bearer valid-token';
|
|
81
153
|
|
|
@@ -102,6 +174,7 @@ describe('oauthMiddleware', () => {
|
|
|
102
174
|
|
|
103
175
|
it('should return 401 if token verification fails', async () => {
|
|
104
176
|
process.env.OAUTH_ENABLED = 'true';
|
|
177
|
+
process.env.GOOGLE_OAUTH_AUDIENCE = 'valid-audience';
|
|
105
178
|
req.body.method = 'tools/call';
|
|
106
179
|
req.headers.authorization = 'Bearer invalid-token';
|
|
107
180
|
|
|
@@ -127,12 +200,13 @@ describe('oauthMiddleware', () => {
|
|
|
127
200
|
assert.deepStrictEqual(res.status.mock.calls[0].arguments, [401]);
|
|
128
201
|
});
|
|
129
202
|
|
|
130
|
-
it('should return 401 if audience does not match', async () => {
|
|
203
|
+
it('should return 401 if audience does not match and remove token from oauth cache', async () => {
|
|
131
204
|
process.env.OAUTH_ENABLED = 'true';
|
|
132
205
|
process.env.GOOGLE_OAUTH_AUDIENCE = 'expected-audience';
|
|
133
206
|
req.body.method = 'tools/call';
|
|
134
207
|
req.headers.authorization = 'Bearer valid-token-wrong-audience';
|
|
135
208
|
|
|
209
|
+
const mockOAuthMap = new Map([['valid-token-wrong-audience', {}]]);
|
|
136
210
|
const mockGetTokenInfo = mock.fn(async () => ({ aud: 'wrong-audience' }));
|
|
137
211
|
const mockGetOAuthClient = mock.fn(async () => ({
|
|
138
212
|
getTokenInfo: mockGetTokenInfo,
|
|
@@ -142,6 +216,7 @@ describe('oauthMiddleware', () => {
|
|
|
142
216
|
'../../../lib/middleware/oauth.js',
|
|
143
217
|
{
|
|
144
218
|
'../../../lib/clients.js': {
|
|
219
|
+
clients: { oauth: mockOAuthMap },
|
|
145
220
|
getOAuthClient: mockGetOAuthClient,
|
|
146
221
|
},
|
|
147
222
|
}
|
|
@@ -152,5 +227,31 @@ describe('oauthMiddleware', () => {
|
|
|
152
227
|
assert.strictEqual(next.mock.callCount(), 0);
|
|
153
228
|
assert.strictEqual(res.status.mock.callCount(), 1);
|
|
154
229
|
assert.deepStrictEqual(res.status.mock.calls[0].arguments, [401]);
|
|
230
|
+
assert.strictEqual(mockOAuthMap.has('valid-token-wrong-audience'), false);
|
|
231
|
+
assert.strictEqual(
|
|
232
|
+
res.json.mock.calls[0].arguments[0].error.data,
|
|
233
|
+
'Invalid or missing access token'
|
|
234
|
+
);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should set WWW-Authenticate header on 401 when OAUTH_PROTECTED_RESOURCE is set', async () => {
|
|
238
|
+
process.env.OAUTH_ENABLED = 'true';
|
|
239
|
+
process.env.OAUTH_PROTECTED_RESOURCE = 'https://example.com/resource';
|
|
240
|
+
|
|
241
|
+
const { oauthMiddleware } = await esmock(
|
|
242
|
+
'../../../lib/middleware/oauth.js',
|
|
243
|
+
{}
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
await oauthMiddleware(req, res, next);
|
|
247
|
+
|
|
248
|
+
assert.strictEqual(next.mock.callCount(), 0);
|
|
249
|
+
assert.strictEqual(res.set.mock.callCount(), 1);
|
|
250
|
+
assert.deepStrictEqual(res.set.mock.calls[0].arguments, [
|
|
251
|
+
'WWW-Authenticate',
|
|
252
|
+
'Bearer resource_metadata="https://example.com/.well-known/oauth-protected-resource"',
|
|
253
|
+
]);
|
|
254
|
+
assert.strictEqual(res.status.mock.callCount(), 1);
|
|
255
|
+
assert.deepStrictEqual(res.status.mock.calls[0].arguments, [401]);
|
|
155
256
|
});
|
|
156
257
|
});
|
package/test/local/test-utils.js
CHANGED
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
/*
|
|
2
|
-
Copyright 2025 Google LLC
|
|
3
|
-
|
|
4
|
-
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
-
you may not use this file except in compliance with the License.
|
|
6
|
-
You may obtain a copy of the License at
|
|
7
|
-
|
|
8
|
-
https://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
-
|
|
10
|
-
Unless required by applicable law or agreed to in writing, software
|
|
11
|
-
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
-
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
-
See the License for the specific language governing permissions and
|
|
14
|
-
limitations under the License.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
1
|
/**
|
|
18
2
|
* Waits for a specific string to appear in a stream's output.
|
|
19
3
|
* This is useful for synchronizing tests with asynchronous child processes,
|
|
@@ -22,11 +6,11 @@ limitations under the License.
|
|
|
22
6
|
*
|
|
23
7
|
* @param {ReadableStream} stream - The stream to listen to (e.g., process.stdout or process.stderr).
|
|
24
8
|
* @param {string} str - The string to wait for in the stream's output.
|
|
25
|
-
* @param {number} [timeoutMs=
|
|
9
|
+
* @param {number} [timeoutMs=10000] - The maximum time in milliseconds to wait before rejecting.
|
|
26
10
|
* @returns {Promise<string>} A promise that resolves with the accumulated data
|
|
27
11
|
* when the string is found, or rejects if the timeout is reached.
|
|
28
12
|
*/
|
|
29
|
-
export async function waitForString(stream, str, timeoutMs =
|
|
13
|
+
export async function waitForString(stream, str, timeoutMs = 10000) {
|
|
30
14
|
let accumulatedData = '';
|
|
31
15
|
return new Promise((resolve, reject) => {
|
|
32
16
|
const timeout = setTimeout(() => {
|
package/test/local/tools.test.js
CHANGED
|
@@ -207,6 +207,7 @@ describe('registerTools', () => {
|
|
|
207
207
|
name: 'my-service',
|
|
208
208
|
uri: 'my-uri',
|
|
209
209
|
lastModifier: 'me',
|
|
210
|
+
updateTime: { seconds: '1735732800', nanos: 0 }, // 2025-01-01T12:00:00Z
|
|
210
211
|
}),
|
|
211
212
|
},
|
|
212
213
|
}
|
|
@@ -223,11 +224,12 @@ describe('registerTools', () => {
|
|
|
223
224
|
service: 'my-service',
|
|
224
225
|
});
|
|
225
226
|
|
|
227
|
+
const expectedDate = new Date('2025-01-01T12:00:00Z').toUTCString();
|
|
226
228
|
assert.deepStrictEqual(result, {
|
|
227
229
|
content: [
|
|
228
230
|
{
|
|
229
231
|
type: 'text',
|
|
230
|
-
text:
|
|
232
|
+
text: `Name: my-service\nRegion: my-region\nProject: my-project\nURL: my-uri\nConsole URL: https://console.cloud.google.com/run/detail/my-region/my-service?project=my-project\nLast deployed by: me\nLast deployed date: ${expectedDate}`,
|
|
231
233
|
},
|
|
232
234
|
],
|
|
233
235
|
});
|
|
@@ -324,6 +326,46 @@ describe('registerTools', () => {
|
|
|
324
326
|
],
|
|
325
327
|
});
|
|
326
328
|
});
|
|
329
|
+
|
|
330
|
+
it('should deploy local folder with ingress', async () => {
|
|
331
|
+
const server = {
|
|
332
|
+
registerTool: mock.fn(),
|
|
333
|
+
};
|
|
334
|
+
const deployMock = mock.fn(() => Promise.resolve({ uri: 'my-uri' }));
|
|
335
|
+
|
|
336
|
+
const { registerTools } = await esmock(
|
|
337
|
+
'../../tools/tools.js',
|
|
338
|
+
{},
|
|
339
|
+
{
|
|
340
|
+
'../../lib/deployment/deployer.js': {
|
|
341
|
+
deploy: deployMock,
|
|
342
|
+
},
|
|
343
|
+
}
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
registerTools(server, {
|
|
347
|
+
gcpCredentialsAvailable: true,
|
|
348
|
+
ingress: 'INGRESS_TRAFFIC_INTERNAL_ONLY',
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const handler = server.registerTool.mock.calls.find(
|
|
352
|
+
(call) => call.arguments[0] === 'deploy_local_folder'
|
|
353
|
+
).arguments[2];
|
|
354
|
+
await handler(
|
|
355
|
+
{
|
|
356
|
+
project: 'my-project',
|
|
357
|
+
region: 'my-region',
|
|
358
|
+
service: 'my-service',
|
|
359
|
+
folderPath: '/my/folder',
|
|
360
|
+
},
|
|
361
|
+
{ sendNotification: mock.fn() }
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
assert.strictEqual(
|
|
365
|
+
deployMock.mock.calls[0].arguments[0].ingress,
|
|
366
|
+
'INGRESS_TRAFFIC_INTERNAL_ONLY'
|
|
367
|
+
);
|
|
368
|
+
});
|
|
327
369
|
});
|
|
328
370
|
|
|
329
371
|
describe('deploy_file_contents', () => {
|
|
@@ -22,15 +22,8 @@ describe('Universal Maker', () => {
|
|
|
22
22
|
const fsMock = {
|
|
23
23
|
existsSync: mock.fn(),
|
|
24
24
|
mkdirSync: mock.fn(),
|
|
25
|
-
chmodSync: mock.fn(),
|
|
26
25
|
readFileSync: mock.fn(),
|
|
27
26
|
rmSync: mock.fn(),
|
|
28
|
-
unlinkSync: mock.fn(),
|
|
29
|
-
createWriteStream: mock.fn(),
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const cryptoMock = {
|
|
33
|
-
createHash: mock.fn(),
|
|
34
27
|
};
|
|
35
28
|
|
|
36
29
|
const childProcessMock = {
|
|
@@ -46,34 +39,27 @@ describe('Universal Maker', () => {
|
|
|
46
39
|
logAndProgress: mock.fn(),
|
|
47
40
|
};
|
|
48
41
|
|
|
49
|
-
const
|
|
50
|
-
|
|
42
|
+
const artifactsMock = {
|
|
43
|
+
ensureRepositoryDownloaded: mock.fn(),
|
|
51
44
|
};
|
|
52
45
|
|
|
53
46
|
test('runUniversalMaker skips download if binary exists and runs successfully', async () => {
|
|
54
47
|
fsMock.existsSync.mock.resetCalls();
|
|
55
48
|
fsMock.readFileSync.mock.resetCalls();
|
|
56
49
|
childProcessMock.exec.mock.resetCalls();
|
|
50
|
+
artifactsMock.ensureRepositoryDownloaded.mock.resetCalls();
|
|
57
51
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
'
|
|
61
|
-
|
|
62
|
-
cryptoMock.createHash.mock.mockImplementation(() => ({
|
|
63
|
-
update: mock.fn(() => ({
|
|
64
|
-
digest: mock.fn(() => sha256),
|
|
65
|
-
})),
|
|
66
|
-
}));
|
|
52
|
+
// Mock binary exists and is up to date
|
|
53
|
+
artifactsMock.ensureRepositoryDownloaded.mock.mockImplementation(
|
|
54
|
+
() => '/home/user/.cloud-run-mcp/bin/universal_maker'
|
|
55
|
+
);
|
|
67
56
|
|
|
68
|
-
// Mock binary exists
|
|
69
57
|
fsMock.existsSync.mock.mockImplementation((p) => {
|
|
70
|
-
if (p.includes('universal_maker')) return true;
|
|
71
58
|
if (p.includes('build_output.json')) return true;
|
|
72
59
|
return false;
|
|
73
60
|
});
|
|
74
61
|
|
|
75
62
|
fsMock.readFileSync.mock.mockImplementation((p) => {
|
|
76
|
-
if (p.includes('universal_maker')) return localContent;
|
|
77
63
|
if (p.includes('build_output.json')) {
|
|
78
64
|
return JSON.stringify({
|
|
79
65
|
command: 'node',
|
|
@@ -85,34 +71,12 @@ describe('Universal Maker', () => {
|
|
|
85
71
|
return '';
|
|
86
72
|
});
|
|
87
73
|
|
|
88
|
-
// Mock Artifact Registry client
|
|
89
|
-
const artifactRegistryClientMock = {
|
|
90
|
-
filePath: mock.fn(
|
|
91
|
-
(p, l, r, f) =>
|
|
92
|
-
`projects/${p}/locations/${l}/repositories/${r}/files/${f}`
|
|
93
|
-
),
|
|
94
|
-
getFile: mock.fn(() => [
|
|
95
|
-
{
|
|
96
|
-
hashes: [
|
|
97
|
-
{
|
|
98
|
-
type: 'SHA256',
|
|
99
|
-
value: Buffer.from(sha256, 'hex').toString('base64'),
|
|
100
|
-
},
|
|
101
|
-
],
|
|
102
|
-
},
|
|
103
|
-
]),
|
|
104
|
-
};
|
|
105
|
-
clientsMock.getArtifactRegistryClient.mock.mockImplementation(
|
|
106
|
-
() => artifactRegistryClientMock
|
|
107
|
-
);
|
|
108
|
-
|
|
109
74
|
const um = await esmock('../../lib/deployment/universal-maker.js', {
|
|
110
75
|
fs: fsMock,
|
|
111
76
|
child_process: childProcessMock,
|
|
112
77
|
os: osMock,
|
|
113
|
-
crypto: cryptoMock,
|
|
114
78
|
'../../lib/util/helpers.js': helpersMock,
|
|
115
|
-
'../../lib/
|
|
79
|
+
'../../lib/util/artifacts.js': artifactsMock,
|
|
116
80
|
});
|
|
117
81
|
|
|
118
82
|
// Mock exec only for binary execution
|
|
@@ -132,92 +96,38 @@ describe('Universal Maker', () => {
|
|
|
132
96
|
assert.deepEqual(result.args, ['index.js']);
|
|
133
97
|
assert.equal(result.runtime, 'nodejs20');
|
|
134
98
|
|
|
135
|
-
// Verify
|
|
136
|
-
|
|
137
|
-
|
|
99
|
+
// Verify ensureRepositoryDownloaded was called
|
|
100
|
+
assert.strictEqual(
|
|
101
|
+
artifactsMock.ensureRepositoryDownloaded.mock.callCount(),
|
|
102
|
+
1
|
|
138
103
|
);
|
|
139
|
-
assert.ok(binExistsCall);
|
|
140
104
|
});
|
|
141
105
|
|
|
142
|
-
test('runUniversalMaker
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
let downloaded = false;
|
|
148
|
-
|
|
149
|
-
// Mock binary missing first, then exists after "download"
|
|
150
|
-
fsMock.existsSync.mock.mockImplementation((p) => {
|
|
151
|
-
if (p.includes('universal_maker')) return downloaded;
|
|
152
|
-
if (p.includes('build_output.json')) return true;
|
|
153
|
-
return true; // for dir checks
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
// Mock Artifact Registry client for download
|
|
157
|
-
const artifactRegistryClientMock = {
|
|
158
|
-
filePath: mock.fn(
|
|
159
|
-
(p, l, r, f) =>
|
|
160
|
-
`projects/${p}/locations/${l}/repositories/${r}/files/${f}`
|
|
161
|
-
),
|
|
162
|
-
getFile: mock.fn(() => [{ hashes: [] }]), // trigger download
|
|
163
|
-
auth: {
|
|
164
|
-
request: mock.fn(() => {
|
|
165
|
-
downloaded = true;
|
|
166
|
-
return {
|
|
167
|
-
data: {
|
|
168
|
-
pipe: (dest) => {
|
|
169
|
-
setTimeout(() => dest.emit('finish'), 10);
|
|
170
|
-
return dest;
|
|
171
|
-
},
|
|
172
|
-
},
|
|
173
|
-
};
|
|
174
|
-
}),
|
|
175
|
-
},
|
|
176
|
-
};
|
|
177
|
-
clientsMock.getArtifactRegistryClient.mock.mockImplementation(
|
|
178
|
-
() => artifactRegistryClientMock
|
|
179
|
-
);
|
|
180
|
-
|
|
181
|
-
const writeStreamMock = {
|
|
182
|
-
on: mock.fn(function (event, cb) {
|
|
183
|
-
if (event === 'finish') this.finishCb = cb;
|
|
184
|
-
return this;
|
|
185
|
-
}),
|
|
186
|
-
emit: mock.fn(function (event) {
|
|
187
|
-
if (event === 'finish' && this.finishCb) this.finishCb();
|
|
188
|
-
}),
|
|
189
|
-
pipeFrom: mock.fn(),
|
|
190
|
-
};
|
|
191
|
-
fsMock.createWriteStream = mock.fn(() => writeStreamMock);
|
|
192
|
-
|
|
193
|
-
fsMock.readFileSync.mock.mockImplementation(() =>
|
|
194
|
-
JSON.stringify({ command: 'npm', args: ['start'] })
|
|
106
|
+
test('runUniversalMaker behaves correctly when binary fails to download', async () => {
|
|
107
|
+
artifactsMock.ensureRepositoryDownloaded.mock.resetCalls();
|
|
108
|
+
artifactsMock.ensureRepositoryDownloaded.mock.mockImplementation(
|
|
109
|
+
() => null
|
|
195
110
|
);
|
|
196
111
|
|
|
197
112
|
const um = await esmock('../../lib/deployment/universal-maker.js', {
|
|
198
113
|
fs: fsMock,
|
|
199
114
|
child_process: childProcessMock,
|
|
200
115
|
os: osMock,
|
|
201
|
-
crypto: cryptoMock,
|
|
202
116
|
'../../lib/util/helpers.js': helpersMock,
|
|
203
|
-
'../../lib/
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
// Mock exec only for binary execution
|
|
207
|
-
childProcessMock.exec.mock.mockImplementation((cmd, cb) => {
|
|
208
|
-
cb(null, { stdout: '', stderr: '' });
|
|
117
|
+
'../../lib/util/artifacts.js': artifactsMock,
|
|
209
118
|
});
|
|
210
119
|
|
|
211
|
-
// runUniversalMaker(appDir, accessToken, progressCallback)
|
|
212
120
|
const result = await um.runUniversalMaker(
|
|
213
121
|
'/app/dir',
|
|
214
122
|
'fake-token',
|
|
215
123
|
mock.fn()
|
|
216
124
|
);
|
|
217
125
|
|
|
218
|
-
assert.
|
|
219
|
-
assert.
|
|
220
|
-
|
|
126
|
+
assert.equal(result, null);
|
|
127
|
+
assert.strictEqual(
|
|
128
|
+
artifactsMock.ensureRepositoryDownloaded.mock.callCount(),
|
|
129
|
+
1
|
|
130
|
+
);
|
|
221
131
|
});
|
|
222
132
|
|
|
223
133
|
test('runUniversalMaker returns null if binary not supported on platform', async () => {
|
|
@@ -230,9 +140,8 @@ describe('Universal Maker', () => {
|
|
|
230
140
|
fs: fsMock,
|
|
231
141
|
child_process: childProcessMock,
|
|
232
142
|
os: osMock,
|
|
233
|
-
crypto: cryptoMock,
|
|
234
143
|
'../../lib/util/helpers.js': helpersMock,
|
|
235
|
-
'../../lib/
|
|
144
|
+
'../../lib/util/artifacts.js': artifactsMock,
|
|
236
145
|
});
|
|
237
146
|
|
|
238
147
|
const result = await um.runUniversalMaker(
|