Skip to content

Commit 0030f53

Browse files
committed
resolveProjectId present
1 parent 30f2693 commit 0030f53

10 files changed

+981
-94
lines changed

package-lock.json

+684-80
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Binary file not shown.

packages/synthetics-sdk-broken-links/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"chai": "^4.3.7",
3333
"chai-exclude": "^2.1.0",
3434
"express": "^4.18.2",
35+
"proxyquire": "^2.1.3",
3536
"sinon": "^16.1.1",
3637
"supertest": "^6.3.3",
3738
"synthetics-sdk-broken-links": "file:./"
@@ -40,6 +41,7 @@
4041
"node": ">=18"
4142
},
4243
"dependencies": {
44+
"@google-cloud/storage": "^7.7.0",
4345
"@google-cloud/synthetics-sdk-api": "google-cloud-synthetics-sdk-api-0.5.1.tgz",
4446
"puppeteer": "21.3.6"
4547
}

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

+25-1
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@
1313
// limitations under the License.
1414

1515
import puppeteer, { Browser, Page } from 'puppeteer';
16+
import { Bucket, Storage } from '@google-cloud/storage';
1617
import {
18+
BaseError,
1719
BrokenLinksResultV1_BrokenLinkCheckerOptions,
20+
BrokenLinksResultV1_BrokenLinkCheckerOptions_ScreenshotOptions_ScreenshotCondition,
1821
BrokenLinksResultV1_SyntheticLinkResult,
1922
instantiateMetadata,
2023
getRuntimeMetadata,
@@ -36,6 +39,10 @@ import {
3639
openNewPage,
3740
} from './navigation_func';
3841
import { processOptions } from './options_func';
42+
import {
43+
createStorageClientIfStorageSelected,
44+
getOrCreateStorageBucket,
45+
} from './storage_func';
3946

4047
export interface BrokenLinkCheckerOptions {
4148
origin_uri: string;
@@ -103,6 +110,22 @@ export async function runBrokenLinks(
103110
const adjusted_synthetic_timeout_millis =
104111
options.total_synthetic_timeout_millis! - 7000;
105112

113+
const errors: BaseError[] = [];
114+
115+
// Initialize Storage Client with Error Handling. Set to `null` if
116+
// screenshot_condition is 'None'
117+
const storageClient = createStorageClientIfStorageSelected(
118+
errors,
119+
options.screenshot_options!.screenshot_condition
120+
);
121+
122+
// Bucket Validation
123+
const bucket: Bucket | null = await getOrCreateStorageBucket(
124+
storageClient,
125+
options.screenshot_options!.storage_location,
126+
errors
127+
);
128+
106129
// Create Promise and variables used to set and resolve the time limit
107130
// imposed by `adjusted_synthetic_timeout`
108131
const [timeLimitPromise, timeLimitTimeout, timeLimitresolver] =
@@ -161,7 +184,8 @@ export async function runBrokenLinks(
161184
startTime,
162185
runtime_metadata,
163186
options,
164-
followed_links
187+
followed_links,
188+
errors
165189
);
166190
} catch (err) {
167191
const errorMessage =

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

+4-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import { HTTPResponse } from 'puppeteer';
1616
import {
17+
BaseError,
1718
BrokenLinksResultV1,
1819
BrokenLinksResultV1_BrokenLinkCheckerOptions,
1920
BrokenLinksResultV1_BrokenLinkCheckerOptions_LinkOrder,
@@ -218,12 +219,14 @@ export function createSyntheticResult(
218219
start_time: string,
219220
runtime_metadata: { [key: string]: string },
220221
options: BrokenLinksResultV1_BrokenLinkCheckerOptions,
221-
followed_links: BrokenLinksResultV1_SyntheticLinkResult[]
222+
followed_links: BrokenLinksResultV1_SyntheticLinkResult[],
223+
errors: BaseError[]
222224
): SyntheticResult {
223225
// Create BrokenLinksResultV1 by parsing followed links and setting options
224226
const broken_links_result: BrokenLinksResultV1 =
225227
parseFollowedLinks(followed_links);
226228
broken_links_result.options = options;
229+
broken_links_result.errors = errors;
227230

228231
// Create SyntheticResult object
229232
const synthetic_result: SyntheticResult = {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import { Storage, Bucket } from '@google-cloud/storage';
16+
import {
17+
BaseError,
18+
BrokenLinksResultV1_BrokenLinkCheckerOptions_ScreenshotOptions_ScreenshotCondition,
19+
resolveProjectId,
20+
getExecutionRegion,
21+
} from '@google-cloud/synthetics-sdk-api';
22+
23+
/**
24+
* Attempts to get an existing storage bucket or creates a new one.
25+
* Handles various errors gracefully, providing structured details in the `errors` array.
26+
*
27+
* @param storageClient - An initialized Storage client from the
28+
* '@google-cloud/storage' SDK.
29+
* @param storageLocation - The desired storage location (bucket or folder)
30+
* provided by the user. Can be empty.
31+
* @param errors - An array to accumulate potential errors of type `BaseError`.
32+
* @returns A 'Bucket' object if successful, or null if errors occurred.
33+
*/
34+
export async function getOrCreateStorageBucket(
35+
storageClient: Storage | null,
36+
storageLocation: string,
37+
errors: BaseError[]
38+
): Promise<Bucket | null> {
39+
// if storage client was not properly initialized or storage no selected
40+
if (!storageClient) return null;
41+
42+
// No storage_location given, create one
43+
if (storageLocation.length === 0) {
44+
const projectId = await resolveProjectId();
45+
const region = await getExecutionRegion();
46+
const bucketName = `gcm-${projectId}-synthetics-${region}}`;
47+
48+
try {
49+
const bucket = storageClient.bucket(bucketName);
50+
const [bucketExists] = await bucket.exists();
51+
52+
if (bucketExists) {
53+
return bucket;
54+
} else {
55+
// Bucket doesn't exist, let's create it
56+
const [newBucket] = await bucket.create({
57+
location: region, // Set bucket location
58+
storageClass: 'STANDARD', // Standard storage class
59+
});
60+
return newBucket;
61+
}
62+
} catch (err) {
63+
if (err instanceof Error) process.stderr.write(err.message);
64+
errors.push({
65+
error_type: 'BucketCreationError',
66+
error_message: `Failed to create bucket ${bucketName}. Please reference server logs for further information.`,
67+
});
68+
return null;
69+
}
70+
} else {
71+
// User provided storage location
72+
const bucketName = storageLocation.split('/')[0]; // Only first part needed for validation
73+
try {
74+
const bucket = storageClient.bucket(bucketName);
75+
const [bucketExists] = await bucket.exists();
76+
77+
if (bucketExists) {
78+
// Valid bucket
79+
return bucket;
80+
} else {
81+
// Invalid bucket
82+
errors.push({
83+
error_type: 'InvalidStorageLocation',
84+
error_message: `Invalid storage_location: Bucket ${bucketName} does not exist.`,
85+
});
86+
return null;
87+
}
88+
} catch (err) {
89+
// Catch other (non-404) errors
90+
if (err instanceof Error) process.stderr.write(err.message);
91+
errors.push({
92+
error_type: 'StorageValidationError',
93+
error_message: `Error validating storage location: ${bucketName}. Please reference server logs for further information.`,
94+
});
95+
return null;
96+
}
97+
}
98+
}
99+
100+
/**
101+
* Initializes a Google Cloud Storage client, if storage is selected. Handles
102+
* both expected and unexpected errors during initialization.
103+
*
104+
* @param errors - An array to accumulate potential errors of type `BaseError`.
105+
* @returns A Storage client object if successful, or null if errors occurred.
106+
*/
107+
export function createStorageClientIfStorageSelected(
108+
errors: BaseError[],
109+
storage_condition: BrokenLinksResultV1_BrokenLinkCheckerOptions_ScreenshotOptions_ScreenshotCondition
110+
): Storage | null {
111+
if (
112+
storage_condition ===
113+
BrokenLinksResultV1_BrokenLinkCheckerOptions_ScreenshotOptions_ScreenshotCondition.NONE
114+
)
115+
return null;
116+
117+
try {
118+
return new Storage();
119+
} catch (err) {
120+
if (err instanceof Error) process.stderr.write(err.message);
121+
errors.push({
122+
error_type: 'StorageClientInitializationError',
123+
error_message:
124+
'Failed to initialize Storage client. Please reference server logs for further information.',
125+
});
126+
return null;
127+
}
128+
}

packages/synthetics-sdk-broken-links/test/integration/integration.spec.ts

+10-9
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,11 @@ describe('CloudFunctionV2 Running Broken Link Synthetics', async () => {
4040
storage_location: '',
4141
};
4242

43-
const default_screenshot_output : BrokenLinksResultV1_SyntheticLinkResult_ScreenshotOutput = {
43+
const default_screenshot_output: BrokenLinksResultV1_SyntheticLinkResult_ScreenshotOutput =
44+
{
4445
screenshot_file: '',
4546
screenshot_error: {} as BaseError,
46-
}
47+
};
4748

4849
it('Handles error when trying to visit page that does not exist', async () => {
4950
const server = getTestServer('BrokenLinksPageDoesNotExist');
@@ -94,7 +95,7 @@ describe('CloudFunctionV2 Running Broken Link Synthetics', async () => {
9495
link_start_time: 'NA',
9596
link_end_time: 'NA',
9697
is_origin: true,
97-
screenshot_output : default_screenshot_output
98+
screenshot_output: default_screenshot_output,
9899
});
99100

100101
expect(followed_links).to.deep.equal([]);
@@ -171,7 +172,7 @@ describe('CloudFunctionV2 Running Broken Link Synthetics', async () => {
171172
link_start_time: 'NA',
172173
link_end_time: 'NA',
173174
is_origin: true,
174-
screenshot_output : default_screenshot_output
175+
screenshot_output: default_screenshot_output,
175176
});
176177

177178
expect(followed_links).to.deep.equal([]);
@@ -280,7 +281,7 @@ describe('CloudFunctionV2 Running Broken Link Synthetics', async () => {
280281
link_start_time: 'NA',
281282
link_end_time: 'NA',
282283
is_origin: true,
283-
screenshot_output : default_screenshot_output
284+
screenshot_output: default_screenshot_output,
284285
});
285286

286287
const sorted_followed_links = followed_links?.sort((a, b) =>
@@ -309,7 +310,7 @@ describe('CloudFunctionV2 Running Broken Link Synthetics', async () => {
309310
link_start_time: 'NA',
310311
link_end_time: 'NA',
311312
is_origin: false,
312-
screenshot_output : default_screenshot_output
313+
screenshot_output: default_screenshot_output,
313314
},
314315
{
315316
link_passed: false,
@@ -323,7 +324,7 @@ describe('CloudFunctionV2 Running Broken Link Synthetics', async () => {
323324
link_start_time: 'NA',
324325
link_end_time: 'NA',
325326
is_origin: false,
326-
screenshot_output : default_screenshot_output
327+
screenshot_output: default_screenshot_output,
327328
},
328329
]);
329330

@@ -407,7 +408,7 @@ describe('CloudFunctionV2 Running Broken Link Synthetics', async () => {
407408
link_start_time: 'NA',
408409
link_end_time: 'NA',
409410
is_origin: true,
410-
screenshot_output : default_screenshot_output
411+
screenshot_output: default_screenshot_output,
411412
});
412413

413414
expect(followed_links)
@@ -426,7 +427,7 @@ describe('CloudFunctionV2 Running Broken Link Synthetics', async () => {
426427
link_start_time: 'NA',
427428
link_end_time: 'NA',
428429
is_origin: false,
429-
screenshot_output : default_screenshot_output
430+
screenshot_output: default_screenshot_output,
430431
},
431432
]);
432433

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

+7-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
import { expect } from 'chai';
1616
import {
17+
BaseError,
1718
BrokenLinksResultV1_BrokenLinkCheckerOptions,
1819
BrokenLinksResultV1_BrokenLinkCheckerOptions_LinkOrder,
1920
BrokenLinksResultV1_SyntheticLinkResult,
@@ -48,6 +49,9 @@ describe('GCM Synthetics Broken Links Utilies', async () => {
4849
const status_class_5xx: ResponseStatusCode = {
4950
status_class: ResponseStatusCode_StatusClass.STATUS_CLASS_5XX,
5051
};
52+
const default_errors: BaseError[] = [
53+
{ error_type: 'fake-error-type', error_message: 'fake-error-message' },
54+
];
5155

5256
it('checkStatusPassing returns correctly when passed a number as ResponseStatusCode', () => {
5357
// expecting success
@@ -120,7 +124,8 @@ describe('GCM Synthetics Broken Links Utilies', async () => {
120124
start_time,
121125
runtime_metadata,
122126
options,
123-
all_links
127+
all_links,
128+
default_errors
124129
);
125130

126131
// BrokenLinkResultV1 expectations (testing `parseFollowedLinks`)
@@ -139,7 +144,7 @@ describe('GCM Synthetics Broken Links Utilies', async () => {
139144
origin_link_result: origin_link,
140145
followed_link_results: followed_links,
141146
execution_data_storage_path: '',
142-
errors: []
147+
errors: default_errors,
143148
});
144149

145150
expect(

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import {
2828
validateInputOptions,
2929
} from '../../src/options_func';
3030

31-
describe('GCM Synthetics Broken Links options_func suite testing', () => {
31+
describe('GCM Synthetics Broken Links options_func suite testing', () => {
3232
const status_value_304: ResponseStatusCode = { status_value: 304 };
3333
const status_class_2xx: ResponseStatusCode = {
3434
status_class: ResponseStatusCode_StatusClass.STATUS_CLASS_2XX,

0 commit comments

Comments
 (0)