summaryrefslogtreecommitdiff
path: root/packages/web-util/src/tests/mock.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/web-util/src/tests/mock.ts')
-rw-r--r--packages/web-util/src/tests/mock.ts503
1 files changed, 503 insertions, 0 deletions
diff --git a/packages/web-util/src/tests/mock.ts b/packages/web-util/src/tests/mock.ts
new file mode 100644
index 000000000..d09e8b4a6
--- /dev/null
+++ b/packages/web-util/src/tests/mock.ts
@@ -0,0 +1,503 @@
+/*
+ This file is part of GNU Taler
+ (C) 2022 Taler Systems S.A.
+
+ GNU Taler is free software; you can redistribute it and/or modify it under the
+ terms of the GNU General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License along with
+ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+ */
+
+import { Logger } from "@gnu-taler/taler-util";
+
+type HttpMethod =
+ | "get"
+ | "GET"
+ | "delete"
+ | "DELETE"
+ | "head"
+ | "HEAD"
+ | "options"
+ | "OPTIONS"
+ | "post"
+ | "POST"
+ | "put"
+ | "PUT"
+ | "patch"
+ | "PATCH"
+ | "purge"
+ | "PURGE"
+ | "link"
+ | "LINK"
+ | "unlink"
+ | "UNLINK";
+
+/**
+ * @deprecated do not use it, it will be removed
+ */
+export type Query<Req, Res> = {
+ method: HttpMethod;
+ url: string;
+ code?: number;
+};
+
+type ExpectationValues = {
+ query: Query<any, any>;
+ auth?: string;
+ params?: {
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ request?: object;
+ qparam?: Record<string, string>;
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ response?: object;
+ };
+};
+
+type TestValues = {
+ currentExpectedQuery: ExpectationValues | undefined;
+ lastQuery: ExpectationValues | undefined;
+};
+
+const logger = new Logger("testing/mock.ts");
+
+type MockedResponse = {
+ queryMade: ExpectationValues;
+ expectedQuery?: ExpectationValues;
+};
+
+/**
+ * @deprecated do not use it, it will be removed
+ */
+export abstract class MockEnvironment {
+ expectations: Array<ExpectationValues> = [];
+ queriesMade: Array<ExpectationValues> = [];
+ index = 0;
+
+ debug: boolean;
+ constructor(debug: boolean) {
+ this.debug = debug;
+ this.saveRequestAndGetMockedResponse.bind(this);
+ }
+
+ public addRequestExpectation<
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ RequestType extends object,
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ ResponseType extends object,
+ >(
+ query: Query<RequestType, ResponseType>,
+ params: {
+ auth?: string;
+ request?: RequestType;
+ qparam?: any;
+ response?: ResponseType;
+ },
+ ): void {
+ const expected = { query, params, auth: params.auth };
+ this.expectations.push(expected);
+ if (this.debug) {
+ logger.info("saving query as expected", expected);
+ }
+ }
+
+ public saveRequestAndGetMockedResponse<
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ RequestType extends object,
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ ResponseType extends object,
+ >(
+ query: Query<RequestType, ResponseType>,
+ params: {
+ auth?: string;
+ request?: RequestType;
+ qparam?: any;
+ response?: ResponseType;
+ },
+ ): MockedResponse {
+ const queryMade = { query, params, auth: params.auth };
+ this.queriesMade.push(queryMade);
+ const expectedQuery = this.expectations[this.index];
+ if (!expectedQuery) {
+ if (this.debug) {
+ logger.info("unexpected query made", queryMade);
+ }
+ return { queryMade };
+ }
+
+ if (this.debug) {
+ logger.info("tracking query made", {
+ queryMade,
+ expectedQuery,
+ });
+ }
+ this.index++;
+ return { queryMade, expectedQuery };
+ }
+
+ public assertJustExpectedRequestWereMade(): AssertStatus {
+ let queryNumber = 0;
+
+ while (queryNumber < this.expectations.length) {
+ const r = this.assertNextRequest(queryNumber);
+ if (r.result !== "ok") return r;
+ queryNumber++;
+ }
+ return this.assertNoMoreRequestWereMade(queryNumber);
+ }
+
+ private getLastTestValues(idx: number): TestValues {
+ const currentExpectedQuery = this.expectations[idx];
+ const lastQuery = this.queriesMade[idx];
+
+ return { currentExpectedQuery, lastQuery };
+ }
+
+ private assertNoMoreRequestWereMade(idx: number): AssertStatus {
+ const { currentExpectedQuery, lastQuery } = this.getLastTestValues(idx);
+
+ if (lastQuery !== undefined) {
+ return {
+ result: "error-did-one-more",
+ made: lastQuery,
+ };
+ }
+ if (currentExpectedQuery !== undefined) {
+ return {
+ result: "error-did-one-less",
+ expected: currentExpectedQuery,
+ };
+ }
+
+ return {
+ result: "ok",
+ };
+ }
+
+ private assertNextRequest(index: number): AssertStatus {
+ const { currentExpectedQuery, lastQuery } = this.getLastTestValues(index);
+
+ if (!currentExpectedQuery) {
+ return {
+ result: "error-query-missing",
+ };
+ }
+
+ if (!lastQuery) {
+ return {
+ result: "error-did-one-less",
+ expected: currentExpectedQuery,
+ };
+ }
+
+ if (lastQuery.query.method) {
+ if (currentExpectedQuery.query.method !== lastQuery.query.method) {
+ return {
+ result: "error-difference",
+ diff: "method",
+ last: lastQuery.query.method,
+ expected: currentExpectedQuery.query.method,
+ index,
+ };
+ }
+ if (currentExpectedQuery.query.url !== lastQuery.query.url) {
+ return {
+ result: "error-difference",
+ diff: "url",
+ last: lastQuery.query.url,
+ expected: currentExpectedQuery.query.url,
+ index,
+ };
+ }
+ }
+ if (
+ !deepEquals(
+ currentExpectedQuery.params?.request,
+ lastQuery.params?.request,
+ )
+ ) {
+ return {
+ result: "error-difference",
+ diff: "query-body",
+ expected: currentExpectedQuery.params?.request,
+ last: lastQuery.params?.request,
+ index,
+ };
+ }
+ if (
+ !deepEquals(currentExpectedQuery.params?.qparam, lastQuery.params?.qparam)
+ ) {
+ return {
+ result: "error-difference",
+ diff: "query-params",
+ expected: currentExpectedQuery.params?.qparam,
+ last: lastQuery.params?.qparam,
+ index,
+ };
+ }
+ if (!deepEquals(currentExpectedQuery.auth, lastQuery.auth)) {
+ return {
+ result: "error-difference",
+ diff: "query-auth",
+ expected: currentExpectedQuery.auth,
+ last: lastQuery.auth,
+ index,
+ };
+ }
+
+ return {
+ result: "ok",
+ };
+ }
+}
+
+type AssertStatus =
+ | AssertOk
+ | AssertQueryNotMadeButExpected
+ | AssertQueryMadeButNotExpected
+ | AssertQueryMissing
+ | AssertExpectedQueryMethodMismatch
+ | AssertExpectedQueryUrlMismatch
+ | AssertExpectedQueryAuthMismatch
+ | AssertExpectedQueryBodyMismatch
+ | AssertExpectedQueryParamsMismatch;
+
+interface AssertOk {
+ result: "ok";
+}
+
+//trying to assert for a expected query but there is
+//no expected query in the queue
+interface AssertQueryMissing {
+ result: "error-query-missing";
+}
+
+//tested component did one more query that expected
+interface AssertQueryNotMadeButExpected {
+ result: "error-did-one-more";
+ made: ExpectationValues;
+}
+
+//tested component didn't make an expected query
+interface AssertQueryMadeButNotExpected {
+ result: "error-did-one-less";
+ expected: ExpectationValues;
+}
+
+interface AssertExpectedQueryMethodMismatch {
+ result: "error-difference";
+ diff: "method";
+ last: string;
+ expected: string;
+ index: number;
+}
+interface AssertExpectedQueryUrlMismatch {
+ result: "error-difference";
+ diff: "url";
+ last: string;
+ expected: string;
+ index: number;
+}
+interface AssertExpectedQueryAuthMismatch {
+ result: "error-difference";
+ diff: "query-auth";
+ last: string | undefined;
+ expected: string | undefined;
+ index: number;
+}
+interface AssertExpectedQueryBodyMismatch {
+ result: "error-difference";
+ diff: "query-body";
+ last: any;
+ expected: any;
+ index: number;
+}
+interface AssertExpectedQueryParamsMismatch {
+ result: "error-difference";
+ diff: "query-params";
+ last: any;
+ expected: any;
+ index: number;
+}
+
+/**
+ * helpers
+ *
+ */
+export type Tester = (a: any, b: any) => boolean | undefined;
+
+function deepEquals(
+ a: unknown,
+ b: unknown,
+ aStack: Array<unknown> = [],
+ bStack: Array<unknown> = [],
+): boolean {
+ //one if the element is null or undefined
+ if (a === null || b === null || b === undefined || a === undefined) {
+ return a === b;
+ }
+ //both are errors
+ if (a instanceof Error && b instanceof Error) {
+ return a.message == b.message;
+ }
+ //is the same object
+ if (Object.is(a, b)) {
+ return true;
+ }
+ //both the same class
+ const name = Object.prototype.toString.call(a);
+ if (name != Object.prototype.toString.call(b)) {
+ return false;
+ }
+ //
+ switch (name) {
+ case "[object Boolean]":
+ case "[object String]":
+ case "[object Number]":
+ if (typeof a !== typeof b) {
+ // One is a primitive, one a `new Primitive()`
+ return false;
+ } else if (typeof a !== "object" && typeof b !== "object") {
+ // both are proper primitives
+ return Object.is(a, b);
+ } else {
+ // both are `new Primitive()`s
+ return Object.is(a.valueOf(), b.valueOf());
+ }
+ case "[object Date]": {
+ const _a = a as Date;
+ const _b = b as Date;
+ return _a == _b;
+ }
+ case "[object RegExp]": {
+ const _a = a as RegExp;
+ const _b = b as RegExp;
+ return _a.source === _b.source && _a.flags === _b.flags;
+ }
+ case "[object Array]": {
+ const _a = a as Array<any>;
+ const _b = b as Array<any>;
+ if (_a.length !== _b.length) {
+ return false;
+ }
+ }
+ }
+ if (typeof a !== "object" || typeof b !== "object") {
+ return false;
+ }
+
+ if (
+ typeof a === "object" &&
+ typeof b === "object" &&
+ !Array.isArray(a) &&
+ !Array.isArray(b) &&
+ hasIterator(a) &&
+ hasIterator(b)
+ ) {
+ return iterable(a, b);
+ }
+
+ // Used to detect circular references.
+ let length = aStack.length;
+ while (length--) {
+ if (aStack[length] === a) {
+ return bStack[length] === b;
+ } else if (bStack[length] === b) {
+ return false;
+ }
+ }
+ aStack.push(a);
+ bStack.push(b);
+
+ const aKeys = allKeysFromObject(a);
+ const bKeys = allKeysFromObject(b);
+ let keySize = aKeys.length;
+
+ //same number of keys
+ if (bKeys.length !== keySize) {
+ return false;
+ }
+
+ let keyIterator: string;
+ while (keySize--) {
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ const _a = a as Record<string, object>;
+ // eslint-disable-next-line @typescript-eslint/ban-types
+ const _b = b as Record<string, object>;
+
+ keyIterator = aKeys[keySize];
+
+ const de = deepEquals(_a[keyIterator], _b[keyIterator], aStack, bStack);
+ if (!de) {
+ return false;
+ }
+ }
+
+ aStack.pop();
+ bStack.pop();
+
+ return true;
+}
+
+// eslint-disable-next-line @typescript-eslint/ban-types
+function allKeysFromObject(obj: object): Array<string> {
+ const keys = [];
+ for (const key in obj) {
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
+ keys.push(key);
+ }
+ }
+ return keys;
+}
+
+const IteratorSymbol = Symbol.iterator;
+
+function hasIterator(object: any): boolean {
+ return !!(object != null && object[IteratorSymbol]);
+}
+
+function iterable(
+ a: unknown,
+ b: unknown,
+ aStack: Array<unknown> = [],
+ bStack: Array<unknown> = [],
+): boolean {
+ if (a === null || b === null || b === undefined || a === undefined) {
+ return a === b;
+ }
+ if (a.constructor !== b.constructor) {
+ return false;
+ }
+ let length = aStack.length;
+ while (length--) {
+ if (aStack[length] === a) {
+ return bStack[length] === b;
+ }
+ }
+ aStack.push(a);
+ bStack.push(b);
+
+ const aIterator = (a as any)[IteratorSymbol]();
+ const bIterator = (b as any)[IteratorSymbol]();
+
+ const nextA = aIterator.next();
+ while (nextA.done) {
+ const nextB = bIterator.next();
+ if (nextB.done || !deepEquals(nextA.value, nextB.value)) {
+ return false;
+ }
+ }
+ if (!bIterator.next().done) {
+ return false;
+ }
+
+ // Remove the first value from the stack of traversed values.
+ aStack.pop();
+ bStack.pop();
+ return true;
+}