Skip to content

Commit

Permalink
introduce indirection to be able to mock UrlFetchApp methods
Browse files Browse the repository at this point in the history
  • Loading branch information
diosmosis committed Oct 2, 2024
1 parent f282258 commit d35794f
Show file tree
Hide file tree
Showing 7 changed files with 58 additions and 28 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"create": "mkdir -p dist && clasp create --rootDir dist --title='Matomo Looker Studio Connector' --type=standalone && mv ./dist/.clasp.json .",
"push": "tsx ./scripts/push.ts",
"test:appscript": "jest --config=./jest.config.appscript.ts",
"test": "npm run test:appscript -- ./tests/appscript/api.spec.ts",
"test": "npm run test:appscript",
"test:download-artifacts": "tsx ./scripts/download-expected.ts",
"update-secrets-in-ci": "tsx ./scripts/update_ci_secrets.ts"
},
Expand Down
13 changes: 7 additions & 6 deletions src-test/callFunctionInTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,15 @@ export function callFunctionInTest(functionName: string, testName: string, ...pa
console.log(`calling ${functionName} in test "${testName}"`);

// there is no global object (like window) in apps script, so this is how we get a global function by name
const fn = eval(functionName);
const fn = (new Function(`return ${functionName};`))();
const result = fn(...params);
return JSON.stringify(result);
} catch (e) {
return JSON.stringify({ ...e, message: e.message, stack: e.stack });
return JSON.stringify({
...(typeof e === 'object' ? e : {}),
message: e.message || e,
stack: e.stack || 'no stack', // required so clasp.ts will recognize this object as an error
});
}
}

Expand All @@ -26,10 +30,7 @@ export function callFunctionInTestWithMockFixture(
testName: string,
...params: unknown[]
) {
const fixtureInstance = (ALL_FIXTURES[fixture.name])(...params);
console.log(fixture.name);
console.log(ALL_FIXTURES[fixture.name].toString());
console.log(UrlFetchApp.fetchAll.toString());
const fixtureInstance = (ALL_FIXTURES[fixture.name])(...fixture.params);
fixtureInstance.setUp();
try {
return callFunctionInTest(functionName, testName, ...params);
Expand Down
38 changes: 24 additions & 14 deletions src-test/mock-fixtures/urlfetchapp-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,37 @@
*/

export default function urlFetchAppMock(errorToThrow: string, throwAsString: boolean = false) {
const previousFetchAll = UrlFetchApp.fetchAll;
let isThrown = false;

return {
setUp() {
UrlFetchApp.fetchAll = function (...args) {
if (!isThrown) {
isThrown = true;
const mockUrlFetchApp = new Proxy(UrlFetchApp, {
get(target, prop) {
if (prop === 'fetchAll') {
return function (...args) {
if (!isThrown) {
isThrown = true;

if (throwAsString) {
throw errorToThrow;
if (throwAsString) {
throw errorToThrow;
} else {
throw new Error(errorToThrow);
}
} else {
throw new Error(errorToThrow);
return target[prop].call(this, ...args);
}
} else {
return previousFetchAll.call(this, ...args);
}
};
};
}
return target[prop];
}
});

return {
setUp() {
const getServices = (new Function('return getServices;'))();
getServices().UrlFetchApp = mockUrlFetchApp;
},
tearDown() {
UrlFetchApp.fetchAll = previousFetchAll;
const getServices = (new Function('return getServices;'))();
getServices().UrlFetchApp = UrlFetchApp;
},
};
}
14 changes: 9 additions & 5 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { getScriptElapsedTime } from './connector';
import { throwUnexpectedError, throwUserError } from './error';
import URLFetchRequest = GoogleAppsScript.URL_Fetch.URLFetchRequest;
import { debugLog, log, logError } from './log';
import { getServices } from './services';

const SCRIPT_RUNTIME_LIMIT = parseInt(env.SCRIPT_RUNTIME_LIMIT) || 0;
const API_REQUEST_RETRY_LIMIT_IN_SECS = parseInt(env.API_REQUEST_RETRY_LIMIT_IN_SECS) || 0;
Expand Down Expand Up @@ -117,9 +118,11 @@ function isUrlFetchErrorQuotaLimitReachedError(errorMessage: unknown) {

function isUrlFetchErrorProbablyTemporary(errorMessage: unknown) {
return typeof errorMessage === 'string'
&& !errorMessage.toLowerCase().includes('address unavailable')
&& !errorMessage.toLowerCase().includes('dns error')
&& !errorMessage.toLowerCase().includes('property fetchall on object urlfetchapp');
&& (
errorMessage.toLowerCase().includes('address unavailable')
|| errorMessage.toLowerCase().includes('dns error')
|| errorMessage.toLowerCase().includes('property fetchall on object urlfetchapp')
);
}

/**
Expand Down Expand Up @@ -212,17 +215,18 @@ export function fetchAll(requests: MatomoRequestParams[], options: ApiFetchOptio

let responses = [];
try {
responses = UrlFetchApp.fetchAll(urlsToFetch);
responses = getServices().UrlFetchApp.fetchAll(urlsToFetch);
} catch (e) {
const errorMessage = e.message || e;
console.log(errorMessage);

// throw user friendly error messages if possible
if (isUrlFetchErrorQuotaLimitReachedError(errorMessage)) {
throwUserError('The "urlfetch" daily quota for your account has been reached, further requests for today may not work. See https://developers.google.com/apps-script/guides/services/quotas for more information.');
}

// only rethrow for unknown errors, otherwise retry
if (isUrlFetchErrorProbablyTemporary(errorMessage)) {
if (!isUrlFetchErrorProbablyTemporary(errorMessage)) {
throw e;
}
}
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export { extractBasicAuthFromUrl, fetchAll, isApiErrorNonRandom } from './api';
export * from './auth';
export * from './config';
export * from './data';
export * from './services';
14 changes: 14 additions & 0 deletions src/services.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Matomo - free/libre analytics platform
*
* @link https://matomo.org
* @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
*/

const SERVICES = {
UrlFetchApp,
};

export function getServices() {
return SERVICES;
}
4 changes: 2 additions & 2 deletions tests/appscript/api.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ describe('api', () => {
expect(requestCount).toEqual(1);
});

it.only('should abort when UrlFetchApp throws an unknown error', async () => {
it('should abort when UrlFetchApp throws an unknown error', async () => {
if (!process.env.USE_LOCALTUNNEL) {
console.log('*** SKIPPING TEST ***');
return;
Expand Down Expand Up @@ -326,7 +326,7 @@ describe('api', () => {
throwOnFailedRequest: true,
},
);
}).rejects.toEqual(errorMessage);
}).rejects.toHaveProperty('message', errorMessage);
});

const TEMPORARY_ERRORS = [
Expand Down

0 comments on commit d35794f

Please sign in to comment.