Skip to content

Commit 226b07c

Browse files
authored
take-screenshots (#107)
1 parent d6ad3e4 commit 226b07c

File tree

2 files changed

+251
-9
lines changed

2 files changed

+251
-9
lines changed

packages/synthetics-sdk-broken-links/src/storage_func.ts

+87-7
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,24 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15+
import * as path from 'path';
1516
import { Storage, Bucket } from '@google-cloud/storage';
1617
import {
1718
BaseError,
18-
BrokenLinksResultV1_BrokenLinkCheckerOptions_ScreenshotOptions_ScreenshotCondition,
19+
BrokenLinksResultV1_BrokenLinkCheckerOptions,
20+
BrokenLinksResultV1_BrokenLinkCheckerOptions_ScreenshotOptions_ScreenshotCondition as ApiScreenshotCondition,
1921
resolveProjectId,
2022
getExecutionRegion,
23+
BrokenLinksResultV1_SyntheticLinkResult_ScreenshotOutput as ApiScreenshotOutput,
2124
} from '@google-cloud/synthetics-sdk-api';
2225

26+
export interface StorageParameters {
27+
storageClient: Storage | null;
28+
bucket: Bucket | null;
29+
uptimeId: string;
30+
executionId: string;
31+
}
32+
2333
/**
2434
* Attempts to get an existing storage bucket if provided by the user OR
2535
* create/use a dedicated synthetics bucket.
@@ -97,13 +107,9 @@ export async function getOrCreateStorageBucket(
97107
*/
98108
export function createStorageClientIfStorageSelected(
99109
errors: BaseError[],
100-
storage_condition: BrokenLinksResultV1_BrokenLinkCheckerOptions_ScreenshotOptions_ScreenshotCondition
110+
storageCondition: ApiScreenshotCondition
101111
): Storage | null {
102-
if (
103-
storage_condition ===
104-
BrokenLinksResultV1_BrokenLinkCheckerOptions_ScreenshotOptions_ScreenshotCondition.NONE
105-
)
106-
return null;
112+
if (storageCondition === ApiScreenshotCondition.NONE) return null;
107113

108114
try {
109115
return new Storage();
@@ -117,3 +123,77 @@ export function createStorageClientIfStorageSelected(
117123
return null;
118124
}
119125
}
126+
127+
/**
128+
* Uploads a screenshot to Google Cloud Storage.
129+
*
130+
* @param screenshot - Base64-encoded screenshot data.
131+
* @param filename - Desired filename for the screenshot.
132+
* @param storageParams - An object containing storageClient and bucket.
133+
* @param options - Broken links checker options.
134+
* @returns An ApiScreenshotOutput object indicating success or a screenshot_error.
135+
*/
136+
export async function uploadScreenshotToGCS(
137+
screenshot: string,
138+
filename: string,
139+
storageParams: StorageParameters,
140+
options: BrokenLinksResultV1_BrokenLinkCheckerOptions
141+
): Promise<ApiScreenshotOutput> {
142+
const screenshot_output: ApiScreenshotOutput = {
143+
screenshot_file: '',
144+
screenshot_error: {} as BaseError,
145+
};
146+
try {
147+
// Early exit if storage is not properly configured
148+
if (!storageParams.storageClient || !storageParams.bucket) {
149+
return screenshot_output;
150+
}
151+
152+
// Construct the destination path within the bucket if given
153+
let writeDestination = options.screenshot_options!.storage_location
154+
? getFolderNameFromStorageLocation(
155+
options.screenshot_options!.storage_location
156+
)
157+
: '';
158+
159+
// Ensure writeDestination ends with a slash for proper path joining
160+
if (writeDestination && !writeDestination.endsWith('/')) {
161+
writeDestination += '/';
162+
}
163+
164+
writeDestination = path.join(
165+
writeDestination,
166+
storageParams.uptimeId,
167+
storageParams.executionId,
168+
filename
169+
);
170+
171+
// Upload to GCS
172+
await storageParams.bucket.file(writeDestination).save(screenshot, {
173+
contentType: 'image/png',
174+
});
175+
176+
screenshot_output.screenshot_file = writeDestination;
177+
} catch (err) {
178+
// Handle upload errors
179+
if (err instanceof Error) process.stderr.write(err.message);
180+
screenshot_output.screenshot_error = {
181+
error_type: 'StorageFileUploadError',
182+
error_message: `Failed to upload screenshot for ${filename}. Please reference server logs for further information.`,
183+
};
184+
}
185+
186+
return screenshot_output;
187+
}
188+
189+
// Helper function to extract folder name for a given storage location. If there
190+
// is no '/' present then the storageLocation is just a folder
191+
export function getFolderNameFromStorageLocation(
192+
storageLocation: string
193+
): string {
194+
const firstSlashIndex = storageLocation.indexOf('/');
195+
if (firstSlashIndex === -1) {
196+
return '';
197+
}
198+
return storageLocation.substring(firstSlashIndex + 1);
199+
}

packages/synthetics-sdk-broken-links/test/unit/storage_func.spec.ts

+164-2
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,16 @@
1414

1515
import { expect } from 'chai';
1616
import sinon from 'sinon';
17-
import { Storage, Bucket } from '@google-cloud/storage';
17+
import { Storage, Bucket, File } from '@google-cloud/storage';
1818
import * as sdkApi from '@google-cloud/synthetics-sdk-api';
1919
import {
2020
createStorageClientIfStorageSelected,
21+
getFolderNameFromStorageLocation,
2122
getOrCreateStorageBucket,
23+
StorageParameters,
24+
uploadScreenshotToGCS,
2225
} from '../../src/storage_func';
26+
import { BrokenLinksResultV1_BrokenLinkCheckerOptions } from '@google-cloud/synthetics-sdk-api';
2327
const proxyquire = require('proxyquire');
2428

2529
// global test vars
@@ -33,6 +37,7 @@ describe('GCM Synthetics Broken Links storage_func suite testing', () => {
3337
'@google-cloud/synthetics-sdk-api': {
3438
getExecutionRegion: () => 'test-region',
3539
resolveProjectId: () => 'test-project-id',
40+
getOrCreateStorageBucket: () => getOrCreateStorageBucket,
3641
},
3742
});
3843

@@ -94,10 +99,14 @@ describe('GCM Synthetics Broken Links storage_func suite testing', () => {
9499

95100
const result = await storageFunc.getOrCreateStorageBucket(
96101
storageClientStub,
97-
'',
102+
TEST_BUCKET_NAME + '/fake-folder',
98103
[]
99104
);
100105
expect(result).to.equal(bucketStub);
106+
sinon.assert.calledWithExactly(
107+
storageClientStub.bucket,
108+
TEST_BUCKET_NAME
109+
);
101110
sinon.assert.notCalled(bucketStub.create);
102111
});
103112

@@ -148,4 +157,157 @@ describe('GCM Synthetics Broken Links storage_func suite testing', () => {
148157
expect(result).to.be.an.instanceOf(Storage);
149158
});
150159
});
160+
161+
describe('uploadScreenshotToGCS', () => {
162+
let storageClientStub: sinon.SinonStubbedInstance<Storage>;
163+
let bucketStub: sinon.SinonStubbedInstance<Bucket>;
164+
165+
const screenshotData = 'encoded-image-data';
166+
const filename = 'test-screenshot.png';
167+
168+
beforeEach(() => {
169+
storageClientStub = sinon.createStubInstance(Storage);
170+
bucketStub = sinon.createStubInstance(Bucket);
171+
storageClientStub.bucket.returns(bucketStub);
172+
});
173+
174+
afterEach(() => {
175+
sinon.restore();
176+
});
177+
178+
it('should upload the screenshot and return the write_destination', async () => {
179+
const storageParams = {
180+
storageClient: storageClientStub,
181+
bucket: bucketStub,
182+
uptimeId: 'uptime123',
183+
executionId: 'exec456',
184+
};
185+
const options = {
186+
screenshot_options: { storage_location: 'bucket/folder1/folder2' },
187+
} as BrokenLinksResultV1_BrokenLinkCheckerOptions;
188+
const expectedWriteDestination =
189+
'folder1/folder2/uptime123/exec456/test-screenshot.png';
190+
191+
const successPartialFileMock: Partial<File> = {
192+
save: sinon.stub().resolves(),
193+
};
194+
bucketStub.file.returns(successPartialFileMock as File);
195+
196+
const result = await uploadScreenshotToGCS(
197+
screenshotData,
198+
filename,
199+
storageParams,
200+
options
201+
);
202+
203+
expect(result.screenshot_file).to.equal(expectedWriteDestination);
204+
expect(result.screenshot_error).to.deep.equal({});
205+
});
206+
207+
it('should handle GCS upload errors', async () => {
208+
const storageParams: StorageParameters = {
209+
storageClient: storageClientStub,
210+
bucket: bucketStub,
211+
uptimeId: '',
212+
executionId: '',
213+
};
214+
const options = {
215+
screenshot_options: {},
216+
} as BrokenLinksResultV1_BrokenLinkCheckerOptions;
217+
218+
const gcsError = new Error('Simulated GCS upload error');
219+
const failingPartialFileMock: Partial<File> = {
220+
save: sinon.stub().throws(gcsError),
221+
};
222+
bucketStub.file.returns(failingPartialFileMock as File);
223+
224+
const result = await uploadScreenshotToGCS(
225+
screenshotData,
226+
filename,
227+
storageParams,
228+
options
229+
);
230+
231+
expect(result.screenshot_file).to.equal('');
232+
expect(result.screenshot_error).to.deep.equal({
233+
error_type: 'StorageFileUploadError',
234+
error_message: `Failed to upload screenshot for ${filename}. Please reference server logs for further information.`,
235+
});
236+
});
237+
238+
describe('Invalid Storage Configuration', () => {
239+
const emptyScreenshotData = '';
240+
const emptyFilename = '';
241+
const emptyOptions = {} as BrokenLinksResultV1_BrokenLinkCheckerOptions;
242+
it('should return an empty result if storageClient is null', async () => {
243+
// Missing storageClient
244+
const storageParams = {
245+
storageClient: null,
246+
bucket: bucketStub,
247+
uptimeId: '',
248+
executionId: '',
249+
};
250+
251+
const result = await uploadScreenshotToGCS(
252+
emptyScreenshotData,
253+
emptyFilename,
254+
storageParams,
255+
emptyOptions
256+
);
257+
258+
expect(result).to.deep.equal({
259+
screenshot_file: '',
260+
screenshot_error: {},
261+
});
262+
});
263+
264+
it('should return an empty result if bucket is null', async () => {
265+
// Missing bucket
266+
const storageParams = {
267+
storageClient: storageClientStub,
268+
bucket: null,
269+
uptimeId: '',
270+
executionId: '',
271+
};
272+
273+
const result = await uploadScreenshotToGCS(
274+
emptyScreenshotData,
275+
emptyFilename,
276+
storageParams,
277+
emptyOptions
278+
);
279+
280+
expect(result).to.deep.equal({
281+
screenshot_file: '',
282+
screenshot_error: {},
283+
});
284+
});
285+
});
286+
});
287+
288+
describe('getFolderNameFromStorageLocation', () => {
289+
it('should extract folder name when storage location has a slash', () => {
290+
const storageLocation = 'some-bucket/folder1/folder2';
291+
const expectedFolderName = 'folder1/folder2';
292+
293+
const result = getFolderNameFromStorageLocation(storageLocation);
294+
expect(result).to.equal(expectedFolderName);
295+
});
296+
297+
it('should return an empty string if storage location has no slash', () => {
298+
const storageLocation = 'my-bucket';
299+
const expectedFolderName = '';
300+
301+
const result = getFolderNameFromStorageLocation(storageLocation);
302+
expect(result).to.equal(expectedFolderName);
303+
});
304+
305+
it('should return an empty string if given an empty string', () => {
306+
const storageLocation = '';
307+
const expectedFolderName = '';
308+
309+
const result = getFolderNameFromStorageLocation(storageLocation);
310+
expect(result).to.equal(expectedFolderName);
311+
});
312+
});
151313
});

0 commit comments

Comments
 (0)