Skip to content

Commit efb7416

Browse files
committed
resolveProjectId present
1 parent 30f2693 commit efb7416

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

+24-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
// limitations under the License.
1414

1515
import puppeteer, { Browser, Page } from 'puppeteer';
16+
import { Bucket } from '@google-cloud/storage';
1617
import {
18+
BaseError,
1719
BrokenLinksResultV1_BrokenLinkCheckerOptions,
1820
BrokenLinksResultV1_SyntheticLinkResult,
1921
instantiateMetadata,
@@ -36,6 +38,10 @@ import {
3638
openNewPage,
3739
} from './navigation_func';
3840
import { processOptions } from './options_func';
41+
import {
42+
createStorageClientIfStorageSelected,
43+
getOrCreateStorageBucket,
44+
} from './storage_func';
3945

4046
export interface BrokenLinkCheckerOptions {
4147
origin_uri: string;
@@ -103,6 +109,22 @@ export async function runBrokenLinks(
103109
const adjusted_synthetic_timeout_millis =
104110
options.total_synthetic_timeout_millis! - 7000;
105111

112+
const errors: BaseError[] = [];
113+
114+
// Initialize Storage Client with Error Handling. Set to `null` if
115+
// screenshot_condition is 'None'
116+
const storageClient = createStorageClientIfStorageSelected(
117+
errors,
118+
options.screenshot_options!.screenshot_condition
119+
);
120+
121+
// Bucket Validation
122+
const bucket: Bucket | null = await getOrCreateStorageBucket(
123+
storageClient,
124+
options.screenshot_options!.storage_location,
125+
errors
126+
);
127+
106128
// Create Promise and variables used to set and resolve the time limit
107129
// imposed by `adjusted_synthetic_timeout`
108130
const [timeLimitPromise, timeLimitTimeout, timeLimitresolver] =
@@ -161,7 +183,8 @@ export async function runBrokenLinks(
161183
startTime,
162184
runtime_metadata,
163185
options,
164-
followed_links
186+
followed_links,
187+
errors
165188
);
166189
} catch (err) {
167190
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,129 @@
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 if provided by the user OR
25+
* create/use a dedicated synthetics bucket.
26+
* Handles various errors gracefully, providing structured details in the `errors` array.
27+
*
28+
* @param storageClient - An initialized Storage client from the
29+
* '@google-cloud/storage' SDK.
30+
* @param storageLocation - The desired storage location (bucket or folder)
31+
* provided by the user. Can be empty.
32+
* @param errors - An array to accumulate potential errors of type `BaseError`.
33+
* @returns A 'Bucket' object if successful, or null if errors occurred.
34+
*/
35+
export async function getOrCreateStorageBucket(
36+
storageClient: Storage | null,
37+
storageLocation: string,
38+
errors: BaseError[]
39+
): Promise<Bucket | null> {
40+
// if storage client was not properly initialized or storage no selected
41+
if (!storageClient) return null;
42+
43+
// No storage_location given, create one
44+
if (storageLocation.length === 0) {
45+
const projectId = await resolveProjectId();
46+
const region = await getExecutionRegion();
47+
const bucketName = `gcm-${projectId}-synthetics-${region}}`;
48+
49+
try {
50+
const bucket = storageClient.bucket(bucketName);
51+
const [bucketExists] = await bucket.exists();
52+
53+
if (bucketExists) {
54+
return bucket;
55+
} else {
56+
// Bucket doesn't exist, let's create it
57+
const [newBucket] = await bucket.create({
58+
location: region, // Set bucket location
59+
storageClass: 'STANDARD', // Standard storage class
60+
});
61+
return newBucket;
62+
}
63+
} catch (err) {
64+
if (err instanceof Error) process.stderr.write(err.message);
65+
errors.push({
66+
error_type: 'BucketCreationError',
67+
error_message: `Failed to create bucket ${bucketName}. Please reference server logs for further information.`,
68+
});
69+
return null;
70+
}
71+
} else {
72+
// User provided storage location
73+
const bucketName = storageLocation.split('/')[0]; // Only first part needed for validation
74+
try {
75+
const bucket = storageClient.bucket(bucketName);
76+
const [bucketExists] = await bucket.exists();
77+
78+
if (bucketExists) {
79+
// Valid bucket
80+
return bucket;
81+
} else {
82+
// Invalid bucket
83+
errors.push({
84+
error_type: 'InvalidStorageLocation',
85+
error_message: `Invalid storage_location: Bucket ${bucketName} does not exist.`,
86+
});
87+
return null;
88+
}
89+
} catch (err) {
90+
// Catch other (non-404) errors
91+
if (err instanceof Error) process.stderr.write(err.message);
92+
errors.push({
93+
error_type: 'StorageValidationError',
94+
error_message: `Error validating storage location: ${bucketName}. Please reference server logs for further information.`,
95+
});
96+
return null;
97+
}
98+
}
99+
}
100+
101+
/**
102+
* Initializes a Google Cloud Storage client, if storage is selected. Handles
103+
* both expected and unexpected errors during initialization.
104+
*
105+
* @param errors - An array to accumulate potential errors of type `BaseError`.
106+
* @returns A Storage client object if successful, or null if errors occurred.
107+
*/
108+
export function createStorageClientIfStorageSelected(
109+
errors: BaseError[],
110+
storage_condition: BrokenLinksResultV1_BrokenLinkCheckerOptions_ScreenshotOptions_ScreenshotCondition
111+
): Storage | null {
112+
if (
113+
storage_condition ===
114+
BrokenLinksResultV1_BrokenLinkCheckerOptions_ScreenshotOptions_ScreenshotCondition.NONE
115+
)
116+
return null;
117+
118+
try {
119+
return new Storage();
120+
} catch (err) {
121+
if (err instanceof Error) process.stderr.write(err.message);
122+
errors.push({
123+
error_type: 'StorageClientInitializationError',
124+
error_message:
125+
'Failed to initialize Storage client. Please reference server logs for further information.',
126+
});
127+
return null;
128+
}
129+
}

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)