@google-cloud/cloud-run-mcp 1.10.0 → 1.11.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 +24 -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 +104 -3
- 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/util/archive.js +3 -3
- package/lib/util/artifacts.js +189 -0
- package/lib/util/helpers.js +109 -0
- package/mcp-server.js +4 -0
- package/package.json +6 -3
- package/skills/cloud-run/SKILL.md +109 -0
- package/test/local/archive.test.js +88 -0
- package/test/local/clients.test.js +224 -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/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/workflows/deployment-workflows.test.js +172 -6
- package/tools/register-tools.js +19 -3
- /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
|
@@ -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(
|
|
@@ -0,0 +1,361 @@
|
|
|
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
|
+
import { test, describe, mock } from 'node:test';
|
|
18
|
+
import assert from 'node:assert/strict';
|
|
19
|
+
import esmock from 'esmock';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
import fs from 'fs';
|
|
22
|
+
|
|
23
|
+
describe('Artifact Utilities', () => {
|
|
24
|
+
const fsMock = {
|
|
25
|
+
existsSync: mock.fn(),
|
|
26
|
+
mkdirSync: mock.fn(),
|
|
27
|
+
chmodSync: mock.fn(),
|
|
28
|
+
readFileSync: mock.fn(),
|
|
29
|
+
unlinkSync: mock.fn(),
|
|
30
|
+
createWriteStream: mock.fn(),
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const cryptoMock = {
|
|
34
|
+
createHash: mock.fn(),
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const helpersMock = {
|
|
38
|
+
logAndProgress: mock.fn(),
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const clientsMock = {
|
|
42
|
+
getArtifactRegistryClient: mock.fn(),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const artifactParams = {
|
|
46
|
+
project: 'test-project',
|
|
47
|
+
location: 'test-location',
|
|
48
|
+
repository: 'test-repo',
|
|
49
|
+
artifactPath: 'test-path:1.0:bin',
|
|
50
|
+
displayName: 'Test Binary',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const sha256 =
|
|
54
|
+
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
|
|
55
|
+
|
|
56
|
+
test('isBinaryUpToDate returns true if hashes match', async () => {
|
|
57
|
+
fsMock.existsSync.mock.mockImplementation(() => true);
|
|
58
|
+
fsMock.readFileSync.mock.mockImplementation(() => 'local-content');
|
|
59
|
+
cryptoMock.createHash.mock.mockImplementation(() => ({
|
|
60
|
+
update: mock.fn(() => ({
|
|
61
|
+
digest: mock.fn(() => sha256),
|
|
62
|
+
})),
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
const artifactRegistryClientMock = {
|
|
66
|
+
filePath: mock.fn(() => 'resource-name'),
|
|
67
|
+
getFile: mock.fn(() => [
|
|
68
|
+
{
|
|
69
|
+
hashes: [
|
|
70
|
+
{
|
|
71
|
+
type: 'SHA256',
|
|
72
|
+
value: Buffer.from(sha256, 'hex').toString('base64'),
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
]),
|
|
77
|
+
};
|
|
78
|
+
clientsMock.getArtifactRegistryClient.mock.mockImplementation(
|
|
79
|
+
() => artifactRegistryClientMock
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const { isBinaryUpToDate } = await esmock('../../lib/util/artifacts.js', {
|
|
83
|
+
fs: fsMock,
|
|
84
|
+
crypto: cryptoMock,
|
|
85
|
+
'../../lib/util/helpers.js': helpersMock,
|
|
86
|
+
'../../lib/clients.js': clientsMock,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const result = await isBinaryUpToDate(
|
|
90
|
+
'/bin/path',
|
|
91
|
+
artifactParams,
|
|
92
|
+
'token',
|
|
93
|
+
mock.fn()
|
|
94
|
+
);
|
|
95
|
+
assert.strictEqual(result, true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('isBinaryUpToDate returns false and unlinks if hashes do not match', async () => {
|
|
99
|
+
fsMock.existsSync.mock.mockImplementation(() => true);
|
|
100
|
+
fsMock.readFileSync.mock.mockImplementation(() => 'local-content');
|
|
101
|
+
fsMock.unlinkSync.mock.resetCalls();
|
|
102
|
+
|
|
103
|
+
cryptoMock.createHash.mock.mockImplementation(() => ({
|
|
104
|
+
update: mock.fn(() => ({
|
|
105
|
+
digest: mock.fn(() => 'different-hash'),
|
|
106
|
+
})),
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
const artifactRegistryClientMock = {
|
|
110
|
+
filePath: mock.fn(() => 'resource-name'),
|
|
111
|
+
getFile: mock.fn(() => [
|
|
112
|
+
{
|
|
113
|
+
hashes: [
|
|
114
|
+
{
|
|
115
|
+
type: 'SHA256',
|
|
116
|
+
value: Buffer.from(sha256, 'hex').toString('base64'),
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
]),
|
|
121
|
+
};
|
|
122
|
+
clientsMock.getArtifactRegistryClient.mock.mockImplementation(
|
|
123
|
+
() => artifactRegistryClientMock
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const { isBinaryUpToDate } = await esmock('../../lib/util/artifacts.js', {
|
|
127
|
+
fs: fsMock,
|
|
128
|
+
crypto: cryptoMock,
|
|
129
|
+
'../../lib/util/helpers.js': helpersMock,
|
|
130
|
+
'../../lib/clients.js': clientsMock,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const result = await isBinaryUpToDate(
|
|
134
|
+
'/bin/path',
|
|
135
|
+
artifactParams,
|
|
136
|
+
'token',
|
|
137
|
+
mock.fn()
|
|
138
|
+
);
|
|
139
|
+
assert.strictEqual(result, false);
|
|
140
|
+
assert.strictEqual(fsMock.unlinkSync.mock.callCount(), 1);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test('ensureRepositoryDownloaded downloads binary if it does not exist', async () => {
|
|
144
|
+
fsMock.existsSync.mock.mockImplementation((p) => {
|
|
145
|
+
if (p === '/bin/path') return false;
|
|
146
|
+
return true;
|
|
147
|
+
});
|
|
148
|
+
fsMock.createWriteStream.mock.resetCalls();
|
|
149
|
+
|
|
150
|
+
const artifactRegistryClientMock = {
|
|
151
|
+
filePath: mock.fn(() => 'resource-name'),
|
|
152
|
+
getFile: mock.fn(() => [{ hashes: [] }]),
|
|
153
|
+
auth: {
|
|
154
|
+
request: mock.fn(() => ({
|
|
155
|
+
data: {
|
|
156
|
+
pipe: (dest) => {
|
|
157
|
+
setTimeout(() => dest.emit('finish'), 10);
|
|
158
|
+
return dest;
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
})),
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
clientsMock.getArtifactRegistryClient.mock.mockImplementation(
|
|
165
|
+
() => artifactRegistryClientMock
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const writeStreamMock = {
|
|
169
|
+
on: mock.fn(function (event, cb) {
|
|
170
|
+
if (event === 'finish') this.finishCb = cb;
|
|
171
|
+
return this;
|
|
172
|
+
}),
|
|
173
|
+
emit: mock.fn(function (event) {
|
|
174
|
+
if (event === 'finish' && this.finishCb) this.finishCb();
|
|
175
|
+
}),
|
|
176
|
+
};
|
|
177
|
+
fsMock.createWriteStream.mock.mockImplementation(() => writeStreamMock);
|
|
178
|
+
|
|
179
|
+
const { ensureRepositoryDownloaded } = await esmock(
|
|
180
|
+
'../../lib/util/artifacts.js',
|
|
181
|
+
{
|
|
182
|
+
fs: fsMock,
|
|
183
|
+
crypto: cryptoMock,
|
|
184
|
+
'../../lib/util/helpers.js': helpersMock,
|
|
185
|
+
'../../lib/clients.js': clientsMock,
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const result = await ensureRepositoryDownloaded(
|
|
190
|
+
'/bin/path',
|
|
191
|
+
artifactParams,
|
|
192
|
+
'token',
|
|
193
|
+
mock.fn()
|
|
194
|
+
);
|
|
195
|
+
assert.strictEqual(result, '/bin/path');
|
|
196
|
+
assert.strictEqual(fsMock.createWriteStream.mock.callCount(), 1);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('Utility Helpers', () => {
|
|
201
|
+
test('extractAccessToken correctly extracts token from Bearer header', async () => {
|
|
202
|
+
const { extractAccessToken } = await import('../../lib/util/helpers.js');
|
|
203
|
+
|
|
204
|
+
assert.equal(extractAccessToken('Bearer my-token'), 'my-token');
|
|
205
|
+
assert.equal(extractAccessToken(''), undefined);
|
|
206
|
+
assert.equal(extractAccessToken(null), undefined);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe('getProjectNumber', () => {
|
|
210
|
+
test('extracts project number from name in projects/NNNN format', async () => {
|
|
211
|
+
const getProjectMock = mock.fn(async () => [
|
|
212
|
+
{ name: 'projects/123456789' },
|
|
213
|
+
]);
|
|
214
|
+
const getProjectsClientMock = mock.fn(async () => ({
|
|
215
|
+
getProject: getProjectMock,
|
|
216
|
+
}));
|
|
217
|
+
|
|
218
|
+
const { getProjectNumber } = await esmock('../../lib/util/helpers.js', {
|
|
219
|
+
'../../lib/clients.js': {
|
|
220
|
+
getProjectsClient: getProjectsClientMock,
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const projectId = 'test-project';
|
|
225
|
+
const accessToken = 'test-token';
|
|
226
|
+
const result = await getProjectNumber(projectId, accessToken);
|
|
227
|
+
|
|
228
|
+
assert.equal(result, '123456789');
|
|
229
|
+
assert.equal(getProjectsClientMock.mock.callCount(), 1);
|
|
230
|
+
assert.equal(
|
|
231
|
+
getProjectMock.mock.calls[0].arguments[0].name,
|
|
232
|
+
'projects/test-project'
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('falls back to projectNumber property if name structure is unexpected', async () => {
|
|
237
|
+
const getProjectMock = mock.fn(async () => [
|
|
238
|
+
{ name: 'unexpected', projectNumber: '987654321' },
|
|
239
|
+
]);
|
|
240
|
+
const getProjectsClientMock = mock.fn(async () => ({
|
|
241
|
+
getProject: getProjectMock,
|
|
242
|
+
}));
|
|
243
|
+
|
|
244
|
+
const { getProjectNumber } = await esmock('../../lib/util/helpers.js', {
|
|
245
|
+
'../../lib/clients.js': {
|
|
246
|
+
getProjectsClient: getProjectsClientMock,
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const result = await getProjectNumber('test-project', 'test-token');
|
|
251
|
+
|
|
252
|
+
assert.equal(result, '987654321');
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('calculateSourceFingerprint', () => {
|
|
257
|
+
test('calculates SHA256 of directory content deterministically', async () => {
|
|
258
|
+
const fsMock = {
|
|
259
|
+
promises: {
|
|
260
|
+
readdir: mock.fn(async (dir) => {
|
|
261
|
+
if (dir === '/src') {
|
|
262
|
+
return [
|
|
263
|
+
{
|
|
264
|
+
name: 'b.txt',
|
|
265
|
+
isFile: () => true,
|
|
266
|
+
isDirectory: () => false,
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
name: 'a.txt',
|
|
270
|
+
isFile: () => true,
|
|
271
|
+
isDirectory: () => false,
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: 'subdir',
|
|
275
|
+
isFile: () => false,
|
|
276
|
+
isDirectory: () => true,
|
|
277
|
+
},
|
|
278
|
+
];
|
|
279
|
+
}
|
|
280
|
+
if (dir === '/src/subdir') {
|
|
281
|
+
return [
|
|
282
|
+
{
|
|
283
|
+
name: 'c.txt',
|
|
284
|
+
isFile: () => true,
|
|
285
|
+
isDirectory: () => false,
|
|
286
|
+
},
|
|
287
|
+
];
|
|
288
|
+
}
|
|
289
|
+
return [];
|
|
290
|
+
}),
|
|
291
|
+
readFile: mock.fn(
|
|
292
|
+
async (file) => `content of ${path.basename(file)}`
|
|
293
|
+
),
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const { calculateSourceFingerprint } = await esmock(
|
|
298
|
+
'../../lib/util/helpers.js',
|
|
299
|
+
{
|
|
300
|
+
'node:fs': { promises: fsMock.promises },
|
|
301
|
+
}
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
const result = await calculateSourceFingerprint('/src');
|
|
305
|
+
assert.ok(result);
|
|
306
|
+
assert.equal(typeof result, 'string');
|
|
307
|
+
assert.equal(result.length, 64); // SHA256 hex length
|
|
308
|
+
|
|
309
|
+
// Verify readdir calls (recursive)
|
|
310
|
+
assert.equal(fsMock.promises.readdir.mock.callCount(), 2);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe('sanitizeCloudRunServiceName', () => {
|
|
315
|
+
test('sanitizes various inputs correctly', async () => {
|
|
316
|
+
const { sanitizeCloudRunServiceName } =
|
|
317
|
+
await import('../../lib/util/helpers.js');
|
|
318
|
+
|
|
319
|
+
assert.equal(
|
|
320
|
+
sanitizeCloudRunServiceName('My-Service_Name'),
|
|
321
|
+
'my-service-name'
|
|
322
|
+
);
|
|
323
|
+
assert.equal(sanitizeCloudRunServiceName('123service'), 's-123service');
|
|
324
|
+
assert.equal(sanitizeCloudRunServiceName('-start-end-'), 'start-end');
|
|
325
|
+
assert.equal(
|
|
326
|
+
sanitizeCloudRunServiceName('a'.repeat(100)),
|
|
327
|
+
'a'.repeat(49)
|
|
328
|
+
);
|
|
329
|
+
assert.equal(
|
|
330
|
+
sanitizeCloudRunServiceName('Special!@#$%Chars'),
|
|
331
|
+
'special-chars'
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
describe('sanitizeLabelValue', () => {
|
|
337
|
+
test('sanitizes various inputs correctly', async () => {
|
|
338
|
+
const { sanitizeLabelValue } = await import('../../lib/util/helpers.js');
|
|
339
|
+
|
|
340
|
+
assert.equal(sanitizeLabelValue(''), '');
|
|
341
|
+
assert.equal(sanitizeLabelValue(null), '');
|
|
342
|
+
assert.equal(sanitizeLabelValue(undefined), '');
|
|
343
|
+
assert.equal(sanitizeLabelValue('valid-label'), 'valid-label');
|
|
344
|
+
assert.equal(sanitizeLabelValue('Upper-Case'), 'upper-case');
|
|
345
|
+
assert.equal(sanitizeLabelValue('invalid@char'), 'invalid-char');
|
|
346
|
+
assert.equal(
|
|
347
|
+
sanitizeLabelValue('1starts-with-number'),
|
|
348
|
+
'1starts-with-number'
|
|
349
|
+
);
|
|
350
|
+
assert.equal(
|
|
351
|
+
sanitizeLabelValue('-starts-with-dash'),
|
|
352
|
+
'-starts-with-dash'
|
|
353
|
+
);
|
|
354
|
+
assert.equal(sanitizeLabelValue('a'.repeat(70)), 'a'.repeat(63));
|
|
355
|
+
assert.equal(sanitizeLabelValue(12345), '12345');
|
|
356
|
+
assert.equal(sanitizeLabelValue(0), '0');
|
|
357
|
+
assert.equal(sanitizeLabelValue(true), 'true');
|
|
358
|
+
assert.equal(sanitizeLabelValue(false), 'false');
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
});
|
|
@@ -51,7 +51,7 @@ describe('Cloud Run Deployments', () => {
|
|
|
51
51
|
const config = {
|
|
52
52
|
serviceName: 'hello-from-image',
|
|
53
53
|
region: 'europe-west1',
|
|
54
|
-
files: ['example-sources-to-deploy/main.go'],
|
|
54
|
+
files: ['example-sources-to-deploy/golang/main.go'],
|
|
55
55
|
};
|
|
56
56
|
await assert.rejects(deploy(config), {
|
|
57
57
|
message: 'Error: projectId is required in the configuration object.',
|