Skip to content

Commit 4f83cda

Browse files
committed
resolveProjectId present (#106)
1 parent 40aee94 commit 4f83cda

10 files changed

+1003
-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,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,23 @@ 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+
// TODO. Just to show where this will be called. uncommented in next PR
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,119 @@
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+
let bucketName = '';
41+
42+
try {
43+
const projectId = await resolveProjectId();
44+
const region = await getExecutionRegion();
45+
46+
// if storageClient was not properly initialized OR the user chose to
47+
// use/create the default bucket but we were not able to resolve projectId
48+
// or cloudRegion
49+
if (!storageClient || (!storageLocation && (!projectId || !region)))
50+
return null;
51+
52+
bucketName = storageLocation
53+
? storageLocation.split('/')[0]
54+
: `gcm-${projectId}-synthetics-${region}`;
55+
56+
const bucket = storageClient.bucket(bucketName);
57+
const [bucketExists] = await bucket.exists();
58+
59+
if (bucketExists) {
60+
return bucket; // Bucket exists, return it
61+
} else if (!storageLocation) {
62+
// Create only if no location was provided
63+
const [newBucket] = await bucket.create({
64+
location: region,
65+
storageClass: 'STANDARD',
66+
});
67+
return newBucket;
68+
} else {
69+
// User-provided invalid location
70+
errors.push({
71+
error_type: 'InvalidStorageLocation',
72+
error_message: `Invalid storage_location: Bucket ${bucketName} does not exist.`,
73+
});
74+
}
75+
} catch (err) {
76+
if (err instanceof Error) process.stderr.write(err.message);
77+
errors.push({
78+
// General error handling
79+
error_type: storageLocation
80+
? 'StorageValidationError'
81+
: 'BucketCreationError',
82+
error_message: `Failed to ${
83+
storageLocation ? 'validate' : 'create'
84+
} bucket ${bucketName}. Please reference server logs for further information.`,
85+
});
86+
}
87+
88+
return null; // Return null if bucket retrieval or creation failed
89+
}
90+
91+
/**
92+
* Initializes a Google Cloud Storage client, if storage is selected. Handles
93+
* both expected and unexpected errors during initialization.
94+
*
95+
* @param errors - An array to accumulate potential errors of type `BaseError`.
96+
* @returns A Storage client object if successful, or null if errors occurred.
97+
*/
98+
export function createStorageClientIfStorageSelected(
99+
errors: BaseError[],
100+
storage_condition: BrokenLinksResultV1_BrokenLinkCheckerOptions_ScreenshotOptions_ScreenshotCondition
101+
): Storage | null {
102+
if (
103+
storage_condition ===
104+
BrokenLinksResultV1_BrokenLinkCheckerOptions_ScreenshotOptions_ScreenshotCondition.NONE
105+
)
106+
return null;
107+
108+
try {
109+
return new Storage();
110+
} catch (err) {
111+
if (err instanceof Error) process.stderr.write(err.message);
112+
errors.push({
113+
error_type: 'StorageClientInitializationError',
114+
error_message:
115+
'Failed to initialize Storage client. Please reference server logs for further information.',
116+
});
117+
return null;
118+
}
119+
}

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)