gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-wallet-core] 01/13: better swr mocks


From: gnunet
Subject: [taler-wallet-core] 01/13: better swr mocks
Date: Fri, 21 Apr 2023 16:06:58 +0200

This is an automated email from the git hooks/post-receive script.

sebasjm pushed a commit to branch master
in repository wallet-core.

commit b9c24772f52387c123f3529f8db0754b6f500d74
Author: Sebastian <sebasjm@gmail.com>
AuthorDate: Thu Apr 20 15:32:36 2023 -0300

    better swr mocks
---
 packages/web-util/src/index.browser.ts           |   2 +
 packages/web-util/src/tests/mock.ts              |  16 +-
 packages/web-util/src/tests/swr.ts               | 102 ++++++-----
 packages/web-util/src/utils/http-impl.browser.ts | 203 ++++++++++++++++++++++
 packages/web-util/src/utils/http-impl.sw.ts      | 205 +++++++++++++++++++++++
 packages/web-util/src/utils/request.ts           |  44 +++--
 6 files changed, 512 insertions(+), 60 deletions(-)

diff --git a/packages/web-util/src/index.browser.ts 
b/packages/web-util/src/index.browser.ts
index b1df2f96e..c7ba8435f 100644
--- a/packages/web-util/src/index.browser.ts
+++ b/packages/web-util/src/index.browser.ts
@@ -1,5 +1,7 @@
 export * from "./hooks/index.js";
 export * from "./utils/request.js";
+export * from "./utils/http-impl.browser.js";
+export * from "./utils/http-impl.sw.js";
 export * from "./utils/observable.js";
 export * from "./context/index.js";
 export * from "./components/index.js";
diff --git a/packages/web-util/src/tests/mock.ts 
b/packages/web-util/src/tests/mock.ts
index c01e66849..f4eb0e7aa 100644
--- a/packages/web-util/src/tests/mock.ts
+++ b/packages/web-util/src/tests/mock.ts
@@ -15,6 +15,7 @@
  */
 
 import { Logger } from "@gnu-taler/taler-util";
+import { deprecate } from "util";
 
 type HttpMethod =
   | "get"
@@ -63,6 +64,11 @@ type TestValues = {
 
 const logger = new Logger("testing/mock.ts");
 
+type MockedResponse = {
+  queryMade: ExpectationValues;
+  expectedQuery?: ExpectationValues;
+};
+
 export abstract class MockEnvironment {
   expectations: Array<ExpectationValues> = [];
   queriesMade: Array<ExpectationValues> = [];
@@ -108,7 +114,7 @@ export abstract class MockEnvironment {
       qparam?: any;
       response?: ResponseType;
     },
-  ): { status: number; payload: ResponseType } | undefined {
+  ): MockedResponse {
     const queryMade = { query, params, auth: params.auth };
     this.queriesMade.push(queryMade);
     const expectedQuery = this.expectations[this.index];
@@ -116,11 +122,9 @@ export abstract class MockEnvironment {
       if (this.debug) {
         logger.info("unexpected query made", queryMade);
       }
-      return undefined;
+      return { queryMade };
     }
-    const responseCode = this.expectations[this.index].query.code ?? 200;
-    const mockedResponse = this.expectations[this.index].params
-      ?.response as ResponseType;
+
     if (this.debug) {
       logger.info("tracking query made", {
         queryMade,
@@ -128,7 +132,7 @@ export abstract class MockEnvironment {
       });
     }
     this.index++;
-    return { status: responseCode, payload: mockedResponse };
+    return { queryMade, expectedQuery };
   }
 
   public assertJustExpectedRequestWereMade(): AssertStatus {
diff --git a/packages/web-util/src/tests/swr.ts 
b/packages/web-util/src/tests/swr.ts
index 62a35f83d..903cd48d8 100644
--- a/packages/web-util/src/tests/swr.ts
+++ b/packages/web-util/src/tests/swr.ts
@@ -17,12 +17,17 @@
 import { ComponentChildren, FunctionalComponent, h, VNode } from "preact";
 import { MockEnvironment } from "./mock.js";
 import { SWRConfig } from "swr";
+import * as swr__internal from "swr/_internal";
+import { Logger } from "@gnu-taler/taler-util";
+import { buildRequestFailed, RequestError } from "../index.browser.js";
+
+const logger = new Logger("tests/swr.ts");
 
 /**
  * Helper for hook that use SWR inside.
- * 
+ *
  * buildTestingContext() will return a testing context
- * 
+ *
  */
 export class SwrMockEnvironment extends MockEnvironment {
   constructor(debug = false) {
@@ -32,47 +37,68 @@ export class SwrMockEnvironment extends MockEnvironment {
   public buildTestingContext(): FunctionalComponent<{
     children: ComponentChildren;
   }> {
-    const __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE = 
this.saveRequestAndGetMockedResponse.bind(this);
+    const __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE =
+      this.saveRequestAndGetMockedResponse.bind(this);
+
+    function testingFetcher(params: any): any {
+      const url = JSON.stringify(params);
+      const mocked = __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE<any, any>(
+        {
+          method: "get",
+          url,
+        },
+        {},
+      );
+
+      //unexpected query
+      if (!mocked.expectedQuery) return undefined;
+      const status = mocked.expectedQuery.query.code ?? 200;
+      const requestPayload = mocked.expectedQuery.params?.request;
+      const responsePayload = mocked.expectedQuery.params?.response;
+      //simulated error
+      if (status >= 400) {
+        const error = buildRequestFailed(
+          url,
+          JSON.stringify(responsePayload),
+          status,
+          requestPayload,
+        );
+        //example error handling from 
https://swr.vercel.app/docs/error-handling
+        throw new RequestError(error);
+      }
+      return responsePayload;
+    }
+
+    const value: Partial<swr__internal.PublicConfiguration> & {
+      provider: () => Map<any, any>;
+    } = {
+      use: [
+        (useSWRNext) => {
+          return (key, fetcher, config) => {
+            //prevent the request
+            //use the testing fetcher instead
+            return useSWRNext(key, testingFetcher, config);
+          };
+        },
+      ],
+      fetcher: testingFetcher,
+      //These options are set for ending the test faster
+      //otherwise SWR will create timeouts that will live after the test 
finished
+      loadingTimeout: 0,
+      dedupingInterval: 0,
+      shouldRetryOnError: false,
+      errorRetryInterval: 0,
+      errorRetryCount: 0,
+      //clean cache for every test
+      provider: () => new Map(),
+    };
+
     return function TestingContext({
       children,
     }: {
       children: ComponentChildren;
     }): VNode {
-      return h(
-        SWRConfig,
-        {
-          value: {
-            // eslint-disable-next-line @typescript-eslint/ban-types
-            fetcher: (url: string, options: object) => {
-              const mocked = __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE(
-                {
-                  method: "get",
-                  url,
-                },
-                {},
-              );
-              if (!mocked) return undefined;
-              if (mocked.status > 400) {
-                const e: any = Error("simulated error for testing");
-                //example error handling from 
https://swr.vercel.app/docs/error-handling
-                e.status = mocked.status;
-                throw e;
-              }
-              return mocked.payload;
-            },
-            //These options are set for ending the test faster
-            //otherwise SWR will create timeouts that will live after the test 
finished
-            loadingTimeout: 0,
-            dedupingInterval: 0,
-            shouldRetryOnError: false,
-            errorRetryInterval: 0,
-            errorRetryCount: 0,
-            //clean cache for every test
-            provider: () => new Map(),
-          },
-        },
-        children,
-      );
+      return h(SWRConfig, { value }, children);
     };
   }
 }
diff --git a/packages/web-util/src/utils/http-impl.browser.ts 
b/packages/web-util/src/utils/http-impl.browser.ts
new file mode 100644
index 000000000..2b6ca019c
--- /dev/null
+++ b/packages/web-util/src/utils/http-impl.browser.ts
@@ -0,0 +1,203 @@
+/*
+ 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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+  Logger,
+  RequestThrottler,
+  TalerErrorCode,
+  TalerError,
+} from "@gnu-taler/taler-util";
+
+import {
+  HttpRequestLibrary,
+  HttpRequestOptions,
+  HttpResponse,
+  Headers,
+} from "@gnu-taler/taler-util/http";
+
+const logger = new Logger("browserHttpLib");
+
+/**
+ * An implementation of the [[HttpRequestLibrary]] using the
+ * browser's XMLHttpRequest.
+ */
+export class BrowserHttpLib implements HttpRequestLibrary {
+  private throttle = new RequestThrottler();
+  private throttlingEnabled = true;
+
+  fetch(
+    requestUrl: string,
+    options?: HttpRequestOptions,
+  ): Promise<HttpResponse> {
+    const requestMethod = options?.method ?? "GET";
+    const requestBody = options?.body;
+
+    if (this.throttlingEnabled && this.throttle.applyThrottle(requestUrl)) {
+      const parsedUrl = new URL(requestUrl);
+      throw TalerError.fromDetail(
+        TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
+        {
+          requestMethod,
+          requestUrl,
+          throttleStats: this.throttle.getThrottleStats(requestUrl),
+        },
+        `request to origin ${parsedUrl.origin} was throttled`,
+      );
+    }
+
+    return new Promise<HttpResponse>((resolve, reject) => {
+      const myRequest = new XMLHttpRequest();
+      myRequest.open(requestMethod, requestUrl);
+      if (options?.headers) {
+        for (const headerName in options.headers) {
+          myRequest.setRequestHeader(headerName, options.headers[headerName]);
+        }
+      }
+      myRequest.responseType = "arraybuffer";
+      if (requestBody) {
+        if (requestBody instanceof ArrayBuffer) {
+          myRequest.send(requestBody);
+        } else if (ArrayBuffer.isView(requestBody)) {
+          myRequest.send(requestBody);
+        } else if (typeof requestBody === "string") {
+          myRequest.send(requestBody);
+        } else {
+          myRequest.send(JSON.stringify(requestBody));
+        }
+      } else {
+        myRequest.send();
+      }
+
+      myRequest.onerror = (e) => {
+        logger.error("http request error");
+        reject(
+          TalerError.fromDetail(
+            TalerErrorCode.WALLET_NETWORK_ERROR,
+            {
+              requestUrl,
+              requestMethod,
+            },
+            "Could not make request",
+          ),
+        );
+      };
+
+      myRequest.addEventListener("readystatechange", (e) => {
+        if (myRequest.readyState === XMLHttpRequest.DONE) {
+          if (myRequest.status === 0) {
+            const exc = TalerError.fromDetail(
+              TalerErrorCode.WALLET_NETWORK_ERROR,
+              {
+                requestUrl,
+                requestMethod,
+              },
+              "HTTP request failed (status 0, maybe URI scheme was wrong?)",
+            );
+            reject(exc);
+            return;
+          }
+          const makeText = async (): Promise<string> => {
+            const td = new TextDecoder();
+            return td.decode(myRequest.response);
+          };
+          const makeJson = async (): Promise<any> => {
+            let responseJson;
+            try {
+              const td = new TextDecoder();
+              const responseString = td.decode(myRequest.response);
+              responseJson = JSON.parse(responseString);
+            } catch (e) {
+              throw TalerError.fromDetail(
+                TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+                {
+                  requestUrl,
+                  requestMethod,
+                  httpStatusCode: myRequest.status,
+                },
+                "Invalid JSON from HTTP response",
+              );
+            }
+            if (responseJson === null || typeof responseJson !== "object") {
+              throw TalerError.fromDetail(
+                TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+                {
+                  requestUrl,
+                  requestMethod,
+                  httpStatusCode: myRequest.status,
+                },
+                "Invalid JSON from HTTP response",
+              );
+            }
+            return responseJson;
+          };
+
+          const headers = myRequest.getAllResponseHeaders();
+          const arr = headers.trim().split(/[\r\n]+/);
+
+          // Create a map of header names to values
+          const headerMap: Headers = new Headers();
+          arr.forEach(function (line) {
+            const parts = line.split(": ");
+            const headerName = parts.shift();
+            if (!headerName) {
+              logger.warn("skipping invalid header");
+              return;
+            }
+            const value = parts.join(": ");
+            headerMap.set(headerName, value);
+          });
+          const resp: HttpResponse = {
+            requestUrl: requestUrl,
+            status: myRequest.status,
+            headers: headerMap,
+            requestMethod: requestMethod,
+            json: makeJson,
+            text: makeText,
+            bytes: async () => myRequest.response,
+          };
+          resolve(resp);
+        }
+      });
+    });
+  }
+
+  get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
+    return this.fetch(url, {
+      method: "GET",
+      ...opt,
+    });
+  }
+
+  postJson(
+    url: string,
+    body: any,
+    opt?: HttpRequestOptions,
+  ): Promise<HttpResponse> {
+    return this.fetch(url, {
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify(body),
+      ...opt,
+    });
+  }
+
+  stop(): void {
+    // Nothing to do
+  }
+}
diff --git a/packages/web-util/src/utils/http-impl.sw.ts 
b/packages/web-util/src/utils/http-impl.sw.ts
new file mode 100644
index 000000000..921acd63b
--- /dev/null
+++ b/packages/web-util/src/utils/http-impl.sw.ts
@@ -0,0 +1,205 @@
+/*
+ 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/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+  RequestThrottler,
+  TalerErrorCode,
+  TalerError,
+} from "@gnu-taler/taler-util";
+
+import {
+  Headers,
+  HttpRequestLibrary,
+  HttpRequestOptions,
+  HttpResponse,
+} from "@gnu-taler/taler-util/http";
+
+/**
+ * An implementation of the [[HttpRequestLibrary]] using the
+ * browser's XMLHttpRequest.
+ */
+export class ServiceWorkerHttpLib implements HttpRequestLibrary {
+  private throttle = new RequestThrottler();
+  private throttlingEnabled = true;
+
+  async fetch(
+    requestUrl: string,
+    options?: HttpRequestOptions,
+  ): Promise<HttpResponse> {
+    const requestMethod = options?.method ?? "GET";
+    const requestBody = options?.body;
+    const requestHeader = options?.headers;
+    const requestTimeout = options?.timeout ?? { d_ms: 2 * 1000 };
+
+    if (this.throttlingEnabled && this.throttle.applyThrottle(requestUrl)) {
+      const parsedUrl = new URL(requestUrl);
+      throw TalerError.fromDetail(
+        TalerErrorCode.WALLET_HTTP_REQUEST_THROTTLED,
+        {
+          requestMethod,
+          requestUrl,
+          throttleStats: this.throttle.getThrottleStats(requestUrl),
+        },
+        `request to origin ${parsedUrl.origin} was throttled`,
+      );
+    }
+
+    let myBody: BodyInit | undefined = undefined;
+    if (requestBody != null) {
+      if (typeof requestBody === "string") {
+        myBody = requestBody;
+      } else if (requestBody instanceof ArrayBuffer) {
+        myBody = requestBody;
+      } else if (ArrayBuffer.isView(requestBody)) {
+        myBody = requestBody;
+      } else if (typeof requestBody === "object") {
+        myBody = JSON.stringify(requestBody);
+      } else {
+        throw Error("unsupported request body type");
+      }
+    }
+
+    const controller = new AbortController();
+    let timeoutId: any | undefined;
+    if (requestTimeout.d_ms !== "forever") {
+      timeoutId = setTimeout(() => {
+        controller.abort(TalerErrorCode.WALLET_HTTP_REQUEST_GENERIC_TIMEOUT);
+      }, requestTimeout.d_ms);
+    }
+
+    try {
+      const response = await fetch(requestUrl, {
+        headers: requestHeader,
+        body: myBody,
+        method: requestMethod,
+        signal: controller.signal,
+      });
+
+      if (timeoutId) {
+        clearTimeout(timeoutId);
+      }
+
+      const headerMap = new Headers();
+      response.headers.forEach((value, key) => {
+        headerMap.set(key, value);
+      });
+      return {
+        headers: headerMap,
+        status: response.status,
+        requestMethod,
+        requestUrl,
+        json: makeJsonHandler(response, requestUrl, requestMethod),
+        text: makeTextHandler(response, requestUrl, requestMethod),
+        bytes: async () => (await response.blob()).arrayBuffer(),
+      };
+    } catch (e) {
+      if (controller.signal) {
+        throw TalerError.fromDetail(
+          controller.signal.reason,
+          {},
+          `request to ${requestUrl} timed out`,
+        );
+      }
+      throw e;
+    }
+  }
+
+  get(url: string, opt?: HttpRequestOptions): Promise<HttpResponse> {
+    return this.fetch(url, {
+      method: "GET",
+      ...opt,
+    });
+  }
+
+  postJson(
+    url: string,
+    body: any,
+    opt?: HttpRequestOptions,
+  ): Promise<HttpResponse> {
+    return this.fetch(url, {
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify(body),
+      ...opt,
+    });
+  }
+
+  stop(): void {
+    // Nothing to do
+  }
+}
+
+function makeTextHandler(
+  response: Response,
+  requestUrl: string,
+  requestMethod: string,
+) {
+  return async function getJsonFromResponse(): Promise<any> {
+    let respText;
+    try {
+      respText = await response.text();
+    } catch (e) {
+      throw TalerError.fromDetail(
+        TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+        {
+          requestUrl,
+          requestMethod,
+          httpStatusCode: response.status,
+        },
+        "Invalid JSON from HTTP response",
+      );
+    }
+    return respText;
+  };
+}
+
+function makeJsonHandler(
+  response: Response,
+  requestUrl: string,
+  requestMethod: string,
+) {
+  return async function getJsonFromResponse(): Promise<any> {
+    let responseJson;
+    try {
+      responseJson = await response.json();
+    } catch (e) {
+      throw TalerError.fromDetail(
+        TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+        {
+          requestUrl,
+          requestMethod,
+          httpStatusCode: response.status,
+        },
+        "Invalid JSON from HTTP response",
+      );
+    }
+    if (responseJson === null || typeof responseJson !== "object") {
+      throw TalerError.fromDetail(
+        TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE,
+        {
+          requestUrl,
+          requestMethod,
+          httpStatusCode: response.status,
+        },
+        "Invalid JSON from HTTP response",
+      );
+    }
+    return responseJson;
+  };
+}
diff --git a/packages/web-util/src/utils/request.ts 
b/packages/web-util/src/utils/request.ts
index 8c77814f7..7f7063a23 100644
--- a/packages/web-util/src/utils/request.ts
+++ b/packages/web-util/src/utils/request.ts
@@ -126,11 +126,12 @@ export async function defaultRequestHandler<T>(
     );
     return result;
   } else {
-    const error = await buildRequestFailed(
-      response,
+    const dataTxt = await response.text();
+    const error = buildRequestFailed(
       _url.href,
+      dataTxt,
+      response.status,
       payload,
-      !!options.token,
       options,
     );
     throw new RequestError(error);
@@ -292,47 +293,58 @@ async function buildRequestOk<T>(
   };
 }
 
-async function buildRequestFailed<ErrorDetail>(
-  response: Response,
+export function buildRequestFailed<ErrorDetail>(
   url: string,
+  dataTxt: string,
+  status: number,
   payload: any,
-  hasToken: boolean,
-  options: RequestOptions,
-): Promise<
+  maybeOptions?: RequestOptions,
+):
   | HttpResponseClientError<ErrorDetail>
   | HttpResponseServerError<ErrorDetail>
   | HttpResponseUnreadableError
-  | HttpResponseUnexpectedError
-> {
-  const status = response?.status;
-
+  | HttpResponseUnexpectedError {
+  const options = maybeOptions ?? {};
   const info: RequestInfo = {
     payload,
     url,
-    hasToken,
+    hasToken: !!options.token,
     options,
     status: status || 0,
   };
 
-  const dataTxt = await response.text();
+  // const dataTxt = await response.text();
   try {
     const data = dataTxt ? JSON.parse(dataTxt) : undefined;
+    const errorCode = !data || !data.code ? "" : `(code: ${data.code})`;
+    const errorHint =
+      !data || !data.hint ? "Not hint." : `${data.hint} ${errorCode}`;
+
     if (status && status >= 400 && status < 500) {
+      const message =
+        data === undefined
+          ? `Client error (${status}) without data.`
+          : errorHint;
+
       const error: HttpResponseClientError<ErrorDetail> = {
         type: ErrorType.CLIENT,
         status,
         info,
-        message: data?.hint,
+        message,
         payload: data,
       };
       return error;
     }
     if (status && status >= 500 && status < 600) {
+      const message =
+        data === undefined
+          ? `Server error (${status}) without data.`
+          : errorHint;
       const error: HttpResponseServerError<ErrorDetail> = {
         type: ErrorType.SERVER,
         status,
         info,
-        message: `${data?.hint} (code ${data?.code})`,
+        message,
         payload: data,
       };
       return error;

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]