diff options
Diffstat (limited to 'packages/merchant-backoffice-ui')
58 files changed, 3519 insertions, 4245 deletions
diff --git a/packages/merchant-backoffice-ui/package.json b/packages/merchant-backoffice-ui/package.json index beacd42f6..cffe73e3f 100644 --- a/packages/merchant-backoffice-ui/package.json +++ b/packages/merchant-backoffice-ui/package.json @@ -35,7 +35,6 @@    "dependencies": {      "@gnu-taler/taler-util": "workspace:*",      "@gnu-taler/web-util": "workspace:*", -    "axios": "^0.21.1",      "date-fns": "2.29.3",      "history": "4.10.1",      "jed": "1.1.1", @@ -48,10 +47,8 @@    "devDependencies": {      "@creativebulma/bulma-tooltip": "^1.2.0",      "@gnu-taler/pogen": "^0.0.5", -    "@testing-library/preact": "^2.0.1", -    "@testing-library/preact-hooks": "^1.1.0", +    "@types/chai": "^4.3.0",      "@types/history": "^4.7.8", -    "@types/jest": "^26.0.23",      "@types/mocha": "^8.2.3",      "@types/node": "^18.11.17",      "@typescript-eslint/eslint-plugin": "^4.22.0", @@ -64,6 +61,7 @@      "bulma-switch-control": "^1.1.1",      "bulma-timeline": "^3.0.4",      "bulma-upload-control": "^1.2.0", +    "chai": "^4.3.6",      "dotenv": "^8.2.0",      "eslint": "^7.25.0",      "eslint-config-preact": "^1.1.4", @@ -72,13 +70,12 @@      "html-webpack-inline-source-plugin": "0.0.10",      "html-webpack-skip-assets-plugin": "^1.0.1",      "inline-chunk-html-plugin": "^1.1.1", -    "jest": "^26.6.3", -    "jest-preset-preact": "^4.0.2",      "mocha": "^9.2.0",      "preact-render-to-string": "^5.2.6",      "rimraf": "^3.0.2",      "sass": "1.56.1", +    "source-map-support": "^0.5.21",      "typedoc": "^0.20.36",      "typescript": "4.8.4"    } -}
\ No newline at end of file +} diff --git a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx index 8ac5c698b..1c55572bb 100644 --- a/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx +++ b/packages/merchant-backoffice-ui/src/InstanceRoutes.tsx @@ -28,7 +28,7 @@ import { Loading } from "./components/exception/loading.js";  import { Menu, NotificationCard } from "./components/menu/index.js";  import { useBackendContext } from "./context/backend.js";  import { InstanceContextProvider } from "./context/instance.js"; -import { HttpError } from "./hooks/backend.js"; +import { HttpError } from "./utils/request.js";  import {    useBackendDefaultToken,    useBackendInstanceToken, @@ -484,7 +484,7 @@ export function Redirect({ to }: { to: string }): null {  function AdminInstanceUpdatePage({    id,    ...rest -}: { id: string } & InstanceUpdatePageProps) { +}: { id: string } & InstanceUpdatePageProps): VNode {    const [token, changeToken] = useBackendInstanceToken(id);    const { updateLoginStatus: changeBackend } = useBackendContext();    const updateLoginStatus = (url: string, token?: string) => { diff --git a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx index 7bf39152b..68f79bc35 100644 --- a/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx +++ b/packages/merchant-backoffice-ui/src/components/form/InputPaytoForm.tsx @@ -195,7 +195,7 @@ export function InputPaytoForm<T>({        if (opt_value) url.searchParams.set(opt_key, opt_value);      });    } -  const paytoURL = !url ? "" : url.toString(); +  const paytoURL = !url ? "" : url.href;    const errors: FormErrors<Entity> = {      target: value.target === noTargetValue ? i18n.str`required` : undefined, diff --git a/packages/merchant-backoffice-ui/tests/__mocks__/browserMocks.ts b/packages/merchant-backoffice-ui/src/context/api.ts index 98a5153de..81586bd35 100644 --- a/packages/merchant-backoffice-ui/tests/__mocks__/browserMocks.ts +++ b/packages/merchant-backoffice-ui/src/context/api.ts @@ -14,29 +14,30 @@   GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>   */ - /** +/**   *   * @author Sebastian Javier Marchano (sebasjm)   */ -// Mock Browser API's which are not supported by JSDOM, e.g. ServiceWorker, LocalStorage -/** - * An example how to mock localStorage is given below 👇 - */ - -/*  -// Mocks localStorage -const localStorageMock = (function() { -	let store = {}; - -	return { -		getItem: (key) => store[key] || null, -		setItem: (key, value) => store[key] = value.toString(), -		clear: () => store = {} -	}; - -})(); - -Object.defineProperty(window, 'localStorage', { -	value: localStorageMock -}); */ +import { ComponentChildren, createContext, h, VNode } from "preact"; +import { useContext } from "preact/hooks"; +import { defaultRequestHandler } from "../utils/request.js"; + +interface Type { +  request: typeof defaultRequestHandler; +} + +const Context = createContext<Type>({ +  request: defaultRequestHandler, +}); + +export const useApiContext = (): Type => useContext(Context); +export const ApiContextProvider = ({ +  children, +  value, +}: { +  value: Type; +  children: ComponentChildren; +}): VNode => { +  return h(Context.Provider, { value, children }); +}; diff --git a/packages/merchant-backoffice-ui/src/context/backend.test.ts b/packages/merchant-backoffice-ui/src/context/backend.test.ts new file mode 100644 index 000000000..c7fb19293 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/context/backend.test.ts @@ -0,0 +1,131 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { tests } from "@gnu-taler/web-util/lib/index.browser"; +import { ComponentChildren, h, VNode } from "preact"; +import { MerchantBackend } from "../declaration.js"; +import { +  useAdminAPI, +  useInstanceAPI, +  useManagementAPI, +} from "../hooks/instance.js"; +import { expect } from "chai"; +import { ApiMockEnvironment } from "../hooks/testing.js"; +import { API_CREATE_INSTANCE, API_UPDATE_CURRENT_INSTANCE_AUTH, API_UPDATE_INSTANCE_AUTH_BY_ID } from "../hooks/urls.js"; + +interface TestingContextProps { +  children?: ComponentChildren; +} + +describe("backend context api ", () => { + +  it("should use new token after updating the instance token in the settings as user", async () => { +    const env = new ApiMockEnvironment(); + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const instance = useInstanceAPI(); +        const management = useManagementAPI("default"); +        const admin = useAdminAPI(); + +        return { instance, management, admin }; +      }, +      {}, +      [ +        ({ instance, management, admin }) => { +          env.addRequestExpectation(API_UPDATE_INSTANCE_AUTH_BY_ID("default"), { +            request: { +              method: "token", +              token: "another_token", +            }, +            response: { +              name: "instance_name", +            } as MerchantBackend.Instances.QueryInstancesResponse, +          }); + +          management.setNewToken("another_token") +        }, +        ({ instance, management, admin }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +          env.addRequestExpectation(API_CREATE_INSTANCE, { +            auth: "another_token", +            request: { +              id: "new_instance_id", +            } as MerchantBackend.Instances.InstanceConfigurationMessage, +          }); + +          admin.createInstance({ +            id: "new_instance_id", +          } as MerchantBackend.Instances.InstanceConfigurationMessage); + +        }, +      ], env.buildTestingContext()); + +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +  }); + +  it("should use new token after updating the instance token in the settings as admin", async () => { +    const env = new ApiMockEnvironment(); + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const instance = useInstanceAPI(); +        const management = useManagementAPI("default"); +        const admin = useAdminAPI(); + +        return { instance, management, admin }; +      }, +      {}, +      [ +        ({ instance, management, admin }) => { +          env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { +            request: { +              method: "token", +              token: "another_token", +            }, +            response: { +              name: "instance_name", +            } as MerchantBackend.Instances.QueryInstancesResponse, +          }); +          instance.setNewToken("another_token"); +        }, +        ({ instance, management, admin }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +          env.addRequestExpectation(API_CREATE_INSTANCE, { +            auth: "another_token", +            request: { +              id: "new_instance_id", +            } as MerchantBackend.Instances.InstanceConfigurationMessage, +          }); + +          admin.createInstance({ +            id: "new_instance_id", +          } as MerchantBackend.Instances.InstanceConfigurationMessage); +        }, +      ], env.buildTestingContext()); + +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +  }); +}); diff --git a/packages/merchant-backoffice-ui/src/context/backend.ts b/packages/merchant-backoffice-ui/src/context/backend.ts index f8d1bc397..f7f8afea6 100644 --- a/packages/merchant-backoffice-ui/src/context/backend.ts +++ b/packages/merchant-backoffice-ui/src/context/backend.ts @@ -31,6 +31,7 @@ interface BackendContextType {    clearAllTokens: () => void;    addTokenCleaner: (c: () => void) => void;    updateLoginStatus: (url: string, token?: string) => void; +  updateToken: (token?: string) => void;  }  const BackendContext = createContext<BackendContextType>({ @@ -41,6 +42,7 @@ const BackendContext = createContext<BackendContextType>({    clearAllTokens: () => null,    addTokenCleaner: () => null,    updateLoginStatus: () => null, +  updateToken: () => null,  });  function useBackendContextState( @@ -87,6 +89,7 @@ function useBackendContextState(      updateLoginStatus,      resetBackend,      clearAllTokens, +    updateToken,      addTokenCleaner: addTokenCleanerMemo,    };  } diff --git a/packages/merchant-backoffice-ui/src/context/fetch.ts b/packages/merchant-backoffice-ui/src/context/fetch.ts deleted file mode 100644 index 88c9bc30c..000000000 --- a/packages/merchant-backoffice-ui/src/context/fetch.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, createContext, VNode, ComponentChildren } from "preact"; -import { useContext } from "preact/hooks"; -import useSWR from "swr"; -import useSWRInfinite from "swr/infinite"; - -interface Type { -  useSWR: typeof useSWR; -  useSWRInfinite: typeof useSWRInfinite; -} - -const Context = createContext<Type>({} as Type); - -export const useFetchContext = (): Type => useContext(Context); -export const FetchContextProvider = ({ -  children, -}: { -  children: ComponentChildren; -}): VNode => { -  return h(Context.Provider, { value: { useSWR, useSWRInfinite }, children }); -}; - -export const FetchContextProviderTesting = ({ -  children, -  data, -}: { -  children: ComponentChildren; -  data: any; -}): VNode => { -  return h(Context.Provider, { -    value: { useSWR: () => data, useSWRInfinite }, -    children, -  }); -}; diff --git a/packages/merchant-backoffice-ui/src/hooks/async.ts b/packages/merchant-backoffice-ui/src/hooks/async.ts index 6c116e628..f22badc88 100644 --- a/packages/merchant-backoffice-ui/src/hooks/async.ts +++ b/packages/merchant-backoffice-ui/src/hooks/async.ts @@ -19,7 +19,6 @@   * @author Sebastian Javier Marchano (sebasjm)   */  import { useState } from "preact/hooks"; -import { cancelPendingRequest } from "./backend.js";  export interface Options {    slowTolerance: number; @@ -62,8 +61,7 @@ export function useAsync<T>(      clearTimeout(handler);    }; -  function cancel() { -    cancelPendingRequest(); +  function cancel(): void {      setLoading(false);      setSlow(false);    } diff --git a/packages/merchant-backoffice-ui/src/hooks/backend.ts b/packages/merchant-backoffice-ui/src/hooks/backend.ts index cbfac35de..a0639a4a0 100644 --- a/packages/merchant-backoffice-ui/src/hooks/backend.ts +++ b/packages/merchant-backoffice-ui/src/hooks/backend.ts @@ -20,15 +20,16 @@   */  import { useSWRConfig } from "swr"; -import axios, { AxiosError, AxiosResponse } from "axios";  import { MerchantBackend } from "../declaration.js";  import { useBackendContext } from "../context/backend.js"; -import { useEffect, useState } from "preact/hooks"; -import { DEFAULT_REQUEST_TIMEOUT } from "../utils/constants.js"; +import { useCallback, useEffect, useState } from "preact/hooks"; +import { useInstanceContext } from "../context/instance.js";  import { -  axiosHandler, -  removeAxiosCancelToken, -} from "../utils/switchableAxios.js"; +  HttpResponse, +  HttpResponseOk, +  RequestOptions, +} from "../utils/request.js"; +import { useApiContext } from "../context/api.js";  export function useMatchMutate(): (    re: RegExp, @@ -44,9 +45,7 @@ export function useMatchMutate(): (    return function matchRegexMutate(re: RegExp, value?: unknown) {      const allKeys = Array.from(cache.keys()); -    // console.log(allKeys)      const keys = allKeys.filter((key) => re.test(key)); -    // console.log(allKeys.length, keys.length)      const mutations = keys.map((key) => {        // console.log(key)        mutate(key, value, true); @@ -55,268 +54,234 @@ export function useMatchMutate(): (    };  } -export type HttpResponse<T> = -  | HttpResponseOk<T> -  | HttpResponseLoading<T> -  | HttpError; -export type HttpResponsePaginated<T> = -  | HttpResponseOkPaginated<T> -  | HttpResponseLoading<T> -  | HttpError; - -export interface RequestInfo { -  url: string; -  hasToken: boolean; -  params: unknown; -  data: unknown; -  status: number; -} - -interface HttpResponseLoading<T> { -  ok?: false; -  loading: true; -  clientError?: false; -  serverError?: false; - -  data?: T; -} -export interface HttpResponseOk<T> { -  ok: true; -  loading?: false; -  clientError?: false; -  serverError?: false; - -  data: T; -  info?: RequestInfo; -} - -export type HttpResponseOkPaginated<T> = HttpResponseOk<T> & WithPagination; - -export interface WithPagination { -  loadMore: () => void; -  loadMorePrev: () => void; -  isReachingEnd?: boolean; -  isReachingStart?: boolean; -} - -export type HttpError = -  | HttpResponseClientError -  | HttpResponseServerError -  | HttpResponseUnexpectedError; -export interface SwrError { -  info: unknown; -  status: number; -  message: string; -} -export interface HttpResponseServerError { -  ok?: false; -  loading?: false; -  clientError?: false; -  serverError: true; - -  error?: MerchantBackend.ErrorDetail; -  status: number; -  message: string; -  info?: RequestInfo; -} -interface HttpResponseClientError { -  ok?: false; -  loading?: false; -  clientError: true; -  serverError?: false; - -  info?: RequestInfo; -  isUnauthorized: boolean; -  isNotfound: boolean; -  status: number; -  error?: MerchantBackend.ErrorDetail; -  message: string; -} - -interface HttpResponseUnexpectedError { -  ok?: false; -  loading?: false; -  clientError?: false; -  serverError?: false; - -  info?: RequestInfo; -  status?: number; -  error: unknown; -  message: string; -} - -type Methods = "get" | "post" | "patch" | "delete" | "put"; - -interface RequestOptions { -  method?: Methods; -  token?: string; -  data?: unknown; -  params?: unknown; -} - -function buildRequestOk<T>( -  res: AxiosResponse<T>, -  url: string, -  hasToken: boolean, -): HttpResponseOk<T> { -  return { -    ok: true, -    data: res.data, -    info: { -      params: res.config.params, -      data: res.config.data, -      url, -      hasToken, -      status: res.status, -    }, -  }; -} - -// function buildResponse<T>(data?: T, error?: MerchantBackend.ErrorDetail, isValidating?: boolean): HttpResponse<T> { -//   if (isValidating) return {loading: true} -//   if (error) return buildRequestFailed() -// } - -function buildRequestFailed( -  ex: AxiosError<MerchantBackend.ErrorDetail>, -  url: string, -  hasToken: boolean, -): -  | HttpResponseClientError -  | HttpResponseServerError -  | HttpResponseUnexpectedError { -  const status = ex.response?.status; - -  const info: RequestInfo = { -    data: ex.request?.data, -    params: ex.request?.params, -    url, -    hasToken, -    status: status || 0, -  }; - -  if (status && status >= 400 && status < 500) { -    const error: HttpResponseClientError = { -      clientError: true, -      isNotfound: status === 404, -      isUnauthorized: status === 401, -      status, -      info, -      message: ex.response?.data?.hint || ex.message, -      error: ex.response?.data, -    }; -    return error; -  } -  if (status && status >= 500 && status < 600) { -    const error: HttpResponseServerError = { -      serverError: true, -      status, -      info, -      message: -        `${ex.response?.data?.hint} (code ${ex.response?.data?.code})` || -        ex.message, -      error: ex.response?.data, -    }; -    return error; -  } - -  const error: HttpResponseUnexpectedError = { -    info, -    status, -    error: ex, -    message: ex.message, -  }; - -  return error; -} - -const CancelToken = axios.CancelToken; -let source = CancelToken.source(); - -export function cancelPendingRequest(): void { -  source.cancel("canceled by the user"); -  source = CancelToken.source(); -} - -export function isAxiosError<T>( -  error: AxiosError | any, -): error is AxiosError<T> { -  return error && error.isAxiosError; -} - -export async function request<T>( -  url: string, -  options: RequestOptions = {}, -): Promise<HttpResponseOk<T>> { -  const headers = options.token -    ? { Authorization: `Bearer ${options.token}` } -    : undefined; - -  try { -    const res = await axiosHandler({ -      url, -      responseType: "json", -      headers, -      cancelToken: !removeAxiosCancelToken ? source.token : undefined, -      method: options.method || "get", -      data: options.data, -      params: options.params, -      timeout: DEFAULT_REQUEST_TIMEOUT * 1000, -    }); -    return buildRequestOk<T>(res, url, !!options.token); -  } catch (e) { -    if (isAxiosError<MerchantBackend.ErrorDetail>(e)) { -      const error = buildRequestFailed(e, url, !!options.token); -      throw error; -    } -    throw e; -  } -} - -export function multiFetcher<T>( -  urls: string[], -  token: string, -  backend: string, -): Promise<HttpResponseOk<T>[]> { -  return Promise.all(urls.map((url) => fetcher<T>(url, token, backend))); -} - -export function fetcher<T>( -  url: string, -  token: string, -  backend: string, -): Promise<HttpResponseOk<T>> { -  return request<T>(`${backend}${url}`, { token }); -} -  export function useBackendInstancesTestForAdmin(): HttpResponse<MerchantBackend.Instances.InstancesResponse> { -  const { url, token } = useBackendContext(); +  const { request } = useBackendBaseRequest();    type Type = MerchantBackend.Instances.InstancesResponse;    const [result, setResult] = useState<HttpResponse<Type>>({ loading: true });    useEffect(() => { -    request<Type>(`${url}/management/instances`, { token }) +    request<Type>(`/management/instances`)        .then((data) => setResult(data))        .catch((error) => setResult(error)); -  }, [url, token]); +  }, [request]);    return result;  }  export function useBackendConfig(): HttpResponse<MerchantBackend.VersionResponse> { -  const { url, token } = useBackendContext(); +  const { request } = useBackendBaseRequest();    type Type = MerchantBackend.VersionResponse;    const [result, setResult] = useState<HttpResponse<Type>>({ loading: true });    useEffect(() => { -    request<Type>(`${url}/config`, { token }) +    request<Type>(`/config`)        .then((data) => setResult(data))        .catch((error) => setResult(error)); -  }, [url, token]); +  }, [request]);    return result;  } + +interface useBackendInstanceRequestType { +  request: <T>( +    path: string, +    options?: RequestOptions, +  ) => Promise<HttpResponseOk<T>>; +  fetcher: <T>(path: string) => Promise<HttpResponseOk<T>>; +  reserveDetailFetcher: <T>(path: string) => Promise<HttpResponseOk<T>>; +  tipsDetailFetcher: <T>(path: string) => Promise<HttpResponseOk<T>>; +  multiFetcher: <T>(url: string[]) => Promise<HttpResponseOk<T>[]>; +  orderFetcher: <T>( +    path: string, +    paid?: YesOrNo, +    refunded?: YesOrNo, +    wired?: YesOrNo, +    searchDate?: Date, +    delta?: number, +  ) => Promise<HttpResponseOk<T>>; +  transferFetcher: <T>( +    path: string, +    payto_uri?: string, +    verified?: string, +    position?: string, +    delta?: number, +  ) => Promise<HttpResponseOk<T>>; +  templateFetcher: <T>( +    path: string, +    position?: string, +    delta?: number, +  ) => Promise<HttpResponseOk<T>>; +} +interface useBackendBaseRequestType { +  request: <T>( +    path: string, +    options?: RequestOptions, +  ) => Promise<HttpResponseOk<T>>; +} + +type YesOrNo = "yes" | "no"; + +/** + * + * @param root the request is intended to the base URL and no the instance URL + * @returns request handler to + */ +export function useBackendBaseRequest(): useBackendBaseRequestType { +  const { url: backend, token } = useBackendContext(); +  const { request: requestHandler } = useApiContext(); + +  const request = useCallback( +    function requestImpl<T>( +      path: string, +      options: RequestOptions = {}, +    ): Promise<HttpResponseOk<T>> { +      return requestHandler<T>(backend, path, { token, ...options }); +    }, +    [backend, token], +  ); + +  return { request }; +} + +export function useBackendInstanceRequest(): useBackendInstanceRequestType { +  const { url: baseUrl, token: baseToken } = useBackendContext(); +  const { token: instanceToken, id, admin } = useInstanceContext(); +  const { request: requestHandler } = useApiContext(); + +  const { backend, token } = !admin +    ? { backend: baseUrl, token: baseToken } +    : { backend: `${baseUrl}/instances/${id}`, token: instanceToken }; + +  const request = useCallback( +    function requestImpl<T>( +      path: string, +      options: RequestOptions = {}, +    ): Promise<HttpResponseOk<T>> { +      return requestHandler<T>(backend, path, { token, ...options }); +    }, +    [backend, token], +  ); + +  const multiFetcher = useCallback( +    function multiFetcherImpl<T>( +      paths: string[], +    ): Promise<HttpResponseOk<T>[]> { +      return Promise.all( +        paths.map((path) => requestHandler<T>(backend, path, { token })), +      ); +    }, +    [backend, token], +  ); + +  const fetcher = useCallback( +    function fetcherImpl<T>(path: string): Promise<HttpResponseOk<T>> { +      return requestHandler<T>(backend, path, { token }); +    }, +    [backend, token], +  ); + +  const orderFetcher = useCallback( +    function orderFetcherImpl<T>( +      path: string, +      paid?: YesOrNo, +      refunded?: YesOrNo, +      wired?: YesOrNo, +      searchDate?: Date, +      delta?: number, +    ): Promise<HttpResponseOk<T>> { +      const date_ms = +        delta && delta < 0 && searchDate +          ? searchDate.getTime() + 1 +          : searchDate?.getTime(); +      const params: any = {}; +      if (paid !== undefined) params.paid = paid; +      if (delta !== undefined) params.delta = delta; +      if (refunded !== undefined) params.refunded = refunded; +      if (wired !== undefined) params.wired = wired; +      if (date_ms !== undefined) params.date_ms = date_ms; +      return requestHandler<T>(backend, path, { params, token }); +    }, +    [backend, token], +  ); + +  const reserveDetailFetcher = useCallback( +    function reserveDetailFetcherImpl<T>( +      path: string, +    ): Promise<HttpResponseOk<T>> { +      return requestHandler<T>(backend, path, { +        params: { +          tips: "yes", +        }, +        token, +      }); +    }, +    [backend, token], +  ); + +  const tipsDetailFetcher = useCallback( +    function tipsDetailFetcherImpl<T>( +      path: string, +    ): Promise<HttpResponseOk<T>> { +      return requestHandler<T>(backend, path, { +        params: { +          pickups: "yes", +        }, +        token, +      }); +    }, +    [backend, token], +  ); + +  const transferFetcher = useCallback( +    function transferFetcherImpl<T>( +      path: string, +      payto_uri?: string, +      verified?: string, +      position?: string, +      delta?: number, +    ): Promise<HttpResponseOk<T>> { +      const params: any = {}; +      if (payto_uri !== undefined) params.payto_uri = payto_uri; +      if (verified !== undefined) params.verified = verified; +      if (delta !== undefined) { +        params.limit = delta; +      } +      if (position !== undefined) params.offset = position; + +      return requestHandler<T>(backend, path, { params, token }); +    }, +    [backend, token], +  ); + +  const templateFetcher = useCallback( +    function templateFetcherImpl<T>( +      path: string, +      position?: string, +      delta?: number, +    ): Promise<HttpResponseOk<T>> { +      const params: any = {}; +      if (delta !== undefined) { +        params.limit = delta; +      } +      if (position !== undefined) params.offset = position; + +      return requestHandler<T>(backend, path, { params, token }); +    }, +    [backend, token], +  ); + +  return { +    request, +    fetcher, +    multiFetcher, +    orderFetcher, +    reserveDetailFetcher, +    tipsDetailFetcher, +    transferFetcher, +    templateFetcher, +  }; +} diff --git a/packages/merchant-backoffice-ui/src/hooks/index.ts b/packages/merchant-backoffice-ui/src/hooks/index.ts index 0581d9938..bb210c9ba 100644 --- a/packages/merchant-backoffice-ui/src/hooks/index.ts +++ b/packages/merchant-backoffice-ui/src/hooks/index.ts @@ -59,6 +59,7 @@ export function useBackendDefaultToken(  export function useBackendInstanceToken(    id: string,  ): [string | undefined, StateUpdater<string | undefined>] { +  const [random, setRandom] = useState(0);    const [token, setToken] = useLocalStorage(`backend-token-${id}`);    const [defaultToken, defaultSetToken] = useBackendDefaultToken(); @@ -66,8 +67,20 @@ export function useBackendInstanceToken(    if (id === "default") {      return [defaultToken, defaultSetToken];    } +  function updateToken( +    value: +      | (string | undefined) +      | ((s: string | undefined) => string | undefined), +  ): void { +    setToken((p) => { +      const toStore = value instanceof Function ? value(p) : value; +      // setToken(value) +      setRandom(new Date().getTime()); +      return toStore; +    }); +  } -  return [token, setToken]; +  return [token, updateToken];  }  export function useLang(initial?: string): [string, StateUpdater<string>] { diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts new file mode 100644 index 000000000..c7aa63e20 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts @@ -0,0 +1,660 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { tests } from "@gnu-taler/web-util/lib/index.browser"; +import { expect } from "chai"; +import { MerchantBackend } from "../declaration.js"; +import { useAdminAPI, useBackendInstances, useInstanceAPI, useInstanceDetails, useManagementAPI } from "./instance.js"; +import { ApiMockEnvironment } from "./testing.js"; +import { +  API_CREATE_INSTANCE, +  API_DELETE_INSTANCE, +  API_GET_CURRENT_INSTANCE, +  API_LIST_INSTANCES, +  API_UPDATE_CURRENT_INSTANCE, +  API_UPDATE_CURRENT_INSTANCE_AUTH, API_UPDATE_INSTANCE_BY_ID +} from "./urls.js"; + +describe("instance api interaction with details", () => { + +  it("should evict cache when updating an instance", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { +      response: { +        name: 'instance_name' +      } as MerchantBackend.Instances.QueryInstancesResponse, +    }); + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const api = useInstanceAPI(); +        const query = useInstanceDetails(); +        return { query, api }; +      }, +      {}, +      [ +        ({ query, api }) => { +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            name: 'instance_name' +          }); +          env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE, { +            request: { +              name: 'other_name' +            } as MerchantBackend.Instances.InstanceReconfigurationMessage, +          }); +          env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { +            response: { +              name: 'other_name' +            } as MerchantBackend.Instances.QueryInstancesResponse, +          }); +          api.updateInstance({ +            name: 'other_name' +          } as MerchantBackend.Instances.InstanceReconfigurationMessage); +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            name: 'other_name' +          }); +        }, +      ], env.buildTestingContext()); + +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +  }); + +  it("should evict cache when setting the instance's token", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { +      response: { +        name: 'instance_name', +        auth: { +          method: 'token', +          token: 'not-secret', +        } +      } as MerchantBackend.Instances.QueryInstancesResponse, +    }); + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const api = useInstanceAPI(); +        const query = useInstanceDetails(); +        return { query, api }; +      }, +      {}, +      [ +        ({ query, api }) => { +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            name: 'instance_name', +            auth: { +              method: 'token', +              token: 'not-secret', +            } +          }); +          env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { +            request: { +              method: 'token', +              token: 'secret' +            } as MerchantBackend.Instances.InstanceAuthConfigurationMessage, +          }); +          env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { +            response: { +              name: 'instance_name', +              auth: { +                method: 'token', +                token: 'secret', +              } +            } as MerchantBackend.Instances.QueryInstancesResponse, +          }); +          api.setNewToken('secret') +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            name: 'instance_name', +            auth: { +              method: 'token', +              token: 'secret', +            } +          }); +        }, +      ], env.buildTestingContext()); +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +  }); + +  it("should evict cache when clearing the instance's token", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { +      response: { +        name: 'instance_name', +        auth: { +          method: 'token', +          token: 'not-secret', +        } +      } as MerchantBackend.Instances.QueryInstancesResponse, +    }); + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const api = useInstanceAPI(); +        const query = useInstanceDetails(); +        return { query, api }; +      }, +      {}, +      [ +        ({ query, api }) => { +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            name: 'instance_name', +            auth: { +              method: 'token', +              token: 'not-secret', +            } +          }); +          env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { +            request: { +              method: 'external', +            } as MerchantBackend.Instances.InstanceAuthConfigurationMessage, +          }); +          env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { +            response: { +              name: 'instance_name', +              auth: { +                method: 'external', +              } +            } as MerchantBackend.Instances.QueryInstancesResponse, +          }); + +          api.clearToken(); +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            name: 'instance_name', +            auth: { +              method: 'external', +            } +          }); +        }, +      ], env.buildTestingContext()); +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +    // const { result, waitForNextUpdate } = renderHook( +    //   () => { +    //     const api = useInstanceAPI(); +    //     const query = useInstanceDetails(); + +    //     return { query, api }; +    //   }, +    //   { wrapper: TestingContext } +    // ); + +    // expect(result.current).not.undefined; +    // if (!result.current) { +    //   return; +    // } +    // expect(result.current.query.loading).true; + +    // await waitForNextUpdate({ timeout: 1 }); + +    // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +    // expect(result.current.query.loading).false; + +    // expect(result.current?.query.ok).true; +    // if (!result.current?.query.ok) return; + +    // expect(result.current.query.data).equals({ +    //   name: 'instance_name', +    //   auth: { +    //     method: 'token', +    //     token: 'not-secret', +    //   } +    // }); + + +    // act(async () => { +    //   await result.current?.api.clearToken(); +    // }); + +    // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + + +    // expect(result.current.query.loading).false; + +    // await waitForNextUpdate({ timeout: 1 }); + +    // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +    // expect(result.current.query.loading).false; +    // expect(result.current.query.ok).true; + +    // expect(result.current.query.data).equals({ +    //   name: 'instance_name', +    //   auth: { +    //     method: 'external', +    //   } +    // }); +  }); +}); + +describe("instance admin api interaction with listing", () => { + +  it("should evict cache when creating a new instance", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_LIST_INSTANCES, { +      response: { +        instances: [{ +          name: 'instance_name' +        } as MerchantBackend.Instances.Instance] +      }, +    }); + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const api = useAdminAPI(); +        const query = useBackendInstances(); +        return { query, api }; +      }, +      {}, +      [ +        ({ query, api }) => { +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            instances: [{ +              name: 'instance_name' +            }] +          }); + +          env.addRequestExpectation(API_CREATE_INSTANCE, { +            request: { +              name: 'other_name' +            } as MerchantBackend.Instances.InstanceConfigurationMessage, +          }); +          env.addRequestExpectation(API_LIST_INSTANCES, { +            response: { +              instances: [{ +                name: 'instance_name' +              } as MerchantBackend.Instances.Instance, +              { +                name: 'other_name' +              } as MerchantBackend.Instances.Instance] +            }, +          }); + +          api.createInstance({ +            name: 'other_name' +          } as MerchantBackend.Instances.InstanceConfigurationMessage); +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            instances: [{ +              name: 'instance_name' +            }, { +              name: 'other_name' +            }] +          }); +        }, +      ], env.buildTestingContext()); +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +  }); + +  it("should evict cache when deleting an instance", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_LIST_INSTANCES, { +      response: { +        instances: [{ +          id: 'default', +          name: 'instance_name' +        } as MerchantBackend.Instances.Instance, +        { +          id: 'the_id', +          name: 'second_instance' +        } as MerchantBackend.Instances.Instance] +      }, +    }); + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const api = useAdminAPI(); +        const query = useBackendInstances(); +        return { query, api }; +      }, +      {}, +      [ +        ({ query, api }) => { +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            instances: [{ +              id: 'default', +              name: 'instance_name' +            }, { +              id: 'the_id', +              name: 'second_instance' +            }] +          }); + +          env.addRequestExpectation(API_DELETE_INSTANCE('the_id'), {}); +          env.addRequestExpectation(API_LIST_INSTANCES, { +            response: { +              instances: [{ +                id: 'default', +                name: 'instance_name' +              } as MerchantBackend.Instances.Instance] +            }, +          }); + +          api.deleteInstance('the_id'); +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            instances: [{ +              id: 'default', +              name: 'instance_name' +            }] +          }); +        }, +      ], env.buildTestingContext()); +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +    // const { result, waitForNextUpdate } = renderHook( +    //   () => { +    //     const api = useAdminAPI(); +    //     const query = useBackendInstances(); + +    //     return { query, api }; +    //   }, +    //   { wrapper: TestingContext } +    // ); + +    // expect(result.current).not.undefined; +    // if (!result.current) { +    //   return; +    // } +    // expect(result.current.query.loading).true; + +    // await waitForNextUpdate({ timeout: 1 }); + +    // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +    // expect(result.current.query.loading).false; + +    // expect(result.current?.query.ok).true; +    // if (!result.current?.query.ok) return; + +    // expect(result.current.query.data).equals({ +    //   instances: [{ +    //     id: 'default', +    //     name: 'instance_name' +    //   }, { +    //     id: 'the_id', +    //     name: 'second_instance' +    //   }] +    // }); + +    // env.addRequestExpectation(API_DELETE_INSTANCE('the_id'), {}); + +    // act(async () => { +    //   await result.current?.api.deleteInstance('the_id'); +    // }); + +    // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +    // env.addRequestExpectation(API_LIST_INSTANCES, { +    //   response: { +    //     instances: [{ +    //       id: 'default', +    //       name: 'instance_name' +    //     } as MerchantBackend.Instances.Instance] +    //   }, +    // }); + +    // expect(result.current.query.loading).false; + +    // await waitForNextUpdate({ timeout: 1 }); + +    // expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +    // expect(result.current.query.loading).false; +    // expect(result.current.query.ok).true; + +    // expect(result.current.query.data).equals({ +    //   instances: [{ +    //     id: 'default', +    //     name: 'instance_name' +    //   }] +    // }); +  }); + +  it("should evict cache when deleting (purge) an instance", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_LIST_INSTANCES, { +      response: { +        instances: [{ +          id: 'default', +          name: 'instance_name' +        } as MerchantBackend.Instances.Instance, +        { +          id: 'the_id', +          name: 'second_instance' +        } as MerchantBackend.Instances.Instance] +      }, +    }); + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const api = useAdminAPI(); +        const query = useBackendInstances(); +        return { query, api }; +      }, +      {}, +      [ +        ({ query, api }) => { +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            instances: [{ +              id: 'default', +              name: 'instance_name' +            }, { +              id: 'the_id', +              name: 'second_instance' +            }] +          }); + +          env.addRequestExpectation(API_DELETE_INSTANCE('the_id'), { +            qparam: { +              purge: 'YES' +            } +          }); +          env.addRequestExpectation(API_LIST_INSTANCES, { +            response: { +              instances: [{ +                id: 'default', +                name: 'instance_name' +              } as MerchantBackend.Instances.Instance] +            }, +          }); + +          api.purgeInstance('the_id') +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            instances: [{ +              id: 'default', +              name: 'instance_name' +            }] +          }); +        }, +      ], env.buildTestingContext()); +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +  }); +}); + +describe("instance management api interaction with listing", () => { + +  it("should evict cache when updating an instance", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_LIST_INSTANCES, { +      response: { +        instances: [{ +          id: 'managed', +          name: 'instance_name' +        } as MerchantBackend.Instances.Instance] +      }, +    }); + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const api = useManagementAPI('managed'); +        const query = useBackendInstances(); +        return { query, api }; +      }, +      {}, +      [ +        ({ query, api }) => { +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            instances: [{ +              id: 'managed', +              name: 'instance_name' +            }] +          }); + +          env.addRequestExpectation(API_UPDATE_INSTANCE_BY_ID('managed'), { +            request: { +              name: 'other_name' +            } as MerchantBackend.Instances.InstanceReconfigurationMessage, +          }); +          env.addRequestExpectation(API_LIST_INSTANCES, { +            response: { +              instances: [ +                { +                  id: 'managed', +                  name: 'other_name' +                } as MerchantBackend.Instances.Instance] +            }, +          }); + +          api.updateInstance({ +            name: 'other_name' +          } as MerchantBackend.Instances.InstanceConfigurationMessage); +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            instances: [{ +              id: 'managed', +              name: 'other_name' +            }] +          }); +        }, +      ], env.buildTestingContext()); +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +  }); + +}); + diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts index ab59487de..3c05472d0 100644 --- a/packages/merchant-backoffice-ui/src/hooks/instance.ts +++ b/packages/merchant-backoffice-ui/src/hooks/instance.ts @@ -15,14 +15,11 @@   */  import useSWR, { useSWRConfig } from "swr";  import { useBackendContext } from "../context/backend.js"; -import { useInstanceContext } from "../context/instance.js";  import { MerchantBackend } from "../declaration.js"; +import { HttpError, HttpResponse, HttpResponseOk } from "../utils/request.js";  import { -  fetcher, -  HttpError, -  HttpResponse, -  HttpResponseOk, -  request, +  useBackendBaseRequest, +  useBackendInstanceRequest,    useMatchMutate,  } from "./backend.js"; @@ -36,15 +33,14 @@ interface InstanceAPI {  }  export function useAdminAPI(): AdminAPI { -  const { url, token } = useBackendContext(); +  const { request } = useBackendBaseRequest();    const mutateAll = useMatchMutate();    const createInstance = async (      instance: MerchantBackend.Instances.InstanceConfigurationMessage,    ): Promise<void> => { -    await request(`${url}/management/instances`, { -      method: "post", -      token, +    await request(`/management/instances`, { +      method: "POST",        data: instance,      }); @@ -52,18 +48,16 @@ export function useAdminAPI(): AdminAPI {    };    const deleteInstance = async (id: string): Promise<void> => { -    await request(`${url}/management/instances/${id}`, { -      method: "delete", -      token, +    await request(`/management/instances/${id}`, { +      method: "DELETE",      });      mutateAll(/\/management\/instances/);    };    const purgeInstance = async (id: string): Promise<void> => { -    await request(`${url}/management/instances/${id}`, { -      method: "delete", -      token, +    await request(`/management/instances/${id}`, { +      method: "DELETE",        params: {          purge: "YES",        }, @@ -85,14 +79,14 @@ export interface AdminAPI {  export function useManagementAPI(instanceId: string): InstanceAPI {    const mutateAll = useMatchMutate(); -  const { url, token, updateLoginStatus } = useBackendContext(); +  const { updateToken } = useBackendContext(); +  const { request } = useBackendBaseRequest();    const updateInstance = async (      instance: MerchantBackend.Instances.InstanceReconfigurationMessage,    ): Promise<void> => { -    await request(`${url}/management/instances/${instanceId}`, { -      method: "patch", -      token, +    await request(`/management/instances/${instanceId}`, { +      method: "PATCH",        data: instance,      }); @@ -100,18 +94,16 @@ export function useManagementAPI(instanceId: string): InstanceAPI {    };    const deleteInstance = async (): Promise<void> => { -    await request(`${url}/management/instances/${instanceId}`, { -      method: "delete", -      token, +    await request(`/management/instances/${instanceId}`, { +      method: "DELETE",      });      mutateAll(/\/management\/instances/);    };    const clearToken = async (): Promise<void> => { -    await request(`${url}/management/instances/${instanceId}/auth`, { -      method: "post", -      token, +    await request(`/management/instances/${instanceId}/auth`, { +      method: "POST",        data: { method: "external" },      }); @@ -119,13 +111,12 @@ export function useManagementAPI(instanceId: string): InstanceAPI {    };    const setNewToken = async (newToken: string): Promise<void> => { -    await request(`${url}/management/instances/${instanceId}/auth`, { -      method: "post", -      token, +    await request(`/management/instances/${instanceId}/auth`, { +      method: "POST",        data: { method: "token", token: newToken },      }); -    updateLoginStatus(url, newToken); +    updateToken(newToken);      mutateAll(/\/management\/instances/);    }; @@ -139,71 +130,59 @@ export function useInstanceAPI(): InstanceAPI {      token: adminToken,      updateLoginStatus,    } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { url: baseUrl, token: adminToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { request } = useBackendInstanceRequest();    const updateInstance = async (      instance: MerchantBackend.Instances.InstanceReconfigurationMessage,    ): Promise<void> => { -    await request(`${url}/private/`, { -      method: "patch", -      token, +    await request(`/private/`, { +      method: "PATCH",        data: instance,      });      if (adminToken) mutate(["/private/instances", adminToken, baseUrl], null); -    mutate([`/private/`, token, url], null); +    mutate([`/private/`], null);    };    const deleteInstance = async (): Promise<void> => { -    await request(`${url}/private/`, { -      method: "delete", -      token: adminToken, +    await request(`/private/`, { +      method: "DELETE", +      // token: adminToken,      });      if (adminToken) mutate(["/private/instances", adminToken, baseUrl], null); -    mutate([`/private/`, token, url], null); +    mutate([`/private/`], null);    };    const clearToken = async (): Promise<void> => { -    await request(`${url}/private/auth`, { -      method: "post", -      token, +    await request(`/private/auth`, { +      method: "POST",        data: { method: "external" },      }); -    mutate([`/private/`, token, url], null); +    mutate([`/private/`], null);    };    const setNewToken = async (newToken: string): Promise<void> => { -    await request(`${url}/private/auth`, { -      method: "post", -      token, +    await request(`/private/auth`, { +      method: "POST",        data: { method: "token", token: newToken },      });      updateLoginStatus(baseUrl, newToken); -    mutate([`/private/`, token, url], null); +    mutate([`/private/`], null);    };    return { updateInstance, deleteInstance, setNewToken, clearToken };  }  export function useInstanceDetails(): HttpResponse<MerchantBackend.Instances.QueryInstancesResponse> { -  const { url: baseUrl, token: baseToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { url: baseUrl, token: baseToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { fetcher } = useBackendInstanceRequest();    const { data, error, isValidating } = useSWR<      HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>,      HttpError -  >([`/private/`, token, url], fetcher, { +  >([`/private/`], fetcher, {      refreshInterval: 0,      refreshWhenHidden: false,      revalidateOnFocus: false, @@ -225,17 +204,12 @@ type KYCStatus =    | { type: "redirect"; status: MerchantBackend.Instances.AccountKycRedirects };  export function useInstanceKYCDetails(): HttpResponse<KYCStatus> { -  const { url: baseUrl, token: baseToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { url: baseUrl, token: baseToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { fetcher } = useBackendInstanceRequest();    const { data, error } = useSWR<      HttpResponseOk<MerchantBackend.Instances.AccountKycRedirects>,      HttpError -  >([`/private/kyc`, token, url], fetcher, { +  >([`/private/kyc`], fetcher, {      refreshInterval: 5000,      refreshWhenHidden: false,      revalidateOnFocus: false, @@ -258,12 +232,12 @@ export function useInstanceKYCDetails(): HttpResponse<KYCStatus> {  export function useManagedInstanceDetails(    instanceId: string,  ): HttpResponse<MerchantBackend.Instances.QueryInstancesResponse> { -  const { url, token } = useBackendContext(); +  const { request } = useBackendBaseRequest();    const { data, error, isValidating } = useSWR<      HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>,      HttpError -  >([`/management/instances/${instanceId}`, token, url], fetcher, { +  >([`/management/instances/${instanceId}`], request, {      refreshInterval: 0,      refreshWhenHidden: false,      revalidateOnFocus: false, @@ -281,13 +255,12 @@ export function useManagedInstanceDetails(  }  export function useBackendInstances(): HttpResponse<MerchantBackend.Instances.InstancesResponse> { -  const { url } = useBackendContext(); -  const { token } = useInstanceContext(); +  const { request } = useBackendBaseRequest();    const { data, error, isValidating } = useSWR<      HttpResponseOk<MerchantBackend.Instances.InstancesResponse>,      HttpError -  >(["/management/instances", token, url], fetcher); +  >(["/management/instances"], request);    if (isValidating) return { loading: true, data: data?.data };    if (data) return data; diff --git a/packages/merchant-backoffice-ui/src/hooks/order.test.ts b/packages/merchant-backoffice-ui/src/hooks/order.test.ts new file mode 100644 index 000000000..be4d1d804 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/hooks/order.test.ts @@ -0,0 +1,579 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { tests } from "@gnu-taler/web-util/lib/index.browser"; +import { expect } from "chai"; +import { MerchantBackend } from "../declaration.js"; +import { useInstanceOrders, useOrderAPI, useOrderDetails } from "./order.js"; +import { ApiMockEnvironment } from "./testing.js"; +import { +  API_CREATE_ORDER, +  API_DELETE_ORDER, +  API_FORGET_ORDER_BY_ID, +  API_GET_ORDER_BY_ID, +  API_LIST_ORDERS, API_REFUND_ORDER_BY_ID +} from "./urls.js"; + +describe("order api interaction with listing", () => { + +  it("should evict cache when creating an order", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: 0, paid: "yes" }, +      response: { +        orders: [{ order_id: "1" } as MerchantBackend.Orders.OrderHistoryEntry], +      }, +    }); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: -20, paid: "yes" }, +      response: { +        orders: [{ order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry], +      }, +    }); + + +    const newDate = (d: Date) => { +      //console.log("new date", d); +    }; + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const query = useInstanceOrders({ paid: "yes" }, newDate); +        const api = useOrderAPI(); +        return { query, api }; +      }, +      {}, +      [ +        ({ query, api }) => { +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).undefined; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            orders: [{ order_id: "1" }, { order_id: "2" }], +          }); + +          env.addRequestExpectation(API_CREATE_ORDER, { +            request: { +              order: { amount: "ARS:12", summary: "pay me" }, +            }, +            response: { order_id: "3" }, +          }); + +          env.addRequestExpectation(API_LIST_ORDERS, { +            qparam: { delta: 0, paid: "yes" }, +            response: { +              orders: [{ order_id: "1" } as any], +            }, +          }); + +          env.addRequestExpectation(API_LIST_ORDERS, { +            qparam: { delta: -20, paid: "yes" }, +            response: { +              orders: [{ order_id: "2" } as any, { order_id: "3" } as any], +            }, +          }); + +          api.createOrder({ +            order: { amount: "ARS:12", summary: "pay me" }, +          } as any); +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +          expect(query.loading).undefined; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            orders: [{ order_id: "1" }, { order_id: "2" }, { order_id: "3" }], +          }); +        }, +      ], env.buildTestingContext()); +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +  }); + +  it("should evict cache when doing a refund", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: 0, paid: "yes" }, +      response: { +        orders: [{ order_id: "1", amount: 'EUR:12', refundable: true } as MerchantBackend.Orders.OrderHistoryEntry], +      }, +    }); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: -20, paid: "yes" }, +      response: { orders: [], }, +    }); + + +    const newDate = (d: Date) => { +      //console.log("new date", d); +    }; + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const query = useInstanceOrders({ paid: "yes" }, newDate); +        const api = useOrderAPI(); +        return { query, api }; +      }, +      {}, +      [ +        ({ query, api }) => { +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +          expect(query.loading).undefined; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            orders: [{ +              order_id: "1", +              amount: 'EUR:12', +              refundable: true, +            }], +          }); +          env.addRequestExpectation(API_REFUND_ORDER_BY_ID('1'), { +            request: { +              reason: 'double pay', +              refund: 'EUR:1' +            }, +          }); + +          env.addRequestExpectation(API_LIST_ORDERS, { +            qparam: { delta: 0, paid: "yes" }, +            response: { +              orders: [{ order_id: "1", amount: 'EUR:12', refundable: false } as any], +            }, +          }); + +          env.addRequestExpectation(API_LIST_ORDERS, { +            qparam: { delta: -20, paid: "yes" }, +            response: { orders: [], }, +          }); + +          api.refundOrder('1', { +            reason: 'double pay', +            refund: 'EUR:1' +          }); +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +          expect(query.loading).undefined; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            orders: [{ +              order_id: "1", +              amount: 'EUR:12', +              refundable: false, +            }], +          }); +        }, +      ], env.buildTestingContext()); + +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +  }); + +  it("should evict cache when deleting an order", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: 0, paid: "yes" }, +      response: { +        orders: [{ order_id: "1" } as MerchantBackend.Orders.OrderHistoryEntry], +      }, +    }); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: -20, paid: "yes" }, +      response: { +        orders: [{ order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry], +      }, +    }); + + +    const newDate = (d: Date) => { +      //console.log("new date", d); +    }; + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const query = useInstanceOrders({ paid: "yes" }, newDate); +        const api = useOrderAPI(); +        return { query, api }; +      }, +      {}, +      [ +        ({ query, api }) => { +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).undefined; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            orders: [{ order_id: "1" }, { order_id: "2" }], +          }); + +          env.addRequestExpectation(API_DELETE_ORDER('1'), {}); + +          env.addRequestExpectation(API_LIST_ORDERS, { +            qparam: { delta: 0, paid: "yes" }, +            response: { +              orders: [], +            }, +          }); + +          env.addRequestExpectation(API_LIST_ORDERS, { +            qparam: { delta: -20, paid: "yes" }, +            response: { +              orders: [{ order_id: "2" } as any], +            }, +          }); + +          api.deleteOrder('1'); +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).undefined; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            orders: [{ order_id: "2" }], +          }); +        }, +      ], env.buildTestingContext()); +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +  }); + +}); + +describe("order api interaction with details", () => { + +  it("should evict cache when doing a refund", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_GET_ORDER_BY_ID('1'), { +      // qparam: { delta: 0, paid: "yes" }, +      response: { +        summary: 'description', +        refund_amount: 'EUR:0', +      } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, +    }); + +    const newDate = (d: Date) => { +      //console.log("new date", d); +    }; + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const query = useOrderDetails('1') +        const api = useOrderAPI(); +        return { query, api }; +      }, +      {}, +      [ +        ({ query, api }) => { +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            summary: 'description', +            refund_amount: 'EUR:0', +          }); +          env.addRequestExpectation(API_REFUND_ORDER_BY_ID('1'), { +            request: { +              reason: 'double pay', +              refund: 'EUR:1' +            }, +          }); + +          env.addRequestExpectation(API_GET_ORDER_BY_ID('1'), { +            response: { +              summary: 'description', +              refund_amount: 'EUR:1', +            } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, +          }); + +          api.refundOrder('1', { +            reason: 'double pay', +            refund: 'EUR:1' +          }) +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            summary: 'description', +            refund_amount: 'EUR:1', +          }); +        }, +      ], env.buildTestingContext()); + +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +  }) + +  it("should evict cache when doing a forget", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_GET_ORDER_BY_ID('1'), { +      // qparam: { delta: 0, paid: "yes" }, +      response: { +        summary: 'description', +        refund_amount: 'EUR:0', +      } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, +    }); + +    const newDate = (d: Date) => { +      //console.log("new date", d); +    }; + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const query = useOrderDetails('1') +        const api = useOrderAPI(); +        return { query, api }; +      }, +      {}, +      [ +        ({ query, api }) => { +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            summary: 'description', +            refund_amount: 'EUR:0', +          }); +          env.addRequestExpectation(API_FORGET_ORDER_BY_ID('1'), { +            request: { +              fields: ['$.summary'] +            }, +          }); + +          env.addRequestExpectation(API_GET_ORDER_BY_ID('1'), { +            response: { +              summary: undefined, +            } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, +          }); + +          api.forgetOrder('1', { +            fields: ['$.summary'] +          }) +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            summary: undefined, +          }); +        }, +      ], env.buildTestingContext()); +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +  }) +}) + +describe("order listing pagination", () => { + +  it("should not load more if has reach the end", async () => { +    const env = new ApiMockEnvironment(); +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: 20, wired: "yes", date_ms: 12 }, +      response: { +        orders: [{ order_id: "1" } as any], +      }, +    }); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: -20, wired: "yes", date_ms: 13 }, +      response: { +        orders: [{ order_id: "2" } as any], +      }, +    }); + + +    const newDate = (d: Date) => { +      //console.log("new date", d); +    }; + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const date = new Date(12); +        const query = useInstanceOrders({ wired: "yes", date }, newDate) +        const api = useOrderAPI(); +        return { query, api }; +      }, +      {}, +      [ +        ({ query, api }) => { +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).undefined; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            orders: [{ order_id: "1" }, { order_id: "2" }], +          }); +          expect(query.isReachingEnd).true +          expect(query.isReachingStart).true + +          // should not trigger new state update or query +          query.loadMore() +          query.loadMorePrev(); +        }, +      ], env.buildTestingContext()); + +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +  }); + +  it("should load more if result brings more that PAGE_SIZE", async () => { +    const env = new ApiMockEnvironment(); + +    const ordersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({ order_id: String(i) })) +    const ordersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({ order_id: String(i + 20) })) +    const ordersFrom20to0 = [...ordersFrom0to20].reverse() + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: 20, wired: "yes", date_ms: 12 }, +      response: { +        orders: ordersFrom0to20, +      }, +    }); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: -20, wired: "yes", date_ms: 13 }, +      response: { +        orders: ordersFrom20to40, +      }, +    }); + +    const newDate = (d: Date) => { +      //console.log("new date", d); +    }; + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const date = new Date(12); +        const query = useInstanceOrders({ wired: "yes", date }, newDate) +        const api = useOrderAPI(); +        return { query, api }; +      }, +      {}, +      [ +        ({ query, api }) => { +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).undefined; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            orders: [...ordersFrom20to0, ...ordersFrom20to40], +          }); +          expect(query.isReachingEnd).false +          expect(query.isReachingStart).false + +          env.addRequestExpectation(API_LIST_ORDERS, { +            qparam: { delta: -40, wired: "yes", date_ms: 13 }, +            response: { +              orders: [...ordersFrom20to40, { order_id: '41' }], +            }, +          }); + +          query.loadMore() +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(query.loading).undefined; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            orders: [...ordersFrom20to0, ...ordersFrom20to40, { order_id: '41' }], +          }); + +          env.addRequestExpectation(API_LIST_ORDERS, { +            qparam: { delta: 40, wired: "yes", date_ms: 12 }, +            response: { +              orders: [...ordersFrom0to20, { order_id: '-1' }], +            }, +          }); + +          query.loadMorePrev() +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(query.loading).undefined; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            orders: [{ order_id: '-1' }, ...ordersFrom20to0, ...ordersFrom20to40, { order_id: '41' }], +          }); +        }, +      ], env.buildTestingContext()); +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +  }); + + +}); diff --git a/packages/merchant-backoffice-ui/src/hooks/order.ts b/packages/merchant-backoffice-ui/src/hooks/order.ts index d1e26b671..0bea6b963 100644 --- a/packages/merchant-backoffice-ui/src/hooks/order.ts +++ b/packages/merchant-backoffice-ui/src/hooks/order.ts @@ -14,20 +14,16 @@   GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>   */  import { useEffect, useState } from "preact/hooks"; -import useSWR, { useSWRConfig } from "swr"; -import { useBackendContext } from "../context/backend.js"; -import { useInstanceContext } from "../context/instance.js"; +import useSWR from "swr";  import { MerchantBackend } from "../declaration.js";  import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";  import { -  fetcher,    HttpError,    HttpResponse,    HttpResponseOk,    HttpResponsePaginated, -  request, -  useMatchMutate, -} from "./backend.js"; +} from "../utils/request.js"; +import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";  export interface OrderAPI {    //FIXME: add OutOfStockResponse on 410 @@ -48,52 +44,17 @@ export interface OrderAPI {  type YesOrNo = "yes" | "no"; -export function orderFetcher<T>( -  url: string, -  token: string, -  backend: string, -  paid?: YesOrNo, -  refunded?: YesOrNo, -  wired?: YesOrNo, -  searchDate?: Date, -  delta?: number, -): Promise<HttpResponseOk<T>> { -  const date_ms = -    delta && delta < 0 && searchDate -      ? searchDate.getTime() + 1 -      : searchDate?.getTime(); -  const params: any = {}; -  if (paid !== undefined) params.paid = paid; -  if (delta !== undefined) params.delta = delta; -  if (refunded !== undefined) params.refunded = refunded; -  if (wired !== undefined) params.wired = wired; -  if (date_ms !== undefined) params.date_ms = date_ms; -  return request<T>(`${backend}${url}`, { token, params }); -} -  export function useOrderAPI(): OrderAPI {    const mutateAll = useMatchMutate(); -  const { url: baseUrl, token: adminToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { -        url: baseUrl, -        token: adminToken, -      } -    : { -        url: `${baseUrl}/instances/${id}`, -        token: instanceToken, -      }; +  const { request } = useBackendInstanceRequest();    const createOrder = async (      data: MerchantBackend.Orders.PostOrderRequest,    ): Promise<HttpResponseOk<MerchantBackend.Orders.PostOrderResponse>> => {      const res = await request<MerchantBackend.Orders.PostOrderResponse>( -      `${url}/private/orders`, +      `/private/orders`,        { -        method: "post", -        token, +        method: "POST",          data,        },      ); @@ -107,10 +68,9 @@ export function useOrderAPI(): OrderAPI {    ): Promise<HttpResponseOk<MerchantBackend.Orders.MerchantRefundResponse>> => {      mutateAll(/@"\/private\/orders"@/);      const res = request<MerchantBackend.Orders.MerchantRefundResponse>( -      `${url}/private/orders/${orderId}/refund`, +      `/private/orders/${orderId}/refund`,        { -        method: "post", -        token, +        method: "POST",          data,        },      ); @@ -125,9 +85,8 @@ export function useOrderAPI(): OrderAPI {      data: MerchantBackend.Orders.ForgetRequest,    ): Promise<HttpResponseOk<void>> => {      mutateAll(/@"\/private\/orders"@/); -    const res = request<void>(`${url}/private/orders/${orderId}/forget`, { -      method: "patch", -      token, +    const res = request<void>(`/private/orders/${orderId}/forget`, { +      method: "PATCH",        data,      });      // we may be forgetting some fields that are pare of the listing, so we must evict everything @@ -138,9 +97,8 @@ export function useOrderAPI(): OrderAPI {      orderId: string,    ): Promise<HttpResponseOk<void>> => {      mutateAll(/@"\/private\/orders"@/); -    const res = request<void>(`${url}/private/orders/${orderId}`, { -      method: "delete", -      token, +    const res = request<void>(`/private/orders/${orderId}`, { +      method: "DELETE",      });      await mutateAll(/.*private\/orders.*/);      return res; @@ -150,10 +108,9 @@ export function useOrderAPI(): OrderAPI {      orderId: string,    ): Promise<HttpResponseOk<string>> => {      return request<MerchantBackend.Orders.MerchantOrderStatusResponse>( -      `${url}/private/orders/${orderId}`, +      `/private/orders/${orderId}`,        { -        method: "get", -        token, +        method: "GET",        },      ).then((res) => {        const url = @@ -172,17 +129,12 @@ export function useOrderAPI(): OrderAPI {  export function useOrderDetails(    oderId: string,  ): HttpResponse<MerchantBackend.Orders.MerchantOrderStatusResponse> { -  const { url: baseUrl, token: baseToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { url: baseUrl, token: baseToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { fetcher } = useBackendInstanceRequest();    const { data, error, isValidating } = useSWR<      HttpResponseOk<MerchantBackend.Orders.MerchantOrderStatusResponse>,      HttpError -  >([`/private/orders/${oderId}`, token, url], fetcher, { +  >([`/private/orders/${oderId}`], fetcher, {      refreshInterval: 0,      refreshWhenHidden: false,      revalidateOnFocus: false, @@ -207,12 +159,7 @@ export function useInstanceOrders(    args?: InstanceOrderFilter,    updateFilter?: (d: Date) => void,  ): HttpResponsePaginated<MerchantBackend.Orders.OrderHistory> { -  const { url: baseUrl, token: baseToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { url: baseUrl, token: baseToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { orderFetcher } = useBackendInstanceRequest();    const [pageBefore, setPageBefore] = useState(1);    const [pageAfter, setPageAfter] = useState(1); @@ -233,8 +180,6 @@ export function useInstanceOrders(    } = useSWR<HttpResponseOk<MerchantBackend.Orders.OrderHistory>, HttpError>(      [        `/private/orders`, -      token, -      url,        args?.paid,        args?.refunded,        args?.wired, @@ -250,8 +195,6 @@ export function useInstanceOrders(    } = useSWR<HttpResponseOk<MerchantBackend.Orders.OrderHistory>, HttpError>(      [        `/private/orders`, -      token, -      url,        args?.paid,        args?.refunded,        args?.wired, @@ -314,9 +257,9 @@ export function useInstanceOrders(      !beforeData || !afterData        ? []        : (beforeData || lastBefore).data.orders -          .slice() -          .reverse() -          .concat((afterData || lastAfter).data.orders); +        .slice() +        .reverse() +        .concat((afterData || lastAfter).data.orders);    if (loadingAfter || loadingBefore) return { loading: true, data: { orders } };    if (beforeData && afterData) {      return { ok: true, data: { orders }, ...pagination }; diff --git a/packages/merchant-backoffice-ui/src/hooks/product.test.ts b/packages/merchant-backoffice-ui/src/hooks/product.test.ts new file mode 100644 index 000000000..a182b28f4 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/hooks/product.test.ts @@ -0,0 +1,326 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { tests } from "@gnu-taler/web-util/lib/index.browser"; +import { expect } from "chai"; +import { MerchantBackend } from "../declaration.js"; +import { useInstanceProducts, useProductAPI, useProductDetails } from "./product.js"; +import { ApiMockEnvironment } from "./testing.js"; +import { +  API_CREATE_PRODUCT, +  API_DELETE_PRODUCT, API_GET_PRODUCT_BY_ID, +  API_LIST_PRODUCTS, +  API_UPDATE_PRODUCT_BY_ID +} from "./urls.js"; + +describe("product api interaction with listing", () => { +  it("should evict cache when creating a product", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_LIST_PRODUCTS, { +      response: { +        products: [{ product_id: "1234" }], +      }, +    }); +    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { +      response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, +    }); + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const query = useInstanceProducts(); +        const api = useProductAPI(); +        return { query, api }; +      }, +      {}, +      [ +        ({ query, api }) => { +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(query.loading).undefined; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals([ +            { id: "1234", price: "ARS:12" }, +          ]); + +          env.addRequestExpectation(API_CREATE_PRODUCT, { +            request: { price: "ARS:23" } as MerchantBackend.Products.ProductAddDetail, +          }); + +          env.addRequestExpectation(API_LIST_PRODUCTS, { +            response: { +              products: [{ product_id: "1234" }, { product_id: "2345" }], +            }, +          }); +          env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { +            response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, +          }); +          env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { +            response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, +          }); +          env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), { +            response: { price: "ARS:23" } as MerchantBackend.Products.ProductDetail, +          }); + +          api.createProduct({ +            price: "ARS:23", +          } as any) + +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).undefined; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals([ +            { +              id: "1234", +              price: "ARS:12", +            }, +            { +              id: "2345", +              price: "ARS:23", +            }, +          ]); +        }, +      ], env.buildTestingContext()); + +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +  }); + +  it("should evict cache when updating a product", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_LIST_PRODUCTS, { +      response: { +        products: [{ product_id: "1234" }], +      }, +    }); +    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { +      response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, +    }); + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const query = useInstanceProducts(); +        const api = useProductAPI(); +        return { query, api }; +      }, +      {}, +      [ +        ({ query, api }) => { +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).undefined; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals([ +            { id: "1234", price: "ARS:12" }, +          ]); + +          env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("1234"), { +            request: { price: "ARS:13" } as MerchantBackend.Products.ProductPatchDetail, +          }); + +          env.addRequestExpectation(API_LIST_PRODUCTS, { +            response: { +              products: [{ product_id: "1234" }], +            }, +          }); +          env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { +            response: { price: "ARS:13" } as MerchantBackend.Products.ProductDetail, +          }); + +          api.updateProduct("1234", { +            price: "ARS:13", +          } as any) + +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).undefined; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals([ +            { +              id: "1234", +              price: "ARS:13", +            }, +          ]); +        }, +      ], env.buildTestingContext()); + +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +  }); + +  it("should evict cache when deleting a product", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_LIST_PRODUCTS, { +      response: { +        products: [{ product_id: "1234" }, { product_id: "2345" }], +      }, +    }); +    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { +      response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, +    }); +    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), { +      response: { price: "ARS:23" } as MerchantBackend.Products.ProductDetail, +    }); + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const query = useInstanceProducts(); +        const api = useProductAPI(); +        return { query, api }; +      }, +      {}, +      [ +        ({ query, api }) => { +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(query.loading).undefined; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals([ +            { id: "1234", price: "ARS:12" }, +            { id: "2345", price: "ARS:23" }, +          ]); + +          env.addRequestExpectation(API_DELETE_PRODUCT("2345"), {}); + +          env.addRequestExpectation(API_LIST_PRODUCTS, { +            response: { +              products: [{ product_id: "1234" }], +            }, +          }); + +          env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { +            response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, +          }); +          api.deleteProduct("2345"); +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).undefined; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals([ +            { id: "1234", price: "ARS:12" }, +          ]); +        }, +      ], env.buildTestingContext()); + +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +  }); + +}); + +describe("product api interaction with details", () => { +  it("should evict cache when updating a product", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), { +      response: { +        description: "this is a description", +      } as MerchantBackend.Products.ProductDetail, +    }); + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const query = useProductDetails("12"); +        const api = useProductAPI(); +        return { query, api }; +      }, +      {}, +      [ +        ({ query, api }) => { +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            description: "this is a description", +          }); + +          env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("12"), { +            request: { description: "other description" } as MerchantBackend.Products.ProductPatchDetail, +          }); + +          env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), { +            response: { +              description: "other description", +            } as MerchantBackend.Products.ProductDetail, +          }); + +          api.updateProduct("12", { +            description: "other description", +          } as any); + +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            description: "other description", +          }); +        }, +      ], env.buildTestingContext()); + +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +  }) +})
\ No newline at end of file diff --git a/packages/merchant-backoffice-ui/src/hooks/product.ts b/packages/merchant-backoffice-ui/src/hooks/product.ts index fb7889834..af8ad74f3 100644 --- a/packages/merchant-backoffice-ui/src/hooks/product.ts +++ b/packages/merchant-backoffice-ui/src/hooks/product.ts @@ -14,18 +14,9 @@   GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>   */  import useSWR, { useSWRConfig } from "swr"; -import { useBackendContext } from "../context/backend.js"; -import { useInstanceContext } from "../context/instance.js";  import { MerchantBackend, WithId } from "../declaration.js"; -import { -  fetcher, -  HttpError, -  HttpResponse, -  HttpResponseOk, -  multiFetcher, -  request, -  useMatchMutate, -} from "./backend.js"; +import { HttpError, HttpResponse, HttpResponseOk } from "../utils/request.js"; +import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";  export interface ProductAPI {    createProduct: ( @@ -45,19 +36,14 @@ export interface ProductAPI {  export function useProductAPI(): ProductAPI {    const mutateAll = useMatchMutate();    const { mutate } = useSWRConfig(); -  const { url: baseUrl, token: adminToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); -  const { url, token } = !admin -    ? { url: baseUrl, token: adminToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { request } = useBackendInstanceRequest();    const createProduct = async (      data: MerchantBackend.Products.ProductAddDetail,    ): Promise<void> => { -    const res = await request(`${url}/private/products`, { -      method: "post", -      token, +    const res = await request(`/private/products`, { +      method: "POST",        data,      }); @@ -68,9 +54,8 @@ export function useProductAPI(): ProductAPI {      productId: string,      data: MerchantBackend.Products.ProductPatchDetail,    ): Promise<void> => { -    const r = await request(`${url}/private/products/${productId}`, { -      method: "patch", -      token, +    const r = await request(`/private/products/${productId}`, { +      method: "PATCH",        data,      }); @@ -78,20 +63,18 @@ export function useProductAPI(): ProductAPI {    };    const deleteProduct = async (productId: string): Promise<void> => { -    await request(`${url}/private/products/${productId}`, { -      method: "delete", -      token, +    await request(`/private/products/${productId}`, { +      method: "DELETE",      }); -    await mutate([`/private/products`, token, url]); +    await mutate([`/private/products`]);    };    const lockProduct = async (      productId: string,      data: MerchantBackend.Products.LockRequest,    ): Promise<void> => { -    await request(`${url}/private/products/${productId}/lock`, { -      method: "post", -      token, +    await request(`/private/products/${productId}/lock`, { +      method: "POST",        data,      }); @@ -104,17 +87,12 @@ export function useProductAPI(): ProductAPI {  export function useInstanceProducts(): HttpResponse<    (MerchantBackend.Products.ProductDetail & WithId)[]  > { -  const { url: baseUrl, token: baseToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { url: baseUrl, token: baseToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { fetcher, multiFetcher } = useBackendInstanceRequest();    const { data: list, error: listError } = useSWR<      HttpResponseOk<MerchantBackend.Products.InventorySummaryResponse>,      HttpError -  >([`/private/products`, token, url], fetcher, { +  >([`/private/products`], fetcher, {      refreshInterval: 0,      refreshWhenHidden: false,      revalidateOnFocus: false, @@ -128,7 +106,7 @@ export function useInstanceProducts(): HttpResponse<    const { data: products, error: productError } = useSWR<      HttpResponseOk<MerchantBackend.Products.ProductDetail>[],      HttpError -  >([paths, token, url], multiFetcher, { +  >([paths], multiFetcher, {      refreshInterval: 0,      refreshWhenHidden: false,      revalidateOnFocus: false, @@ -144,7 +122,7 @@ export function useInstanceProducts(): HttpResponse<        //take the id from the queried url        return {          ...d.data, -        id: d.info?.url.replace(/.*\/private\/products\//, "") || "", +        id: d.info?.url.href.replace(/.*\/private\/products\//, "") || "",        };      });      return { ok: true, data: dataWithId }; @@ -155,23 +133,12 @@ export function useInstanceProducts(): HttpResponse<  export function useProductDetails(    productId: string,  ): HttpResponse<MerchantBackend.Products.ProductDetail> { -  const { url: baseUrl, token: baseToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { -        url: baseUrl, -        token: baseToken, -      } -    : { -        url: `${baseUrl}/instances/${id}`, -        token: instanceToken, -      }; +  const { fetcher } = useBackendInstanceRequest();    const { data, error, isValidating } = useSWR<      HttpResponseOk<MerchantBackend.Products.ProductDetail>,      HttpError -  >([`/private/products/${productId}`, token, url], fetcher, { +  >([`/private/products/${productId}`], fetcher, {      refreshInterval: 0,      refreshWhenHidden: false,      revalidateOnFocus: false, diff --git a/packages/merchant-backoffice-ui/src/hooks/reserve.test.ts b/packages/merchant-backoffice-ui/src/hooks/reserve.test.ts new file mode 100644 index 000000000..da0e054e5 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/hooks/reserve.test.ts @@ -0,0 +1,431 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { expect } from "chai"; +import { MerchantBackend } from "../declaration.js"; +import { +  useInstanceReserves, +  useReserveDetails, +  useReservesAPI, +  useTipDetails +} from "./reserves.js"; +import { ApiMockEnvironment } from "./testing.js"; +import { +  API_AUTHORIZE_TIP, +  API_AUTHORIZE_TIP_FOR_RESERVE, +  API_CREATE_RESERVE, +  API_DELETE_RESERVE, +  API_GET_RESERVE_BY_ID, +  API_GET_TIP_BY_ID, +  API_LIST_RESERVES +} from "./urls.js"; +import { tests } from "@gnu-taler/web-util/lib/index.browser"; + +describe("reserve api interaction with listing", () => { +  it("should evict cache when creating a reserve", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_LIST_RESERVES, { +      response: { +        reserves: [ +          { +            reserve_pub: "11", +          } as MerchantBackend.Tips.ReserveStatusEntry, +        ], +      }, +    }); + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const api = useReservesAPI(); +        const query = useInstanceReserves(); +        return { query, api }; +      }, +      {}, +      [ +        ({ query, api }) => { +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            reserves: [{ reserve_pub: "11" }], +          }); + +          env.addRequestExpectation(API_CREATE_RESERVE, { +            request: { +              initial_balance: "ARS:3333", +              exchange_url: "http://url", +              wire_method: "iban", +            }, +            response: { +              reserve_pub: "22", +              payto_uri: "payto", +            }, +          }); + +          env.addRequestExpectation(API_LIST_RESERVES, { +            response: { +              reserves: [ +                { +                  reserve_pub: "11", +                } as MerchantBackend.Tips.ReserveStatusEntry, +                { +                  reserve_pub: "22", +                } as MerchantBackend.Tips.ReserveStatusEntry, +              ], +            }, +          }); + +          api.createReserve({ +            initial_balance: "ARS:3333", +            exchange_url: "http://url", +            wire_method: "iban", +          }) +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; +          expect(query.ok).true; +          if (!query.ok) return; + +          expect(query.data).deep.equals({ +            reserves: [ +              { +                reserve_pub: "11", +              } as MerchantBackend.Tips.ReserveStatusEntry, +              { +                reserve_pub: "22", +              } as MerchantBackend.Tips.ReserveStatusEntry, +            ], +          }); +        }, +      ], env.buildTestingContext()); + +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +  }); + +  it("should evict cache when deleting a reserve", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_LIST_RESERVES, { +      response: { +        reserves: [ +          { +            reserve_pub: "11", +          } as MerchantBackend.Tips.ReserveStatusEntry, +          { +            reserve_pub: "22", +          } as MerchantBackend.Tips.ReserveStatusEntry, +          { +            reserve_pub: "33", +          } as MerchantBackend.Tips.ReserveStatusEntry, +        ], +      }, +    }); + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const api = useReservesAPI(); +        const query = useInstanceReserves(); +        return { query, api }; +      }, +      {}, +      [ +        ({ query, api }) => { +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }) + +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            reserves: [ +              { reserve_pub: "11" }, +              { reserve_pub: "22" }, +              { reserve_pub: "33" }, +            ], +          }); + +          env.addRequestExpectation(API_DELETE_RESERVE("11"), {}); +          env.addRequestExpectation(API_LIST_RESERVES, { +            response: { +              reserves: [ +                { +                  reserve_pub: "22", +                } as MerchantBackend.Tips.ReserveStatusEntry, +                { +                  reserve_pub: "33", +                } as MerchantBackend.Tips.ReserveStatusEntry, +              ], +            }, +          }); + +          api.deleteReserve("11") +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }) +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            reserves: [ +              { reserve_pub: "22" }, +              { reserve_pub: "33" }, +            ], +          }); +        }, +      ], env.buildTestingContext()); + +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +  }); +}); + +describe("reserve api interaction with details", () => { +  it("should evict cache when adding a tip for a specific reserve", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { +      response: { +        payto_uri: "payto://here", +        tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }], +      } as MerchantBackend.Tips.ReserveDetail, +      qparam: { +        tips: "yes" +      } +    }); + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const api = useReservesAPI(); +        const query = useReserveDetails("11"); +        return { query, api }; +      }, +      {}, +      [ +        ({ query, api }) => { +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            payto_uri: "payto://here", +            tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }], +          }); + +          env.addRequestExpectation(API_AUTHORIZE_TIP_FOR_RESERVE("11"), { +            request: { +              amount: "USD:12", +              justification: "not", +              next_url: "http://taler.net", +            }, +            response: { +              tip_id: "id2", +              taler_tip_uri: "uri", +              tip_expiration: { t_s: 1 }, +              tip_status_url: "url", +            } +          },); + +          env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { +            response: { +              payto_uri: "payto://here", +              tips: [ +                { reason: "why?", tip_id: "id1", total_amount: "USD:10" }, +                { reason: "not", tip_id: "id2", total_amount: "USD:12" }, +              ], +            } as MerchantBackend.Tips.ReserveDetail, +            qparam: { +              tips: "yes" +            } +          }); + +          api.authorizeTipReserve("11", { +            amount: "USD:12", +            justification: "not", +            next_url: "http://taler.net", +          }) +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; + +          expect(query.loading).false; +          expect(query.ok).true; +          if (!query.ok) return; + +          expect(query.data).deep.equals({ +            payto_uri: "payto://here", +            tips: [ +              { reason: "why?", tip_id: "id1", total_amount: "USD:10" }, +              { reason: "not", tip_id: "id2", total_amount: "USD:12" }, +            ], +          }); +        }, +      ], env.buildTestingContext()); + +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +  }); + +  it("should evict cache when adding a tip for a random reserve", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { +      response: { +        payto_uri: "payto://here", +        tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }], +      } as MerchantBackend.Tips.ReserveDetail, +      qparam: { +        tips: "yes" +      } +    }); + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const api = useReservesAPI(); +        const query = useReserveDetails("11"); +        return { query, api }; +      }, +      {}, +      [ +        ({ query, api }) => { +          expect(query.loading).true; +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            payto_uri: "payto://here", +            tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }], +          }); + +          env.addRequestExpectation(API_AUTHORIZE_TIP, { +            request: { +              amount: "USD:12", +              justification: "not", +              next_url: "http://taler.net", +            }, +            response: { +              tip_id: "id2", +              taler_tip_uri: "uri", +              tip_expiration: { t_s: 1 }, +              tip_status_url: "url", +            }, +          }); + +          env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { +            response: { +              payto_uri: "payto://here", +              tips: [ +                { reason: "why?", tip_id: "id1", total_amount: "USD:10" }, +                { reason: "not", tip_id: "id2", total_amount: "USD:12" }, +              ], +            } as MerchantBackend.Tips.ReserveDetail, +            qparam: { +              tips: "yes" +            } +          }); + +          api.authorizeTip({ +            amount: "USD:12", +            justification: "not", +            next_url: "http://taler.net", +          }); + +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).false; +          expect(query.ok).true; +          if (!query.ok) return; + +          expect(query.data).deep.equals({ +            payto_uri: "payto://here", +            tips: [ +              { reason: "why?", tip_id: "id1", total_amount: "USD:10" }, +              { reason: "not", tip_id: "id2", total_amount: "USD:12" }, +            ], +          }); +        }, +      ], env.buildTestingContext()); + +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +  }); +}); + +describe("reserve api interaction with tip details", () => { + +  it("should list tips", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_GET_TIP_BY_ID("11"), { +      response: { +        total_picked_up: "USD:12", +        reason: "not", +      } as MerchantBackend.Tips.TipDetails, +      qparam: { +        pickups: "yes" +      } +    }); + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const query = useTipDetails("11"); +        return { query }; +      }, +      {}, +      [ +        ({ query }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +          expect(query.loading).true; +        }, +        ({ query }) => { +          expect(query.loading).false; +          expect(query.ok).true +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            total_picked_up: "USD:12", +            reason: "not", +          }); +        }, +      ], env.buildTestingContext()); + +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); + +  }); +}); diff --git a/packages/merchant-backoffice-ui/src/hooks/reserves.ts b/packages/merchant-backoffice-ui/src/hooks/reserves.ts index f6d77f113..dc127af13 100644 --- a/packages/merchant-backoffice-ui/src/hooks/reserves.ts +++ b/packages/merchant-backoffice-ui/src/hooks/reserves.ts @@ -14,27 +14,14 @@   GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>   */  import useSWR, { useSWRConfig } from "swr"; -import { useBackendContext } from "../context/backend.js"; -import { useInstanceContext } from "../context/instance.js";  import { MerchantBackend } from "../declaration.js"; -import { -  fetcher, -  HttpError, -  HttpResponse, -  HttpResponseOk, -  request, -  useMatchMutate, -} from "./backend.js"; +import { HttpError, HttpResponse, HttpResponseOk } from "../utils/request.js"; +import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";  export function useReservesAPI(): ReserveMutateAPI {    const mutateAll = useMatchMutate();    const { mutate } = useSWRConfig(); -  const { url: baseUrl, token: adminToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { url: baseUrl, token: adminToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { request } = useBackendInstanceRequest();    const createReserve = async (      data: MerchantBackend.Tips.ReserveCreateRequest, @@ -42,10 +29,9 @@ export function useReservesAPI(): ReserveMutateAPI {      HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation>    > => {      const res = await request<MerchantBackend.Tips.ReserveCreateConfirmation>( -      `${url}/private/reserves`, +      `/private/reserves`,        { -        method: "post", -        token, +        method: "POST",          data,        },      ); @@ -61,16 +47,15 @@ export function useReservesAPI(): ReserveMutateAPI {      data: MerchantBackend.Tips.TipCreateRequest,    ): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => {      const res = await request<MerchantBackend.Tips.TipCreateConfirmation>( -      `${url}/private/reserves/${pub}/authorize-tip`, +      `/private/reserves/${pub}/authorize-tip`,        { -        method: "post", -        token, +        method: "POST",          data,        },      );      //evict reserve details query -    await mutate([`/private/reserves/${pub}`, token, url]); +    await mutate([`/private/reserves/${pub}`]);      return res;    }; @@ -79,10 +64,9 @@ export function useReservesAPI(): ReserveMutateAPI {      data: MerchantBackend.Tips.TipCreateRequest,    ): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => {      const res = await request<MerchantBackend.Tips.TipCreateConfirmation>( -      `${url}/private/tips`, +      `/private/tips`,        { -        method: "post", -        token, +        method: "POST",          data,        },      ); @@ -94,9 +78,8 @@ export function useReservesAPI(): ReserveMutateAPI {    };    const deleteReserve = async (pub: string): Promise<HttpResponse<void>> => { -    const res = await request<void>(`${url}/private/reserves/${pub}`, { -      method: "delete", -      token, +    const res = await request<void>(`/private/reserves/${pub}`, { +      method: "DELETE",      });      //evict reserve list query @@ -123,17 +106,12 @@ export interface ReserveMutateAPI {  }  export function useInstanceReserves(): HttpResponse<MerchantBackend.Tips.TippingReserveStatus> { -  const { url: baseUrl, token: baseToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { url: baseUrl, token: baseToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { fetcher } = useBackendInstanceRequest();    const { data, error, isValidating } = useSWR<      HttpResponseOk<MerchantBackend.Tips.TippingReserveStatus>,      HttpError -  >([`/private/reserves`, token, url], fetcher); +  >([`/private/reserves`], fetcher);    if (isValidating) return { loading: true, data: data?.data };    if (data) return data; @@ -144,15 +122,12 @@ export function useInstanceReserves(): HttpResponse<MerchantBackend.Tips.Tipping  export function useReserveDetails(    reserveId: string,  ): HttpResponse<MerchantBackend.Tips.ReserveDetail> { -  const { url: baseUrl } = useBackendContext(); -  const { token, id: instanceId, admin } = useInstanceContext(); - -  const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}`; +  const { reserveDetailFetcher } = useBackendInstanceRequest();    const { data, error, isValidating } = useSWR<      HttpResponseOk<MerchantBackend.Tips.ReserveDetail>,      HttpError -  >([`/private/reserves/${reserveId}`, token, url], reserveDetailFetcher, { +  >([`/private/reserves/${reserveId}`], reserveDetailFetcher, {      refreshInterval: 0,      refreshWhenHidden: false,      revalidateOnFocus: false, @@ -169,15 +144,12 @@ export function useReserveDetails(  export function useTipDetails(    tipId: string,  ): HttpResponse<MerchantBackend.Tips.TipDetails> { -  const { url: baseUrl } = useBackendContext(); -  const { token, id: instanceId, admin } = useInstanceContext(); - -  const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}`; +  const { tipsDetailFetcher } = useBackendInstanceRequest();    const { data, error, isValidating } = useSWR<      HttpResponseOk<MerchantBackend.Tips.TipDetails>,      HttpError -  >([`/private/tips/${tipId}`, token, url], tipsDetailFetcher, { +  >([`/private/tips/${tipId}`], tipsDetailFetcher, {      refreshInterval: 0,      refreshWhenHidden: false,      revalidateOnFocus: false, @@ -190,29 +162,3 @@ export function useTipDetails(    if (error) return error;    return { loading: true };  } - -function reserveDetailFetcher<T>( -  url: string, -  token: string, -  backend: string, -): Promise<HttpResponseOk<T>> { -  return request<T>(`${backend}${url}`, { -    token, -    params: { -      tips: "yes", -    }, -  }); -} - -function tipsDetailFetcher<T>( -  url: string, -  token: string, -  backend: string, -): Promise<HttpResponseOk<T>> { -  return request<T>(`${backend}${url}`, { -    token, -    params: { -      pickups: "yes", -    }, -  }); -} diff --git a/packages/merchant-backoffice-ui/src/hooks/templates.ts b/packages/merchant-backoffice-ui/src/hooks/templates.ts index 3e69d78d0..55c3875b5 100644 --- a/packages/merchant-backoffice-ui/src/hooks/templates.ts +++ b/packages/merchant-backoffice-ui/src/hooks/templates.ts @@ -14,57 +14,26 @@   GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>   */  import { MerchantBackend } from "../declaration.js"; -import { useBackendContext } from "../context/backend.js"; +import { useMatchMutate, useBackendInstanceRequest } from "./backend.js"; +import useSWR from "swr"; +import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; +import { useEffect, useState } from "preact/hooks";  import { -  request, -  HttpResponse,    HttpError, +  HttpResponse,    HttpResponseOk,    HttpResponsePaginated, -  useMatchMutate, -} from "./backend.js"; -import useSWR from "swr"; -import { useInstanceContext } from "../context/instance.js"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useEffect, useState } from "preact/hooks"; - -async function templateFetcher<T>( -  url: string, -  token: string, -  backend: string, -  position?: string, -  delta?: number, -): Promise<HttpResponseOk<T>> { -  const params: any = {}; -  if (delta !== undefined) { -    params.limit = delta; -  } -  if (position !== undefined) params.offset = position; - -  return request<T>(`${backend}${url}`, { token, params }); -} +} from "../utils/request.js";  export function useTemplateAPI(): TemplateAPI {    const mutateAll = useMatchMutate(); -  const { url: baseUrl, token: adminToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { -        url: baseUrl, -        token: adminToken, -      } -    : { -        url: `${baseUrl}/instances/${id}`, -        token: instanceToken, -      }; +  const { request } = useBackendInstanceRequest();    const createTemplate = async (      data: MerchantBackend.Template.TemplateAddDetails,    ): Promise<HttpResponseOk<void>> => { -    const res = await request<void>(`${url}/private/templates`, { -      method: "post", -      token, +    const res = await request<void>(`/private/templates`, { +      method: "POST",        data,      });      await mutateAll(/.*private\/templates.*/); @@ -75,9 +44,8 @@ export function useTemplateAPI(): TemplateAPI {      templateId: string,      data: MerchantBackend.Template.TemplatePatchDetails,    ): Promise<HttpResponseOk<void>> => { -    const res = await request<void>(`${url}/private/templates/${templateId}`, { -      method: "patch", -      token, +    const res = await request<void>(`/private/templates/${templateId}`, { +      method: "PATCH",        data,      });      await mutateAll(/.*private\/templates.*/); @@ -87,9 +55,8 @@ export function useTemplateAPI(): TemplateAPI {    const deleteTemplate = async (      templateId: string,    ): Promise<HttpResponseOk<void>> => { -    const res = await request<void>(`${url}/private/templates/${templateId}`, { -      method: "delete", -      token, +    const res = await request<void>(`/private/templates/${templateId}`, { +      method: "DELETE",      });      await mutateAll(/.*private\/templates.*/);      return res; @@ -102,10 +69,9 @@ export function useTemplateAPI(): TemplateAPI {      HttpResponseOk<MerchantBackend.Template.UsingTemplateResponse>    > => {      const res = await request<MerchantBackend.Template.UsingTemplateResponse>( -      `${url}/private/templates/${templateId}`, +      `/private/templates/${templateId}`,        { -        method: "post", -        token, +        method: "POST",          data,        },      ); @@ -140,12 +106,7 @@ export function useInstanceTemplates(    args?: InstanceTemplateFilter,    updatePosition?: (id: string) => void,  ): HttpResponsePaginated<MerchantBackend.Template.TemplateSummaryResponse> { -  const { url: baseUrl, token: baseToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { url: baseUrl, token: baseToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { templateFetcher } = useBackendInstanceRequest();    // const [pageBefore, setPageBefore] = useState(1);    const [pageAfter, setPageAfter] = useState(1); @@ -180,10 +141,7 @@ export function useInstanceTemplates(    } = useSWR<      HttpResponseOk<MerchantBackend.Template.TemplateSummaryResponse>,      HttpError -  >( -    [`/private/templates`, token, url, args?.position, -totalAfter], -    templateFetcher, -  ); +  >([`/private/templates`, args?.position, -totalAfter], templateFetcher);    //this will save last result    // const [lastBefore, setLastBefore] = useState< @@ -216,10 +174,9 @@ export function useInstanceTemplates(        if (afterData.data.templates.length < MAX_RESULT_SIZE) {          setPageAfter(pageAfter + 1);        } else { -        const from = `${ -          afterData.data.templates[afterData.data.templates.length - 1] -            .template_id -        }`; +        const from = `${afterData.data.templates[afterData.data.templates.length - 1] +          .template_id +          }`;          if (from && updatePosition) updatePosition(from);        }      }, @@ -255,17 +212,12 @@ export function useInstanceTemplates(  export function useTemplateDetails(    templateId: string,  ): HttpResponse<MerchantBackend.Template.TemplateDetails> { -  const { url: baseUrl, token: baseToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { url: baseUrl, token: baseToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { templateFetcher } = useBackendInstanceRequest();    const { data, error, isValidating } = useSWR<      HttpResponseOk<MerchantBackend.Template.TemplateDetails>,      HttpError -  >([`/private/templates/${templateId}`, token, url], templateFetcher, { +  >([`/private/templates/${templateId}`], templateFetcher, {      refreshInterval: 0,      refreshWhenHidden: false,      revalidateOnFocus: false, diff --git a/packages/merchant-backoffice-ui/src/hooks/testing.tsx b/packages/merchant-backoffice-ui/src/hooks/testing.tsx new file mode 100644 index 000000000..8c5a5a36b --- /dev/null +++ b/packages/merchant-backoffice-ui/src/hooks/testing.tsx @@ -0,0 +1,120 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { MockEnvironment } from "@gnu-taler/web-util/lib/tests/mock"; +import { ComponentChildren, FunctionalComponent, h, VNode } from "preact"; +import { SWRConfig } from "swr"; +import { ApiContextProvider } from "../context/api.js"; +import { BackendContextProvider } from "../context/backend.js"; +import { InstanceContextProvider } from "../context/instance.js"; +import { HttpResponseOk, RequestOptions } from "../utils/request.js"; + +export class ApiMockEnvironment extends MockEnvironment { +  constructor(debug = false) { +    super(debug); +  } + +  mockApiIfNeeded(): void { +    null; // do nothing +  } + +  public buildTestingContext(): FunctionalComponent<{ +    children: ComponentChildren; +  }> { +    const __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE = +      this.saveRequestAndGetMockedResponse.bind(this); + +    return function TestingContext({ +      children, +    }: { +      children: ComponentChildren; +    }): VNode { +      async function request<T>( +        base: string, +        path: string, +        options: RequestOptions = {}, +      ): Promise<HttpResponseOk<T>> { +        const _url = new URL(`${base}${path}`); +        // Object.entries(options.params ?? {}).forEach(([key, value]) => { +        //   _url.searchParams.set(key, String(value)); +        // }); + +        const mocked = __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE( +          { +            method: options.method ?? "GET", +            url: _url.href, +          }, +          { +            qparam: options.params, +            auth: options.token, +            request: options.data, +          }, +        ); + +        return { +          ok: true, +          data: (!mocked ? undefined : mocked.payload) as T, +          loading: false, +          clientError: false, +          serverError: false, +          info: { +            hasToken: !!options.token, +            status: !mocked ? 200 : mocked.status, +            url: _url, +            payload: options.data, +          }, +        }; +      } +      const SC: any = SWRConfig; + +      return ( +        <BackendContextProvider +          defaultUrl="http://backend" +          initialToken={undefined} +        > +          <InstanceContextProvider +            value={{ +              token: undefined, +              id: "default", +              admin: true, +              changeToken: () => null, +            }} +          > +            <ApiContextProvider value={{ request }}> +              <SC +                value={{ +                  loadingTimeout: 0, +                  dedupingInterval: 0, +                  shouldRetryOnError: false, +                  errorRetryInterval: 0, +                  errorRetryCount: 0, +                  provider: () => new Map(), +                }} +              > +                {children} +              </SC> +            </ApiContextProvider> +          </InstanceContextProvider> +        </BackendContextProvider> +      ); +    }; +  } +} diff --git a/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts b/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts new file mode 100644 index 000000000..a553ed362 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts @@ -0,0 +1,277 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { tests } from "@gnu-taler/web-util/lib/index.browser"; +import { expect } from "chai"; +import { MerchantBackend } from "../declaration.js"; +import { API_INFORM_TRANSFERS, API_LIST_TRANSFERS } from "./urls.js"; +import { ApiMockEnvironment } from "./testing.js"; +import { useInstanceTransfers, useTransferAPI } from "./transfer.js"; + +describe("transfer api interaction with listing", () => { +  it("should evict cache when informing a transfer", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_LIST_TRANSFERS, { +      qparam: { limit: 0 }, +      response: { +        transfers: [{ wtid: "2" } as MerchantBackend.Transfers.TransferDetails], +      }, +    }); +    // FIXME: is this query really needed? if the hook is rendered without +    // position argument then then backend is returning the newest and no need +    // to this second query +    env.addRequestExpectation(API_LIST_TRANSFERS, { +      qparam: { limit: -20 }, +      response: { +        transfers: [], +      }, +    }); + +    const moveCursor = (d: string) => { +      console.log("new position", d); +    }; + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        const query = useInstanceTransfers({}, moveCursor); +        const api = useTransferAPI(); +        return { query, api }; +      }, +      {}, +      [ +        ({ query, api }) => { +          expect(query.loading).true; +        }, + +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ +            result: "ok", +          }); +          expect(query.loading).undefined; +          expect(query.ok).true; +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            transfers: [{ wtid: "2" }], +          }); + +          env.addRequestExpectation(API_INFORM_TRANSFERS, { +            request: { +              wtid: "3", +              credit_amount: "EUR:1", +              exchange_url: "exchange.url", +              payto_uri: "payto://", +            }, +            response: { total: "" } as any, +          }); + +          env.addRequestExpectation(API_LIST_TRANSFERS, { +            qparam: { limit: 0 }, +            response: { +              transfers: [{ wtid: "2" } as any, { wtid: "3" } as any], +            }, +          }); + +          env.addRequestExpectation(API_LIST_TRANSFERS, { +            qparam: { limit: -20 }, +            response: { +              transfers: [], +            }, +          }); + +          api.informTransfer({ +            wtid: "3", +            credit_amount: "EUR:1", +            exchange_url: "exchange.url", +            payto_uri: "payto://", +          }); +        }, +        ({ query, api }) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ +            result: "ok", +          }); +          expect(query.loading).undefined; +          expect(query.ok).true; +          if (!query.ok) return; + +          expect(query.data).deep.equals({ +            transfers: [{ wtid: "3" }, { wtid: "2" }], +          }); +        }, +      ], +      env.buildTestingContext(), +    ); + +    expect(hookBehavior).deep.eq({ result: "ok" }); +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +  }); +}); + +describe("transfer listing pagination", () => { +  it("should not load more if has reach the end", async () => { +    const env = new ApiMockEnvironment(); + +    env.addRequestExpectation(API_LIST_TRANSFERS, { +      qparam: { limit: 0, payto_uri: "payto://" }, +      response: { +        transfers: [{ wtid: "2" } as any], +      }, +    }); + +    env.addRequestExpectation(API_LIST_TRANSFERS, { +      qparam: { limit: -20, payto_uri: "payto://" }, +      response: { +        transfers: [{ wtid: "1" } as any], +      }, +    }); + +    const moveCursor = (d: string) => { +      console.log("new position", d); +    }; +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        return useInstanceTransfers({ payto_uri: "payto://" }, moveCursor); +      }, +      {}, +      [ +        (query) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ +            result: "ok", +          }); +          expect(query.loading).true; +        }, +        (query) => { +          expect(query.loading).undefined; +          expect(query.ok).true; +          if (!query.ok) return; +          expect(query.data).deep.equals({ +            transfers: [{ wtid: "2" }, { wtid: "1" }], +          }); +          expect(query.isReachingEnd).true; +          expect(query.isReachingStart).true; + +          //check that this button won't trigger more updates since +          //has reach end and start +          query.loadMore(); +          query.loadMorePrev(); +        }, +      ], +      env.buildTestingContext(), +    ); + +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +    expect(hookBehavior).deep.eq({ result: "ok" }); +  }); + +  it("should load more if result brings more that PAGE_SIZE", async () => { +    const env = new ApiMockEnvironment(); + +    const transfersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({ +      wtid: String(i), +    })); +    const transfersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({ +      wtid: String(i + 20), +    })); +    const transfersFrom20to0 = [...transfersFrom0to20].reverse(); + +    env.addRequestExpectation(API_LIST_TRANSFERS, { +      qparam: { limit: 20, payto_uri: "payto://", offset: "1" }, +      response: { +        transfers: transfersFrom0to20, +      }, +    }); + +    env.addRequestExpectation(API_LIST_TRANSFERS, { +      qparam: { limit: -20, payto_uri: "payto://", offset: "1" }, +      response: { +        transfers: transfersFrom20to40, +      }, +    }); + +    const moveCursor = (d: string) => { +      console.log("new position", d); +    }; + +    const hookBehavior = await tests.hookBehaveLikeThis( +      () => { +        return useInstanceTransfers( +          { payto_uri: "payto://", position: "1" }, +          moveCursor, +        ); +      }, +      {}, +      [ +        (result) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ +            result: "ok", +          }); +          expect(result.loading).true; +        }, +        (result) => { +          expect(result.loading).undefined; +          expect(result.ok).true; +          if (!result.ok) return; +          expect(result.data).deep.equals({ +            transfers: [...transfersFrom20to0, ...transfersFrom20to40], +          }); +          expect(result.isReachingEnd).false; +          expect(result.isReachingStart).false; + +          //query more +          env.addRequestExpectation(API_LIST_TRANSFERS, { +            qparam: { limit: -40, payto_uri: "payto://", offset: "1" }, +            response: { +              transfers: [...transfersFrom20to40, { wtid: "41" }], +            }, +          }); +          result.loadMore(); +        }, +        (result) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ +            result: "ok", +          }); +          expect(result.loading).true; +        }, +        (result) => { +          expect(env.assertJustExpectedRequestWereMade()).deep.eq({ +            result: "ok", +          }); +          expect(result.loading).undefined; +          expect(result.ok).true; +          if (!result.ok) return; +          expect(result.data).deep.equals({ +            transfers: [ +              ...transfersFrom20to0, +              ...transfersFrom20to40, +              { wtid: "41" }, +            ], +          }); +          expect(result.isReachingEnd).true; +          expect(result.isReachingStart).false; +        }, +      ], +      env.buildTestingContext(), +    ); + +    expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" }); +    expect(hookBehavior).deep.eq({ result: "ok" }); +  }); +}); diff --git a/packages/merchant-backoffice-ui/src/hooks/transfer.ts b/packages/merchant-backoffice-ui/src/hooks/transfer.ts index d1ac2c285..c827772e4 100644 --- a/packages/merchant-backoffice-ui/src/hooks/transfer.ts +++ b/packages/merchant-backoffice-ui/src/hooks/transfer.ts @@ -13,55 +13,21 @@   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 { useEffect, useState } from "preact/hooks"; +import useSWR from "swr";  import { MerchantBackend } from "../declaration.js"; -import { useBackendContext } from "../context/backend.js"; +import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";  import { -  request, -  HttpResponse,    HttpError, +  HttpResponse,    HttpResponseOk,    HttpResponsePaginated, -  useMatchMutate, -} from "./backend.js"; -import useSWR from "swr"; -import { useInstanceContext } from "../context/instance.js"; -import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js"; -import { useEffect, useState } from "preact/hooks"; - -async function transferFetcher<T>( -  url: string, -  token: string, -  backend: string, -  payto_uri?: string, -  verified?: string, -  position?: string, -  delta?: number, -): Promise<HttpResponseOk<T>> { -  const params: any = {}; -  if (payto_uri !== undefined) params.payto_uri = payto_uri; -  if (verified !== undefined) params.verified = verified; -  if (delta !== undefined) { -    params.limit = delta; -  } -  if (position !== undefined) params.offset = position; - -  return request<T>(`${backend}${url}`, { token, params }); -} +} from "../utils/request.js"; +import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";  export function useTransferAPI(): TransferAPI {    const mutateAll = useMatchMutate(); -  const { url: baseUrl, token: adminToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { -        url: baseUrl, -        token: adminToken, -      } -    : { -        url: `${baseUrl}/instances/${id}`, -        token: instanceToken, -      }; +  const { request } = useBackendInstanceRequest();    const informTransfer = async (      data: MerchantBackend.Transfers.TransferInformation, @@ -70,10 +36,9 @@ export function useTransferAPI(): TransferAPI {    > => {      const res =        await request<MerchantBackend.Transfers.MerchantTrackTransferResponse>( -        `${url}/private/transfers`, +        `/private/transfers`,          { -          method: "post", -          token, +          method: "POST",            data,          },        ); @@ -103,12 +68,7 @@ export function useInstanceTransfers(    args?: InstanceTransferFilter,    updatePosition?: (id: string) => void,  ): HttpResponsePaginated<MerchantBackend.Transfers.TransferList> { -  const { url: baseUrl, token: baseToken } = useBackendContext(); -  const { token: instanceToken, id, admin } = useInstanceContext(); - -  const { url, token } = !admin -    ? { url: baseUrl, token: baseToken } -    : { url: `${baseUrl}/instances/${id}`, token: instanceToken }; +  const { transferFetcher } = useBackendInstanceRequest();    const [pageBefore, setPageBefore] = useState(1);    const [pageAfter, setPageAfter] = useState(1); @@ -129,8 +89,6 @@ export function useInstanceTransfers(    } = useSWR<HttpResponseOk<MerchantBackend.Transfers.TransferList>, HttpError>(      [        `/private/transfers`, -      token, -      url,        args?.payto_uri,        args?.verified,        args?.position, @@ -145,8 +103,6 @@ export function useInstanceTransfers(    } = useSWR<HttpResponseOk<MerchantBackend.Transfers.TransferList>, HttpError>(      [        `/private/transfers`, -      token, -      url,        args?.payto_uri,        args?.verified,        args?.position, @@ -185,10 +141,9 @@ export function useInstanceTransfers(        if (afterData.data.transfers.length < MAX_RESULT_SIZE) {          setPageAfter(pageAfter + 1);        } else { -        const from = `${ -          afterData.data.transfers[afterData.data.transfers.length - 1] +        const from = `${afterData.data.transfers[afterData.data.transfers.length - 1]              .transfer_serial_id -        }`; +          }`;          if (from && updatePosition) updatePosition(from);        }      }, @@ -197,10 +152,9 @@ export function useInstanceTransfers(        if (beforeData.data.transfers.length < MAX_RESULT_SIZE) {          setPageBefore(pageBefore + 1);        } else if (beforeData) { -        const from = `${ -          beforeData.data.transfers[beforeData.data.transfers.length - 1] +        const from = `${beforeData.data.transfers[beforeData.data.transfers.length - 1]              .transfer_serial_id -        }`; +          }`;          if (from && updatePosition) updatePosition(from);        }      }, @@ -210,9 +164,9 @@ export function useInstanceTransfers(      !beforeData || !afterData        ? []        : (beforeData || lastBefore).data.transfers -          .slice() -          .reverse() -          .concat((afterData || lastAfter).data.transfers); +        .slice() +        .reverse() +        .concat((afterData || lastAfter).data.transfers);    if (loadingAfter || loadingBefore)      return { loading: true, data: { transfers } };    if (beforeData && afterData) { diff --git a/packages/merchant-backoffice-ui/src/hooks/urls.ts b/packages/merchant-backoffice-ui/src/hooks/urls.ts new file mode 100644 index 000000000..05494c0c9 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/hooks/urls.ts @@ -0,0 +1,291 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { Query } from "@gnu-taler/web-util/lib/tests/mock"; +import { MerchantBackend } from "../declaration.js"; + +//////////////////// +// ORDER +//////////////////// + +export const API_CREATE_ORDER: Query< +  MerchantBackend.Orders.PostOrderRequest, +  MerchantBackend.Orders.PostOrderResponse +> = { +  method: "POST", +  url: "http://backend/instances/default/private/orders", +}; + +export const API_GET_ORDER_BY_ID = ( +  id: string, +): Query<unknown, MerchantBackend.Orders.MerchantOrderStatusResponse> => ({ +  method: "GET", +  url: `http://backend/instances/default/private/orders/${id}`, +}); + +export const API_LIST_ORDERS: Query< +  unknown, +  MerchantBackend.Orders.OrderHistory +> = { +  method: "GET", +  url: "http://backend/instances/default/private/orders", +}; + +export const API_REFUND_ORDER_BY_ID = ( +  id: string, +): Query< +  MerchantBackend.Orders.RefundRequest, +  MerchantBackend.Orders.MerchantRefundResponse +> => ({ +  method: "POST", +  url: `http://backend/instances/default/private/orders/${id}/refund`, +}); + +export const API_FORGET_ORDER_BY_ID = ( +  id: string, +): Query<MerchantBackend.Orders.ForgetRequest, unknown> => ({ +  method: "PATCH", +  url: `http://backend/instances/default/private/orders/${id}/forget`, +}); + +export const API_DELETE_ORDER = ( +  id: string, +): Query<MerchantBackend.Orders.ForgetRequest, unknown> => ({ +  method: "DELETE", +  url: `http://backend/instances/default/private/orders/${id}`, +}); + +//////////////////// +// TRANSFER +//////////////////// + +export const API_LIST_TRANSFERS: Query< +  unknown, +  MerchantBackend.Transfers.TransferList +> = { +  method: "GET", +  url: "http://backend/instances/default/private/transfers", +}; + +export const API_INFORM_TRANSFERS: Query< +  MerchantBackend.Transfers.TransferInformation, +  MerchantBackend.Transfers.MerchantTrackTransferResponse +> = { +  method: "POST", +  url: "http://backend/instances/default/private/transfers", +}; + +//////////////////// +// PRODUCT +//////////////////// + +export const API_CREATE_PRODUCT: Query< +  MerchantBackend.Products.ProductAddDetail, +  unknown +> = { +  method: "POST", +  url: "http://backend/instances/default/private/products", +}; + +export const API_LIST_PRODUCTS: Query< +  unknown, +  MerchantBackend.Products.InventorySummaryResponse +> = { +  method: "GET", +  url: "http://backend/instances/default/private/products", +}; + +export const API_GET_PRODUCT_BY_ID = ( +  id: string, +): Query<unknown, MerchantBackend.Products.ProductDetail> => ({ +  method: "GET", +  url: `http://backend/instances/default/private/products/${id}`, +}); + +export const API_UPDATE_PRODUCT_BY_ID = ( +  id: string, +): Query< +  MerchantBackend.Products.ProductPatchDetail, +  MerchantBackend.Products.InventorySummaryResponse +> => ({ +  method: "PATCH", +  url: `http://backend/instances/default/private/products/${id}`, +}); + +export const API_DELETE_PRODUCT = (id: string): Query<unknown, unknown> => ({ +  method: "DELETE", +  url: `http://backend/instances/default/private/products/${id}`, +}); + +//////////////////// +// RESERVES +//////////////////// + +export const API_CREATE_RESERVE: Query< +  MerchantBackend.Tips.ReserveCreateRequest, +  MerchantBackend.Tips.ReserveCreateConfirmation +> = { +  method: "POST", +  url: "http://backend/instances/default/private/reserves", +}; +export const API_LIST_RESERVES: Query< +  unknown, +  MerchantBackend.Tips.TippingReserveStatus +> = { +  method: "GET", +  url: "http://backend/instances/default/private/reserves", +}; + +export const API_GET_RESERVE_BY_ID = ( +  pub: string, +): Query<unknown, MerchantBackend.Tips.ReserveDetail> => ({ +  method: "GET", +  url: `http://backend/instances/default/private/reserves/${pub}`, +}); + +export const API_GET_TIP_BY_ID = ( +  pub: string, +): Query<unknown, MerchantBackend.Tips.TipDetails> => ({ +  method: "GET", +  url: `http://backend/instances/default/private/tips/${pub}`, +}); + +export const API_AUTHORIZE_TIP_FOR_RESERVE = ( +  pub: string, +): Query< +  MerchantBackend.Tips.TipCreateRequest, +  MerchantBackend.Tips.TipCreateConfirmation +> => ({ +  method: "POST", +  url: `http://backend/instances/default/private/reserves/${pub}/authorize-tip`, +}); + +export const API_AUTHORIZE_TIP: Query< +  MerchantBackend.Tips.TipCreateRequest, +  MerchantBackend.Tips.TipCreateConfirmation +> = { +  method: "POST", +  url: `http://backend/instances/default/private/tips`, +}; + +export const API_DELETE_RESERVE = (id: string): Query<unknown, unknown> => ({ +  method: "DELETE", +  url: `http://backend/instances/default/private/reserves/${id}`, +}); + +//////////////////// +// INSTANCE ADMIN +//////////////////// + +export const API_CREATE_INSTANCE: Query< +  MerchantBackend.Instances.InstanceConfigurationMessage, +  unknown +> = { +  method: "POST", +  url: "http://backend/management/instances", +}; + +export const API_GET_INSTANCE_BY_ID = ( +  id: string, +): Query<unknown, MerchantBackend.Instances.QueryInstancesResponse> => ({ +  method: "GET", +  url: `http://backend/management/instances/${id}`, +}); + +export const API_GET_INSTANCE_KYC_BY_ID = ( +  id: string, +): Query<unknown, MerchantBackend.Instances.AccountKycRedirects> => ({ +  method: "GET", +  url: `http://backend/management/instances/${id}/kyc`, +}); + +export const API_LIST_INSTANCES: Query< +  unknown, +  MerchantBackend.Instances.InstancesResponse +> = { +  method: "GET", +  url: "http://backend/management/instances", +}; + +export const API_UPDATE_INSTANCE_BY_ID = ( +  id: string, +): Query< +  MerchantBackend.Instances.InstanceReconfigurationMessage, +  unknown +> => ({ +  method: "PATCH", +  url: `http://backend/management/instances/${id}`, +}); + +export const API_UPDATE_INSTANCE_AUTH_BY_ID = ( +  id: string, +): Query< +  MerchantBackend.Instances.InstanceAuthConfigurationMessage, +  unknown +> => ({ +  method: "POST", +  url: `http://backend/management/instances/${id}/auth`, +}); + +export const API_DELETE_INSTANCE = (id: string): Query<unknown, unknown> => ({ +  method: "DELETE", +  url: `http://backend/management/instances/${id}`, +}); + +//////////////////// +// INSTANCE +//////////////////// + +export const API_GET_CURRENT_INSTANCE: Query< +  unknown, +  MerchantBackend.Instances.QueryInstancesResponse +> = { +  method: "GET", +  url: `http://backend/instances/default/private/`, +}; + +export const API_GET_CURRENT_INSTANCE_KYC: Query< +  unknown, +  MerchantBackend.Instances.AccountKycRedirects +> = { +  method: "GET", +  url: `http://backend/instances/default/private/kyc`, +}; + +export const API_UPDATE_CURRENT_INSTANCE: Query< +  MerchantBackend.Instances.InstanceReconfigurationMessage, +  unknown +> = { +  method: "PATCH", +  url: `http://backend/instances/default/private/`, +}; + +export const API_UPDATE_CURRENT_INSTANCE_AUTH: Query< +  MerchantBackend.Instances.InstanceAuthConfigurationMessage, +  unknown +> = { +  method: "POST", +  url: `http://backend/instances/default/private/auth`, +}; + +export const API_DELETE_CURRENT_INSTANCE: Query<unknown, unknown> = { +  method: "DELETE", +  url: `http://backend/instances/default/private`, +}; diff --git a/packages/merchant-backoffice-ui/src/manifest.json b/packages/merchant-backoffice-ui/src/manifest.json deleted file mode 100644 index 2c3de2339..000000000 --- a/packages/merchant-backoffice-ui/src/manifest.json +++ /dev/null @@ -1,21 +0,0 @@ -{ -  "name": "backoffice-preact", -  "short_name": "backoffice-preact", -  "start_url": "/", -  "display": "standalone", -  "orientation": "portrait", -  "background_color": "#fff", -  "theme_color": "#673ab8", -  "icons": [ -    { -      "src": "/assets/icons/android-chrome-192x192.png", -      "type": "image/png", -      "sizes": "192x192" -    }, -    { -      "src": "/assets/icons/android-chrome-512x512.png", -      "type": "image/png", -      "sizes": "512x512" -    } -  ] -} diff --git a/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx index 9a81b72d4..bac7a39eb 100644 --- a/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/admin/list/index.tsx @@ -26,7 +26,7 @@ import { Loading } from "../../../components/exception/loading.js";  import { NotificationCard } from "../../../components/menu/index.js";  import { DeleteModal, PurgeModal } from "../../../components/modal/index.js";  import { MerchantBackend } from "../../../declaration.js"; -import { HttpError } from "../../../hooks/backend.js"; +import { HttpError } from "../../../utils/request.js";  import { useAdminAPI, useBackendInstances } from "../../../hooks/instance.js";  import { Notification } from "../../../utils/types.js";  import { View } from "./View.js"; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx index 49b64262b..56d5c0755 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/details/index.tsx @@ -18,7 +18,7 @@ import { useState } from "preact/hooks";  import { Loading } from "../../../components/exception/loading.js";  import { DeleteModal } from "../../../components/modal/index.js";  import { useInstanceContext } from "../../../context/instance.js"; -import { HttpError } from "../../../hooks/backend.js"; +import { HttpError } from "../../../utils/request.js";  import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js";  import { DetailPage } from "./DetailPage.js"; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx index 295d6a749..83af002b3 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/index.tsx @@ -21,7 +21,7 @@  import { h, VNode } from "preact";  import { Loading } from "../../../../components/exception/loading.js"; -import { HttpError } from "../../../../hooks/backend.js"; +import { HttpError } from "../../../../utils/request.js";  import { useInstanceKYCDetails } from "../../../../hooks/instance.js";  import { ListPage } from "./ListPage.js"; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx index 95232da92..5c6293a81 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/index.tsx @@ -24,7 +24,7 @@ import { useState } from "preact/hooks";  import { Loading } from "../../../../components/exception/loading.js";  import { NotificationCard } from "../../../../components/menu/index.js";  import { MerchantBackend } from "../../../../declaration.js"; -import { HttpError } from "../../../../hooks/backend.js"; +import { HttpError } from "../../../../utils/request.js";  import { useInstanceDetails } from "../../../../hooks/instance.js";  import { useOrderAPI } from "../../../../hooks/order.js";  import { useInstanceProducts } from "../../../../hooks/product.js"; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx index bb0240982..19aaddf50 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx @@ -18,7 +18,7 @@ import { Fragment, h, VNode } from "preact";  import { useState } from "preact/hooks";  import { Loading } from "../../../../components/exception/loading.js";  import { NotificationCard } from "../../../../components/menu/index.js"; -import { HttpError } from "../../../../hooks/backend.js"; +import { HttpError } from "../../../../utils/request.js";  import { useOrderAPI, useOrderDetails } from "../../../../hooks/order.js";  import { Notification } from "../../../../utils/types.js";  import { DetailPage } from "./DetailPage.js"; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx index e29c57a7c..3744ce8c5 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/list/index.tsx @@ -25,7 +25,7 @@ import { useState } from "preact/hooks";  import { Loading } from "../../../../components/exception/loading.js";  import { NotificationCard } from "../../../../components/menu/index.js";  import { MerchantBackend } from "../../../../declaration.js"; -import { HttpError } from "../../../../hooks/backend.js"; +import { HttpError } from "../../../../utils/request.js";  import {    InstanceOrderFilter,    useInstanceOrders, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx index 41a07a7aa..25332acee 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/list/index.tsx @@ -25,7 +25,7 @@ import { useState } from "preact/hooks";  import { Loading } from "../../../../components/exception/loading.js";  import { NotificationCard } from "../../../../components/menu/index.js";  import { MerchantBackend, WithId } from "../../../../declaration.js"; -import { HttpError } from "../../../../hooks/backend.js"; +import { HttpError } from "../../../../utils/request.js";  import {    useInstanceProducts,    useProductAPI, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx index e141dc52c..5b19a7aa3 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/products/update/index.tsx @@ -25,7 +25,7 @@ import { useState } from "preact/hooks";  import { Loading } from "../../../../components/exception/loading.js";  import { NotificationCard } from "../../../../components/menu/index.js";  import { MerchantBackend } from "../../../../declaration.js"; -import { HttpError } from "../../../../hooks/backend.js"; +import { HttpError } from "../../../../utils/request.js";  import { useProductAPI, useProductDetails } from "../../../../hooks/product.js";  import { Notification } from "../../../../utils/types.js";  import { UpdatePage } from "./UpdatePage.js"; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx index de2319636..ad0cca74a 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/create/CreatePage.tsx @@ -31,7 +31,7 @@ import { Input } from "../../../../components/form/Input.js";  import { InputCurrency } from "../../../../components/form/InputCurrency.js";  import { InputSelector } from "../../../../components/form/InputSelector.js";  import { ExchangeBackend, MerchantBackend } from "../../../../declaration.js"; -import { request } from "../../../../hooks/backend.js"; +// import { request } from "../../../../utils/request.js";  import {    PAYTO_WIRE_METHOD_LOOKUP,    URL_REGEX, @@ -124,11 +124,10 @@ function ViewStep({              <AsyncButton                class="has-tooltip-left"                onClick={() => { -                return request<ExchangeBackend.WireResponse>( -                  `${reserve.exchange_url}wire`, -                ) +                return fetch(`${reserve.exchange_url}wire`) +                  .then((r) => r.json())                    .then((r) => { -                    const wireMethods = r.data.accounts.map((a) => { +                    const wireMethods = r.data.accounts.map((a: any) => {                        const match = PAYTO_WIRE_METHOD_LOOKUP.exec(a.payto_uri);                        return (match && match[1]) || "";                      }); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/index.tsx index b13b075fd..57ee566d1 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/details/index.tsx @@ -21,7 +21,7 @@  import { Fragment, h, VNode } from "preact";  import { Loading } from "../../../../components/exception/loading.js"; -import { HttpError } from "../../../../hooks/backend.js"; +import { HttpError } from "../../../../utils/request.js";  import { useReserveDetails } from "../../../../hooks/reserves.js";  import { DetailPage } from "./DetailPage.js"; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/index.tsx index 9c3255ee8..597bde167 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/reserves/list/index.tsx @@ -25,7 +25,7 @@ import { useState } from "preact/hooks";  import { Loading } from "../../../../components/exception/loading.js";  import { NotificationCard } from "../../../../components/menu/index.js";  import { MerchantBackend } from "../../../../declaration.js"; -import { HttpError } from "../../../../hooks/backend.js"; +import { HttpError } from "../../../../utils/request.js";  import {    useInstanceReserves,    useReservesAPI, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx index dcac23983..e1a2d019e 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/list/index.tsx @@ -25,7 +25,7 @@ import { useState } from "preact/hooks";  import { Loading } from "../../../../components/exception/loading.js";  import { NotificationCard } from "../../../../components/menu/index.js";  import { MerchantBackend } from "../../../../declaration.js"; -import { HttpError } from "../../../../hooks/backend.js"; +import { HttpError } from "../../../../utils/request.js";  import {    useInstanceTemplates,    useTemplateAPI, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx index 4a4cc4274..684ffd429 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/index.tsx @@ -25,7 +25,7 @@ import { useState } from "preact/hooks";  import { Loading } from "../../../../components/exception/loading.js";  import { NotificationCard } from "../../../../components/menu/index.js";  import { MerchantBackend, WithId } from "../../../../declaration.js"; -import { HttpError } from "../../../../hooks/backend.js"; +import { HttpError } from "../../../../utils/request.js";  import {    useTemplateAPI,    useTemplateDetails, diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx index 242380fbc..59b56a613 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx @@ -23,7 +23,7 @@ import { h, VNode } from "preact";  import { useState } from "preact/hooks";  import { Loading } from "../../../../components/exception/loading.js";  import { MerchantBackend } from "../../../../declaration.js"; -import { HttpError } from "../../../../hooks/backend.js"; +import { HttpError } from "../../../../utils/request.js";  import { useInstanceDetails } from "../../../../hooks/instance.js";  import { useInstanceTransfers } from "../../../../hooks/transfer.js";  import { ListPage } from "./ListPage.js"; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx index 668fe9a8d..02beb36f2 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/update/index.tsx @@ -20,13 +20,13 @@ import { Loading } from "../../../components/exception/loading.js";  import { NotificationCard } from "../../../components/menu/index.js";  import { useInstanceContext } from "../../../context/instance.js";  import { MerchantBackend } from "../../../declaration.js"; -import { HttpError, HttpResponse } from "../../../hooks/backend.js";  import {    useInstanceAPI,    useInstanceDetails,    useManagedInstanceDetails,    useManagementAPI,  } from "../../../hooks/instance.js"; +import { HttpError, HttpResponse } from "../../../utils/request.js";  import { Notification } from "../../../utils/types.js";  import { UpdatePage } from "./UpdatePage.js"; diff --git a/packages/merchant-backoffice-ui/tests/functions/regex.test.ts b/packages/merchant-backoffice-ui/src/utils/regex.test.ts index d866a13a0..41f0156f5 100644 --- a/packages/merchant-backoffice-ui/tests/functions/regex.test.ts +++ b/packages/merchant-backoffice-ui/src/utils/regex.test.ts @@ -19,6 +19,7 @@  * @author Sebastian Javier Marchano (sebasjm)  */ +import { expect } from "chai";  import { AMOUNT_REGEX, PAYTO_REGEX } from "../../src/utils/constants.js";  describe('payto uri format', () => { @@ -31,7 +32,7 @@ describe('payto uri format', () => {    ]    it('should be valid', () => { -    valids.forEach(v => expect(v).toMatch(PAYTO_REGEX)) +    valids.forEach(v => expect(v).match(PAYTO_REGEX))    });    const invalids = [ @@ -48,7 +49,7 @@ describe('payto uri format', () => {    ]    it('should not be valid', () => { -    invalids.forEach(v => expect(v).not.toMatch(PAYTO_REGEX)) +    invalids.forEach(v => expect(v).not.match(PAYTO_REGEX))    });  }) @@ -64,7 +65,7 @@ describe('amount format', () => {    ]    it('should be valid', () => { -    valids.forEach(v => expect(v).toMatch(AMOUNT_REGEX)) +    valids.forEach(v => expect(v).match(AMOUNT_REGEX))    });    const invalids = [ @@ -81,7 +82,7 @@ describe('amount format', () => {    ]    it('should not be valid', () => { -    invalids.forEach(v => expect(v).not.toMatch(AMOUNT_REGEX)) +    invalids.forEach(v => expect(v).not.match(AMOUNT_REGEX))    });  })
\ No newline at end of file diff --git a/packages/merchant-backoffice-ui/src/utils/request.ts b/packages/merchant-backoffice-ui/src/utils/request.ts new file mode 100644 index 000000000..32b31a557 --- /dev/null +++ b/packages/merchant-backoffice-ui/src/utils/request.ts @@ -0,0 +1,282 @@ +/* + This file is part of GNU Taler + (C) 2021-2023 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 axios, { AxiosError, AxiosResponse } from "axios"; +import { MerchantBackend } from "../declaration.js"; + +export async function defaultRequestHandler<T>( +  base: string, +  path: string, +  options: RequestOptions = {}, +): Promise<HttpResponseOk<T>> { +  const requestHeaders = options.token +    ? { Authorization: `Bearer ${options.token}` } +    : undefined; + +  const requestMethod = options?.method ?? "GET"; +  const requestBody = options?.data; +  const requestTimeout = 2 * 1000; +  const requestParams = options.params ?? {}; + +  const _url = new URL(`${base}${path}`); + +  Object.entries(requestParams).forEach(([key, value]) => { +    _url.searchParams.set(key, String(value)); +  }); + +  let payload: BodyInit | undefined = undefined; +  if (requestBody != null) { +    if (typeof requestBody === "string") { +      payload = requestBody; +    } else if (requestBody instanceof ArrayBuffer) { +      payload = requestBody; +    } else if (ArrayBuffer.isView(requestBody)) { +      payload = requestBody; +    } else if (typeof requestBody === "object") { +      payload = JSON.stringify(requestBody); +    } else { +      throw Error("unsupported request body type"); +    } +  } + +  const controller = new AbortController(); +  const timeoutId = setTimeout(() => { +    controller.abort("HTTP_REQUEST_TIMEOUT"); +  }, requestTimeout); + +  const response = await fetch(_url.href, { +    headers: { +      ...requestHeaders, +      "Content-Type": "text/plain", +    }, +    method: requestMethod, +    credentials: "omit", +    mode: "cors", +    body: payload, +    signal: controller.signal, +  }); + +  if (timeoutId) { +    clearTimeout(timeoutId); +  } +  const headerMap = new Headers(); +  response.headers.forEach((value, key) => { +    headerMap.set(key, value); +  }); + +  if (response.ok) { +    const result = await buildRequestOk<T>( +      response, +      _url, +      payload, +      !!options.token, +    ); +    return result; +  } else { +    const error = await buildRequestFailed( +      response, +      _url, +      payload, +      !!options.token, +    ); +    throw error; +  } +} + +export type HttpResponse<T> = +  | HttpResponseOk<T> +  | HttpResponseLoading<T> +  | HttpError; +export type HttpResponsePaginated<T> = +  | HttpResponseOkPaginated<T> +  | HttpResponseLoading<T> +  | HttpError; + +export interface RequestInfo { +  url: URL; +  hasToken: boolean; +  payload: any; +  status: number; +} + +interface HttpResponseLoading<T> { +  ok?: false; +  loading: true; +  clientError?: false; +  serverError?: false; + +  data?: T; +} +export interface HttpResponseOk<T> { +  ok: true; +  loading?: false; +  clientError?: false; +  serverError?: false; + +  data: T; +  info?: RequestInfo; +} + +export type HttpResponseOkPaginated<T> = HttpResponseOk<T> & WithPagination; + +export interface WithPagination { +  loadMore: () => void; +  loadMorePrev: () => void; +  isReachingEnd?: boolean; +  isReachingStart?: boolean; +} + +export type HttpError = +  | HttpResponseClientError +  | HttpResponseServerError +  | HttpResponseUnexpectedError; +export interface SwrError { +  info: unknown; +  status: number; +  message: string; +} +export interface HttpResponseServerError { +  ok?: false; +  loading?: false; +  clientError?: false; +  serverError: true; + +  error?: MerchantBackend.ErrorDetail; +  status: number; +  message: string; +  info?: RequestInfo; +} +interface HttpResponseClientError { +  ok?: false; +  loading?: false; +  clientError: true; +  serverError?: false; + +  info?: RequestInfo; +  isUnauthorized: boolean; +  isNotfound: boolean; +  status: number; +  error?: MerchantBackend.ErrorDetail; +  message: string; +} + +interface HttpResponseUnexpectedError { +  ok?: false; +  loading?: false; +  clientError?: false; +  serverError?: false; + +  info?: RequestInfo; +  status?: number; +  error: unknown; +  message: string; +} + +type Methods = "GET" | "POST" | "PATCH" | "DELETE" | "PUT"; + +export interface RequestOptions { +  method?: Methods; +  token?: string; +  data?: any; +  params?: unknown; +} + +async function buildRequestOk<T>( +  response: Response, +  url: URL, +  payload: any, +  hasToken: boolean, +): Promise<HttpResponseOk<T>> { +  const dataTxt = await response.text(); +  const data = dataTxt ? JSON.parse(dataTxt) : undefined +  return { +    ok: true, +    data, +    info: { +      payload, +      url, +      hasToken, +      status: response.status, +    }, +  }; +} + +async function buildRequestFailed( +  response: Response, +  url: URL, +  payload: any, +  hasToken: boolean, +): Promise< +  | HttpResponseClientError +  | HttpResponseServerError +  | HttpResponseUnexpectedError +> { +  const status = response?.status; + +  const info: RequestInfo = { +    payload, +    url, +    hasToken, +    status: status || 0, +  }; + +  try { +    const dataTxt = await response.text(); +    const data = dataTxt ? JSON.parse(dataTxt) : undefined +    if (status && status >= 400 && status < 500) { +      const error: HttpResponseClientError = { +        clientError: true, +        isNotfound: status === 404, +        isUnauthorized: status === 401, +        status, +        info, +        message: data?.hint, +        error: data, +      }; +      return error; +    } +    if (status && status >= 500 && status < 600) { +      const error: HttpResponseServerError = { +        serverError: true, +        status, +        info, +        message: `${data?.hint} (code ${data?.code})`, +        error: data, +      }; +      return error; +    } +    return { +      info, +      status, +      error: {}, +      message: "NOT DEFINED", +    }; +  } catch (ex) { +    const error: HttpResponseUnexpectedError = { +      info, +      status, +      error: ex, +      message: "NOT DEFINED", +    }; + +    throw error; +  } +} + +// export function isAxiosError<T>( +//   error: AxiosError | any, +// ): error is AxiosError<T> { +//   return error && error.isAxiosError; +// } diff --git a/packages/merchant-backoffice-ui/src/utils/switchableAxios.ts b/packages/merchant-backoffice-ui/src/utils/switchableAxios.ts deleted file mode 100644 index 20ce7043e..000000000 --- a/packages/merchant-backoffice-ui/src/utils/switchableAxios.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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 axios, { AxiosPromise, AxiosRequestConfig } from "axios"; - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -export let removeAxiosCancelToken = false; - -export let axiosHandler = function doAxiosRequest( -  config: AxiosRequestConfig, -): AxiosPromise<any> { -  return axios(config); -}; - -/** - * Set this backend library to testing mode. - * Instead of calling the axios library the @handler will be called - * - * @param handler callback that will mock axios - */ -export function setAxiosRequestAsTestingEnvironment( -  handler: AxiosHandler, -): void { -  removeAxiosCancelToken = true; -  axiosHandler = function defaultTestingHandler(config) { -    const currentHanlder = listOfHandlersToUseOnce.shift(); -    if (!currentHanlder) { -      return handler(config); -    } - -    return currentHanlder(config); -  }; -} - -type AxiosHandler = (config: AxiosRequestConfig) => AxiosPromise<any>; -type AxiosArguments = { args: AxiosRequestConfig | undefined }; - -const listOfHandlersToUseOnce = new Array<AxiosHandler>(); - -/** - * - * @param handler mock function - * @returns savedArgs - */ -export function mockAxiosOnce(handler: AxiosHandler): { -  args: AxiosRequestConfig | undefined; -} { -  const savedArgs: AxiosArguments = { args: undefined }; -  listOfHandlersToUseOnce.push( -    (config: AxiosRequestConfig): AxiosPromise<any> => { -      savedArgs.args = config; -      return handler(config); -    }, -  ); -  return savedArgs; -} diff --git a/packages/merchant-backoffice-ui/tests/__mocks__/fileMocks.ts b/packages/merchant-backoffice-ui/tests/__mocks__/fileMocks.ts deleted file mode 100644 index 982832ea8..000000000 --- a/packages/merchant-backoffice-ui/tests/__mocks__/fileMocks.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - - /** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -// This fixed an error related to the CSS and loading gif breaking my Jest test -// See https://facebook.github.io/jest/docs/en/webpack.html#handling-static-assets -export default 'test-file-stub'; diff --git a/packages/merchant-backoffice-ui/tests/__mocks__/fileTransformer.js b/packages/merchant-backoffice-ui/tests/__mocks__/fileTransformer.js deleted file mode 100644 index b76da9168..000000000 --- a/packages/merchant-backoffice-ui/tests/__mocks__/fileTransformer.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ -// fileTransformer.js - -// eslint-disable-next-line @typescript-eslint/no-var-requires -const path = require('path'); - -module.exports = { -    process(src, filename, config, options) { -        return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';'; -    }, -}; - diff --git a/packages/merchant-backoffice-ui/tests/__mocks__/setupTests.ts b/packages/merchant-backoffice-ui/tests/__mocks__/setupTests.ts deleted file mode 100644 index fe2d72d5c..000000000 --- a/packages/merchant-backoffice-ui/tests/__mocks__/setupTests.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import "regenerator-runtime/runtime"; -// import { configure } from 'enzyme'; -// import Adapter from 'enzyme-adapter-preact-pure'; - -// configure({ -//     adapter: new Adapter() -// }); diff --git a/packages/merchant-backoffice-ui/tests/axiosMock.ts b/packages/merchant-backoffice-ui/tests/axiosMock.ts deleted file mode 100644 index ca8d5096d..000000000 --- a/packages/merchant-backoffice-ui/tests/axiosMock.ts +++ /dev/null @@ -1,445 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ -import * as axios from 'axios'; -import { MerchantBackend } from "../src/declaration.js"; -import { mockAxiosOnce, setAxiosRequestAsTestingEnvironment } from "../src/utils/switchableAxios.js"; -// import { mockAxiosOnce, setAxiosRequestAsTestingEnvironment } from "../src/hooks/backend.js"; - -export type Query<Req, Res> = (GetQuery | PostQuery | DeleteQuery | PatchQuery) & RequestResponse<Req, Res> - -interface RequestResponse<Req, Res> { -  code?: number, -} -interface GetQuery { get: string } -interface PostQuery { post: string } -interface DeleteQuery { delete: string } -interface PatchQuery { patch: string } - - -const JEST_DEBUG_LOG = process.env['JEST_DEBUG_LOG'] !== undefined - -type ExpectationValues = { query: Query<any, any>; params?: { auth?: string, request?: any, qparam?: any, response?: any } } - -type TestValues = [axios.AxiosRequestConfig | undefined, ExpectationValues | undefined] - -const defaultCallback = (actualQuery?: axios.AxiosRequestConfig): axios.AxiosPromise<any> => { -  if (JEST_DEBUG_LOG) { -    console.log('UNEXPECTED QUERY', actualQuery) -  } -  throw Error('Default Axios mock callback is called, this mean that the test did a tried to use axios but there was no expectation in place, try using JEST_DEBUG_LOG env') -} - -setAxiosRequestAsTestingEnvironment( -  defaultCallback -); - -export class AxiosMockEnvironment { -  expectations: Array<{ -    query: Query<any, any>, -    auth?: string, -    params?: { request?: any, qparam?: any, response?: any }, -    result: { args: axios.AxiosRequestConfig | undefined } -  } | undefined> = [] -  // axiosMock: jest.MockedFunction<axios.AxiosStatic> - -  addRequestExpectation<RequestType, ResponseType>(expectedQuery: Query<RequestType, ResponseType>, params: { auth?: string, request?: RequestType, qparam?: any, response?: ResponseType }): void { -    const result = mockAxiosOnce(function (actualQuery?: axios.AxiosRequestConfig): axios.AxiosPromise { - -      if (JEST_DEBUG_LOG) { -        console.log('query to the backend is made', actualQuery) -      } -      if (!expectedQuery) { -        return Promise.reject("a query was made but it was not expected") -      } -      if (JEST_DEBUG_LOG) { -        console.log('expected query:', params?.request) -        console.log('expected qparams:', params?.qparam) -        console.log('sending response:', params?.response) -      } - -      const responseCode = expectedQuery.code || 200 - -      //This response is what buildRequestOk is expecting in file hook/backend.ts -      if (responseCode >= 200 && responseCode < 300) { -        return Promise.resolve({ -          data: params?.response, config: { -            data: params?.response, -            params: actualQuery?.params || {}, -          }, request: { params: actualQuery?.params || {} } -        } as any); -      } -      //This response is what buildRequestFailed is expecting in file hook/backend.ts -      return Promise.reject({ -        response: { -          status: responseCode -        }, -        request: { -          data: params?.response, -          params: actualQuery?.params || {}, -        } -      }) - -    } as any) - -    this.expectations.push(expectedQuery ? { query: expectedQuery, params, result } : undefined) -  } - -  getLastTestValues(): TestValues { -    const expectedQuery = this.expectations.shift() - -    return [ -      expectedQuery?.result.args, expectedQuery -    ] -  } - -} - -export function assertJustExpectedRequestWereMade(env: AxiosMockEnvironment): void { -  let size = env.expectations.length -  while (size-- > 0) { -    assertNextRequest(env) -  } -  assertNoMoreRequestWereMade(env) -} - -export function assertNoMoreRequestWereMade(env: AxiosMockEnvironment): void { -  const [actualQuery, expectedQuery] = env.getLastTestValues() - -  expect(actualQuery).toBeUndefined(); -  expect(expectedQuery).toBeUndefined(); -} - -export function assertNextRequest(env: AxiosMockEnvironment): void { -  const [actualQuery, expectedQuery] = env.getLastTestValues() - -  if (!actualQuery) { -    //expected one query but the tested component didn't execute one -    expect(actualQuery).toBe(expectedQuery); -    return -  } - -  if (!expectedQuery) { -    const errorMessage = 'a query was made to the backend but the test explicitly expected no query'; -    if (JEST_DEBUG_LOG) { -      console.log(errorMessage, actualQuery) -    } -    throw Error(errorMessage) -  } -  if ('get' in expectedQuery.query) { -    expect(actualQuery.method).toBe('get'); -    expect(actualQuery.url).toBe(expectedQuery.query.get); -  } -  if ('post' in expectedQuery.query) { -    expect(actualQuery.method).toBe('post'); -    expect(actualQuery.url).toBe(expectedQuery.query.post); -  } -  if ('delete' in expectedQuery.query) { -    expect(actualQuery.method).toBe('delete'); -    expect(actualQuery.url).toBe(expectedQuery.query.delete); -  } -  if ('patch' in expectedQuery.query) { -    expect(actualQuery.method).toBe('patch'); -    expect(actualQuery.url).toBe(expectedQuery.query.patch); -  } - -  if (expectedQuery.params?.request) { -    expect(actualQuery.data).toMatchObject(expectedQuery.params.request) -  } -  if (expectedQuery.params?.qparam) { -    expect(actualQuery.params).toMatchObject(expectedQuery.params.qparam) -  } - -  if (expectedQuery.params?.auth) { -    expect(actualQuery.headers.Authorization).toBe(expectedQuery.params?.auth) -  } - -} - -//////////////////// -// ORDER -//////////////////// - -export const API_CREATE_ORDER: Query< -  MerchantBackend.Orders.PostOrderRequest, -  MerchantBackend.Orders.PostOrderResponse -> = { -  post: "http://backend/instances/default/private/orders", -}; - -export const API_GET_ORDER_BY_ID = ( -  id: string -): Query< -  unknown, -  MerchantBackend.Orders.MerchantOrderStatusResponse -> => ({ -  get: `http://backend/instances/default/private/orders/${id}`, -}); - -export const API_LIST_ORDERS: Query< -  unknown, -  MerchantBackend.Orders.OrderHistory -> = { -  get: "http://backend/instances/default/private/orders", -}; - -export const API_REFUND_ORDER_BY_ID = ( -  id: string -): Query< -  MerchantBackend.Orders.RefundRequest, -  MerchantBackend.Orders.MerchantRefundResponse -> => ({ -  post: `http://backend/instances/default/private/orders/${id}/refund`, -}); - -export const API_FORGET_ORDER_BY_ID = ( -  id: string -): Query< -  MerchantBackend.Orders.ForgetRequest, -  unknown -> => ({ -  patch: `http://backend/instances/default/private/orders/${id}/forget`, -}); - -export const API_DELETE_ORDER = ( -  id: string -): Query< -  MerchantBackend.Orders.ForgetRequest, -  unknown -> => ({ -  delete: `http://backend/instances/default/private/orders/${id}`, -}); - -//////////////////// -// TRANSFER -//////////////////// - -export const API_LIST_TRANSFERS: Query< -  unknown, -  MerchantBackend.Transfers.TransferList -> = { -  get: "http://backend/instances/default/private/transfers", -}; - -export const API_INFORM_TRANSFERS: Query< -  MerchantBackend.Transfers.TransferInformation, -  MerchantBackend.Transfers.MerchantTrackTransferResponse -> = { -  post: "http://backend/instances/default/private/transfers", -}; - -//////////////////// -// PRODUCT -//////////////////// - -export const API_CREATE_PRODUCT: Query< -  MerchantBackend.Products.ProductAddDetail, -  unknown -> = { -  post: "http://backend/instances/default/private/products", -}; - -export const API_LIST_PRODUCTS: Query< -  unknown, -  MerchantBackend.Products.InventorySummaryResponse -> = { -  get: "http://backend/instances/default/private/products", -}; - -export const API_GET_PRODUCT_BY_ID = ( -  id: string -): Query<unknown, MerchantBackend.Products.ProductDetail> => ({ -  get: `http://backend/instances/default/private/products/${id}`, -}); - -export const API_UPDATE_PRODUCT_BY_ID = ( -  id: string -): Query< -  MerchantBackend.Products.ProductPatchDetail, -  MerchantBackend.Products.InventorySummaryResponse -> => ({ -  patch: `http://backend/instances/default/private/products/${id}`, -}); - -export const API_DELETE_PRODUCT = ( -  id: string -): Query< -  unknown, unknown -> => ({ -  delete: `http://backend/instances/default/private/products/${id}`, -}); - -//////////////////// -// RESERVES -//////////////////// - -export const API_CREATE_RESERVE: Query< -  MerchantBackend.Tips.ReserveCreateRequest, -  MerchantBackend.Tips.ReserveCreateConfirmation -> = { -  post: "http://backend/instances/default/private/reserves", -}; -export const API_LIST_RESERVES: Query< -  unknown, -  MerchantBackend.Tips.TippingReserveStatus -> = { -  get: "http://backend/instances/default/private/reserves", -}; - -export const API_GET_RESERVE_BY_ID = ( -  pub: string -): Query<unknown, MerchantBackend.Tips.ReserveDetail> => ({ -  get: `http://backend/instances/default/private/reserves/${pub}`, -}); - -export const API_GET_TIP_BY_ID = ( -  pub: string -): Query< -  unknown, -  MerchantBackend.Tips.TipDetails -> => ({ -  get: `http://backend/instances/default/private/tips/${pub}`, -}); - -export const API_AUTHORIZE_TIP_FOR_RESERVE = ( -  pub: string -): Query< -  MerchantBackend.Tips.TipCreateRequest, -  MerchantBackend.Tips.TipCreateConfirmation -> => ({ -  post: `http://backend/instances/default/private/reserves/${pub}/authorize-tip`, -}); - -export const API_AUTHORIZE_TIP: Query< -  MerchantBackend.Tips.TipCreateRequest, -  MerchantBackend.Tips.TipCreateConfirmation -> = ({ -  post: `http://backend/instances/default/private/tips`, -}); - - -export const API_DELETE_RESERVE = ( -  id: string -): Query<unknown, unknown> => ({ -  delete: `http://backend/instances/default/private/reserves/${id}`, -}); - - -//////////////////// -// INSTANCE ADMIN -//////////////////// - -export const API_CREATE_INSTANCE: Query< -  MerchantBackend.Instances.InstanceConfigurationMessage, -  unknown -> = { -  post: "http://backend/management/instances", -}; - -export const API_GET_INSTANCE_BY_ID = ( -  id: string -): Query< -  unknown, -  MerchantBackend.Instances.QueryInstancesResponse -> => ({ -  get: `http://backend/management/instances/${id}`, -}); - -export const API_GET_INSTANCE_KYC_BY_ID = ( -  id: string -): Query< -  unknown, -  MerchantBackend.Instances.AccountKycRedirects -> => ({ -  get: `http://backend/management/instances/${id}/kyc`, -}); - -export const API_LIST_INSTANCES: Query< -  unknown, -  MerchantBackend.Instances.InstancesResponse -> = { -  get: "http://backend/management/instances", -}; - -export const API_UPDATE_INSTANCE_BY_ID = ( -  id: string -): Query< -  MerchantBackend.Instances.InstanceReconfigurationMessage, -  unknown -> => ({ -  patch: `http://backend/management/instances/${id}`, -}); - -export const API_UPDATE_INSTANCE_AUTH_BY_ID = ( -  id: string -): Query< -  MerchantBackend.Instances.InstanceAuthConfigurationMessage, -  unknown -> => ({ -  post: `http://backend/management/instances/${id}/auth`, -}); - -export const API_DELETE_INSTANCE = ( -  id: string -): Query<unknown, unknown> => ({ -  delete: `http://backend/management/instances/${id}`, -}); - -//////////////////// -// INSTANCE  -//////////////////// - -export const API_GET_CURRENT_INSTANCE: Query< -  unknown, -  MerchantBackend.Instances.QueryInstancesResponse -> = ({ -  get: `http://backend/instances/default/private/`, -}); - -export const API_GET_CURRENT_INSTANCE_KYC: Query< -  unknown, -  MerchantBackend.Instances.AccountKycRedirects -> = -  ({ -    get: `http://backend/instances/default/private/kyc`, -  }); - -export const API_UPDATE_CURRENT_INSTANCE: Query< -  MerchantBackend.Instances.InstanceReconfigurationMessage, -  unknown -> = { -  patch: `http://backend/instances/default/private/`, -}; - -export const API_UPDATE_CURRENT_INSTANCE_AUTH: Query< -  MerchantBackend.Instances.InstanceAuthConfigurationMessage, -  unknown -> = { -  post: `http://backend/instances/default/private/auth`, -}; - -export const API_DELETE_CURRENT_INSTANCE: Query< -  unknown, -  unknown -> = ({ -  delete: `http://backend/instances/default/private`, -}); - - diff --git a/packages/merchant-backoffice-ui/tests/context/backend.test.tsx b/packages/merchant-backoffice-ui/tests/context/backend.test.tsx deleted file mode 100644 index 671c19d0b..000000000 --- a/packages/merchant-backoffice-ui/tests/context/backend.test.tsx +++ /dev/null @@ -1,172 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { renderHook } from "@testing-library/preact-hooks"; -import { ComponentChildren, h, VNode } from "preact"; -import { act } from "preact/test-utils"; -import { BackendContextProvider } from "../../src/context/backend.js"; -import { InstanceContextProvider } from "../../src/context/instance.js"; -import { MerchantBackend } from "../../src/declaration.js"; -import { -  useAdminAPI, -  useInstanceAPI, -  useManagementAPI, -} from "../../src/hooks/instance.js"; -import { -  API_CREATE_INSTANCE, -  API_GET_CURRENT_INSTANCE, -  API_UPDATE_CURRENT_INSTANCE_AUTH, -  API_UPDATE_INSTANCE_AUTH_BY_ID, -  assertJustExpectedRequestWereMade, -  AxiosMockEnvironment, -} from "../axiosMock.js"; - -interface TestingContextProps { -  children?: ComponentChildren; -} - -function TestingContext({ children }: TestingContextProps): VNode { -  return ( -    <BackendContextProvider defaultUrl="http://backend" initialToken="token"> -      {children} -    </BackendContextProvider> -  ); -} -function AdminTestingContext({ children }: TestingContextProps): VNode { -  return ( -    <BackendContextProvider defaultUrl="http://backend" initialToken="token"> -      <InstanceContextProvider -        value={{ -          token: "token", -          id: "default", -          admin: true, -          changeToken: () => null, -        }} -      > -        {children} -      </InstanceContextProvider> -    </BackendContextProvider> -  ); -} - -describe("backend context api ", () => { -  it("should use new token after updating the instance token in the settings as user", async () => { -    const env = new AxiosMockEnvironment(); - -    const { result, waitForNextUpdate } = renderHook( -      () => { -        const instance = useInstanceAPI(); -        const management = useManagementAPI("default"); -        const admin = useAdminAPI(); - -        return { instance, management, admin }; -      }, -      { wrapper: TestingContext } -    ); - -    if (!result.current) { -      expect(result.current).toBeDefined(); -      return; -    } - -    env.addRequestExpectation(API_UPDATE_INSTANCE_AUTH_BY_ID("default"), { -      request: { -        method: "token", -        token: "another_token", -      }, -      response: { -        name: "instance_name", -      } as MerchantBackend.Instances.QueryInstancesResponse, -    }); - -    await act(async () => { -      await result.current?.management.setNewToken("another_token"); -    }); - -    // await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    env.addRequestExpectation(API_CREATE_INSTANCE, { -      auth: "Bearer another_token", -      request: { -        id: "new_instance_id", -      } as MerchantBackend.Instances.InstanceConfigurationMessage, -    }); - -    result.current.admin.createInstance({ -      id: "new_instance_id", -    } as MerchantBackend.Instances.InstanceConfigurationMessage); - -    assertJustExpectedRequestWereMade(env); -  }); - -  it("should use new token after updating the instance token in the settings as admin", async () => { -    const env = new AxiosMockEnvironment(); - -    const { result, waitForNextUpdate } = renderHook( -      () => { -        const instance = useInstanceAPI(); -        const management = useManagementAPI("default"); -        const admin = useAdminAPI(); - -        return { instance, management, admin }; -      }, -      { wrapper: AdminTestingContext } -    ); - -    if (!result.current) { -      expect(result.current).toBeDefined(); -      return; -    } - -    env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { -      request: { -        method: "token", -        token: "another_token", -      }, -      response: { -        name: "instance_name", -      } as MerchantBackend.Instances.QueryInstancesResponse, -    }); - -    await act(async () => { -      await result.current?.instance.setNewToken("another_token"); -    }); - -    // await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    env.addRequestExpectation(API_CREATE_INSTANCE, { -      auth: "Bearer another_token", -      request: { -        id: "new_instance_id", -      } as MerchantBackend.Instances.InstanceConfigurationMessage, -    }); - -    result.current.admin.createInstance({ -      id: "new_instance_id", -    } as MerchantBackend.Instances.InstanceConfigurationMessage); - -    assertJustExpectedRequestWereMade(env); -  }); -}); diff --git a/packages/merchant-backoffice-ui/tests/declarations.d.ts b/packages/merchant-backoffice-ui/tests/declarations.d.ts deleted file mode 100644 index 677aa9f24..000000000 --- a/packages/merchant-backoffice-ui/tests/declarations.d.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -declare global { -  namespace jest { -    interface Matchers<R> { -      toBeWithinRange(a: number, b: number): R; -    } -  } -} diff --git a/packages/merchant-backoffice-ui/tests/header.test.tsx b/packages/merchant-backoffice-ui/tests/header.test.tsx deleted file mode 100644 index 1cf2b7e6c..000000000 --- a/packages/merchant-backoffice-ui/tests/header.test.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h } from "preact"; -import { ProductList } from "../src/components/product/ProductList.js"; -// See: https://github.com/preactjs/enzyme-adapter-preact-pure -// import { shallow } from 'enzyme'; -import { render } from "@testing-library/preact"; -import * as backend from "../src/context/config.js"; -// import * as i18n from "../src/context/translation.js"; - -// import * as jedLib from "jed"; -// const handler = new jedLib.Jed("en"); - -describe("Initial Test of the Sidebar", () => { -  beforeEach(() => { -    jest -      .spyOn(backend, "useConfigContext") -      .mockImplementation(() => ({ version: "", currency: "" })); -    // jest.spyOn(i18n, "useTranslationContext").mockImplementation(() => ({ -    //   changeLanguage: () => null, -    //   handler, -    //   lang: "en", -    // })); -  }); -  test("Product list renders a table", () => { -    const context = render( -      <ProductList -        list={[ -          { -            description: "description of the product", -            image: "asdasda", -            price: "USD:10", -            quantity: 1, -            taxes: [{ name: "VAT", tax: "EUR:1" }], -            unit: "book", -          }, -        ]} -      />, -    ); - -    expect(context.findAllByText("description of the product")).toBeDefined(); -    // expect(context.find('table tr td img').map(img => img.prop('src'))).toEqual(''); -  }); -}); diff --git a/packages/merchant-backoffice-ui/tests/hooks/async.test.ts b/packages/merchant-backoffice-ui/tests/hooks/async.test.ts deleted file mode 100644 index 18cfc5c55..000000000 --- a/packages/merchant-backoffice-ui/tests/hooks/async.test.ts +++ /dev/null @@ -1,158 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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 { renderHook } from "@testing-library/preact-hooks" -import { useAsync } from "../../src/hooks/async.js" - -/** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ -test("async function is called", async () => { -  jest.useFakeTimers() - -  const timeout = 500 - -  const asyncFunction = jest.fn(() => new Promise((res) => { -    setTimeout(() => { -      res({ the_answer: 'yes' }) -    }, timeout); -  })) - -  const { result, waitForNextUpdate } = renderHook(() => { -    return useAsync(asyncFunction) -  }) - -  expect(result.current?.isLoading).toBeFalsy() - -  result.current?.request() -  expect(asyncFunction).toBeCalled() -  await waitForNextUpdate({ timeout: 1 }) -  expect(result.current?.isLoading).toBeTruthy() - -  jest.advanceTimersByTime(timeout + 1) -  await waitForNextUpdate({ timeout: 1 }) -  expect(result.current?.isLoading).toBeFalsy() -  expect(result.current?.data).toMatchObject({ the_answer: 'yes' }) -  expect(result.current?.error).toBeUndefined() -  expect(result.current?.isSlow).toBeFalsy() -}) - -test("async function return error if rejected", async () => { -  jest.useFakeTimers() - -  const timeout = 500 - -  const asyncFunction = jest.fn(() => new Promise((_, rej) => { -    setTimeout(() => { -      rej({ the_error: 'yes' }) -    }, timeout); -  })) - -  const { result, waitForNextUpdate } = renderHook(() => { -    return useAsync(asyncFunction) -  }) - -  expect(result.current?.isLoading).toBeFalsy() - -  result.current?.request() -  expect(asyncFunction).toBeCalled() -  await waitForNextUpdate({ timeout: 1 }) -  expect(result.current?.isLoading).toBeTruthy() - -  jest.advanceTimersByTime(timeout + 1) -  await waitForNextUpdate({ timeout: 1 }) -  expect(result.current?.isLoading).toBeFalsy() -  expect(result.current?.error).toMatchObject({ the_error: 'yes' }) -  expect(result.current?.data).toBeUndefined() -  expect(result.current?.isSlow).toBeFalsy() -}) - -test("async function is slow", async () => { -  jest.useFakeTimers() - -  const timeout = 2200 - -  const asyncFunction = jest.fn(() => new Promise((res) => { -    setTimeout(() => { -      res({ the_answer: 'yes' }) -    }, timeout); -  })) - -  const { result, waitForNextUpdate } = renderHook(() => { -    return useAsync(asyncFunction) -  }) - -  expect(result.current?.isLoading).toBeFalsy() - -  result.current?.request() -  expect(asyncFunction).toBeCalled() -  await waitForNextUpdate({ timeout: 1 }) -  expect(result.current?.isLoading).toBeTruthy() - -  jest.advanceTimersByTime(timeout / 2) -  await waitForNextUpdate({ timeout: 1 }) -  expect(result.current?.isLoading).toBeTruthy() -  expect(result.current?.isSlow).toBeTruthy() -  expect(result.current?.data).toBeUndefined() -  expect(result.current?.error).toBeUndefined() - -  jest.advanceTimersByTime(timeout / 2) -  await waitForNextUpdate({ timeout: 1 }) -  expect(result.current?.isLoading).toBeFalsy() -  expect(result.current?.data).toMatchObject({ the_answer: 'yes' }) -  expect(result.current?.error).toBeUndefined() -  expect(result.current?.isSlow).toBeFalsy() - -}) - -test("async function is cancellable", async () => { -  jest.useFakeTimers() - -  const timeout = 2200 - -  const asyncFunction = jest.fn(() => new Promise((res) => { -    setTimeout(() => { -      res({ the_answer: 'yes' }) -    }, timeout); -  })) - -  const { result, waitForNextUpdate } = renderHook(() => { -    return useAsync(asyncFunction) -  }) - -  expect(result.current?.isLoading).toBeFalsy() - -  result.current?.request() -  expect(asyncFunction).toBeCalled() -  await waitForNextUpdate({ timeout: 1 }) -  expect(result.current?.isLoading).toBeTruthy() - -  jest.advanceTimersByTime(timeout / 2) -  await waitForNextUpdate({ timeout: 1 }) -  expect(result.current?.isLoading).toBeTruthy() -  expect(result.current?.isSlow).toBeTruthy() -  expect(result.current?.data).toBeUndefined() -  expect(result.current?.error).toBeUndefined() - -  result.current?.cancel() -  await waitForNextUpdate({ timeout: 1 }) -  expect(result.current?.isLoading).toBeFalsy() -  expect(result.current?.data).toBeUndefined() -  expect(result.current?.error).toBeUndefined() -  expect(result.current?.isSlow).toBeFalsy() - -}) diff --git a/packages/merchant-backoffice-ui/tests/hooks/listener.test.ts b/packages/merchant-backoffice-ui/tests/hooks/listener.test.ts deleted file mode 100644 index 8afd5f8d1..000000000 --- a/packages/merchant-backoffice-ui/tests/hooks/listener.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ - -import { renderHook, act } from '@testing-library/preact-hooks'; -import { useListener } from "../../src/hooks/listener.js"; - -// jest.useFakeTimers() - -test('listener', async () => { - - -  function createSomeString() { -    return "hello" -  } -  async function addWorldToTheEnd(resultFromComponentB: string) { -    return `${resultFromComponentB} world` -  } -  const expectedResult = "hello world" - -  const { result } = renderHook(() => useListener(addWorldToTheEnd)) - -  expect(result.current).toBeDefined() -  if (!result.current) { -    return; -  } - -  { -    const [activator, subscriber] = result.current -    expect(activator).toBeUndefined() - -    act(() => { -      subscriber(createSomeString) -    }) - -  } - -  const [activator] = result.current -  expect(activator).toBeDefined() -  if (!activator) return; - -  const response = await activator() -  expect(response).toBe(expectedResult) - -}); diff --git a/packages/merchant-backoffice-ui/tests/hooks/notification.test.ts b/packages/merchant-backoffice-ui/tests/hooks/notification.test.ts deleted file mode 100644 index 801aa0e2e..000000000 --- a/packages/merchant-backoffice-ui/tests/hooks/notification.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - - /** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { renderHook, act} from '@testing-library/preact-hooks'; -import { useNotifications } from "../../src/hooks/notifications.js"; - -jest.useFakeTimers() - -test('notification should disappear after timeout', () => { -  jest.spyOn(global, 'setTimeout'); - -  const timeout = 1000 -  const { result, rerender } = renderHook(() => useNotifications(undefined, timeout)); - -  expect(result.current?.notifications.length).toBe(0); - -  act(() => { -    result.current?.pushNotification({ -      message: 'some_id', -      type: 'INFO' -    }); -  }); -  expect(result.current?.notifications.length).toBe(1); - -  jest.advanceTimersByTime(timeout/2); -  rerender() -  expect(result.current?.notifications.length).toBe(1); - -  jest.advanceTimersByTime(timeout); -  rerender() -  expect(result.current?.notifications.length).toBe(0); - -}); diff --git a/packages/merchant-backoffice-ui/tests/hooks/swr/index.tsx b/packages/merchant-backoffice-ui/tests/hooks/swr/index.tsx deleted file mode 100644 index 2608523e6..000000000 --- a/packages/merchant-backoffice-ui/tests/hooks/swr/index.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { ComponentChildren, h, VNode } from "preact"; -import { SWRConfig } from "swr"; -import { BackendContextProvider } from "../../../src/context/backend.js"; -import { InstanceContextProvider } from "../../../src/context/instance.js"; - -interface TestingContextProps { -  children?: ComponentChildren; -} -export function TestingContext({ children }: TestingContextProps): VNode { -  const SC: any = SWRConfig -  return ( -    <BackendContextProvider defaultUrl="http://backend" initialToken="token"> -      <InstanceContextProvider -        value={{ -          token: "token", -          id: "default", -          admin: true, -          changeToken: () => null, -        }} -      > -        <SC value={{ provider: () => new Map() }}>{children}</SC> -      </InstanceContextProvider> -    </BackendContextProvider> -  ); -} diff --git a/packages/merchant-backoffice-ui/tests/hooks/swr/instance.test.ts b/packages/merchant-backoffice-ui/tests/hooks/swr/instance.test.ts deleted file mode 100644 index 36a2f7241..000000000 --- a/packages/merchant-backoffice-ui/tests/hooks/swr/instance.test.ts +++ /dev/null @@ -1,636 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { renderHook } from "@testing-library/preact-hooks"; -import { act } from "preact/test-utils"; -import { MerchantBackend } from "../../../src/declaration.js"; -import { useAdminAPI, useBackendInstances, useInstanceAPI, useInstanceDetails, useManagementAPI } from "../../../src/hooks/instance.js"; -import { -  API_CREATE_INSTANCE, -  API_DELETE_INSTANCE, -  API_GET_CURRENT_INSTANCE, -  API_LIST_INSTANCES, -  API_UPDATE_CURRENT_INSTANCE, -  API_UPDATE_CURRENT_INSTANCE_AUTH, -  API_UPDATE_INSTANCE_AUTH_BY_ID, -  API_UPDATE_INSTANCE_BY_ID, -  assertJustExpectedRequestWereMade, -  AxiosMockEnvironment -} from "../../axiosMock.js"; -import { TestingContext } from "./index.js"; - -describe("instance api interaction with details", () => { - -  it("should evict cache when updating an instance", async () => { - -    const env = new AxiosMockEnvironment(); - -    env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { -      response: { -        name: 'instance_name' -      } as MerchantBackend.Instances.QueryInstancesResponse, -    }); - -    const { result, waitForNextUpdate } = renderHook( -      () => { -        const api = useInstanceAPI(); -        const query = useInstanceDetails(); - -        return { query, api }; -      }, -      { wrapper: TestingContext } -    ); - -    expect(result.current).toBeDefined(); -    if (!result.current) { -      return; -    } -    expect(result.current.query.loading).toBeTruthy(); - -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); - -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      name: 'instance_name' -    }); - -    env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE, { -      request: { -        name: 'other_name' -      } as MerchantBackend.Instances.InstanceReconfigurationMessage, -    }); - -    act(async () => { -      await result.current?.api.updateInstance({ -        name: 'other_name' -      } as MerchantBackend.Instances.InstanceReconfigurationMessage); -    }); - -    assertJustExpectedRequestWereMade(env); - -    env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { -      response: { -        name: 'other_name' -      } as MerchantBackend.Instances.QueryInstancesResponse, -    }); - -    expect(result.current.query.loading).toBeFalsy(); - -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current.query.ok).toBeTruthy(); - -    expect(result.current.query.data).toEqual({ -      name: 'other_name' -    }); -  }); - -  it("should evict cache when setting the instance's token", async () => { -    const env = new AxiosMockEnvironment(); - -    env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { -      response: { -        name: 'instance_name', -        auth: { -          method: 'token', -          token: 'not-secret', -        } -      } as MerchantBackend.Instances.QueryInstancesResponse, -    }); - -    const { result, waitForNextUpdate } = renderHook( -      () => { -        const api = useInstanceAPI(); -        const query = useInstanceDetails(); - -        return { query, api }; -      }, -      { wrapper: TestingContext } -    ); - -    expect(result.current).toBeDefined(); -    if (!result.current) { -      return; -    } -    expect(result.current.query.loading).toBeTruthy(); - -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); - -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      name: 'instance_name', -      auth: { -        method: 'token', -        token: 'not-secret', -      } -    }); - -    env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { -      request: { -        method: 'token', -        token: 'secret' -      } as MerchantBackend.Instances.InstanceAuthConfigurationMessage, -    }); - -    act(async () => { -      await result.current?.api.setNewToken('secret'); -    }); - -    assertJustExpectedRequestWereMade(env); - -    env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { -      response: { -        name: 'instance_name', -        auth: { -          method: 'token', -          token: 'secret', -        } -      } as MerchantBackend.Instances.QueryInstancesResponse, -    }); - -    expect(result.current.query.loading).toBeFalsy(); - -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current.query.ok).toBeTruthy(); - -    expect(result.current.query.data).toEqual({ -      name: 'instance_name', -      auth: { -        method: 'token', -        token: 'secret', -      } -    }); -  }); - -  it("should evict cache when clearing the instance's token", async () => { -    const env = new AxiosMockEnvironment(); - -    env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { -      response: { -        name: 'instance_name', -        auth: { -          method: 'token', -          token: 'not-secret', -        } -      } as MerchantBackend.Instances.QueryInstancesResponse, -    }); - -    const { result, waitForNextUpdate } = renderHook( -      () => { -        const api = useInstanceAPI(); -        const query = useInstanceDetails(); - -        return { query, api }; -      }, -      { wrapper: TestingContext } -    ); - -    expect(result.current).toBeDefined(); -    if (!result.current) { -      return; -    } -    expect(result.current.query.loading).toBeTruthy(); - -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); - -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      name: 'instance_name', -      auth: { -        method: 'token', -        token: 'not-secret', -      } -    }); - -    env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { -      request: { -        method: 'external', -      } as MerchantBackend.Instances.InstanceAuthConfigurationMessage, -    }); - -    act(async () => { -      await result.current?.api.clearToken(); -    }); - -    assertJustExpectedRequestWereMade(env); - -    env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { -      response: { -        name: 'instance_name', -        auth: { -          method: 'external', -        } -      } as MerchantBackend.Instances.QueryInstancesResponse, -    }); - -    expect(result.current.query.loading).toBeFalsy(); - -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current.query.ok).toBeTruthy(); - -    expect(result.current.query.data).toEqual({ -      name: 'instance_name', -      auth: { -        method: 'external', -      } -    }); -  }); -}); - -describe("instance admin api interaction with listing", () => { - -  it("should evict cache when creating a new instance", async () => { -    const env = new AxiosMockEnvironment(); - -    env.addRequestExpectation(API_LIST_INSTANCES, { -      response: { -        instances: [{ -          name: 'instance_name' -        } as MerchantBackend.Instances.Instance] -      }, -    }); - -    const { result, waitForNextUpdate } = renderHook( -      () => { -        const api = useAdminAPI(); -        const query = useBackendInstances(); - -        return { query, api }; -      }, -      { wrapper: TestingContext } -    ); - -    expect(result.current).toBeDefined(); -    if (!result.current) { -      return; -    } -    expect(result.current.query.loading).toBeTruthy(); - -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); - -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      instances: [{ -        name: 'instance_name' -      }] -    }); - -    env.addRequestExpectation(API_CREATE_INSTANCE, { -      request: { -        name: 'other_name' -      } as MerchantBackend.Instances.InstanceConfigurationMessage, -    }); - -    act(async () => { -      await result.current?.api.createInstance({ -        name: 'other_name' -      } as MerchantBackend.Instances.InstanceConfigurationMessage); -    }); - -    assertJustExpectedRequestWereMade(env); - -    env.addRequestExpectation(API_LIST_INSTANCES, { -      response: { -        instances: [{ -          name: 'instance_name' -        } as MerchantBackend.Instances.Instance, -        { -          name: 'other_name' -        } as MerchantBackend.Instances.Instance] -      }, -    }); - -    expect(result.current.query.loading).toBeFalsy(); - -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current.query.ok).toBeTruthy(); - -    expect(result.current.query.data).toEqual({ -      instances: [{ -        name: 'instance_name' -      }, { -        name: 'other_name' -      }] -    }); -  }); - -  it("should evict cache when deleting an instance", async () => { -    const env = new AxiosMockEnvironment(); - -    env.addRequestExpectation(API_LIST_INSTANCES, { -      response: { -        instances: [{ -          id: 'default', -          name: 'instance_name' -        } as MerchantBackend.Instances.Instance, -        { -          id: 'the_id', -          name: 'second_instance' -        } as MerchantBackend.Instances.Instance] -      }, -    }); - -    const { result, waitForNextUpdate } = renderHook( -      () => { -        const api = useAdminAPI(); -        const query = useBackendInstances(); - -        return { query, api }; -      }, -      { wrapper: TestingContext } -    ); - -    expect(result.current).toBeDefined(); -    if (!result.current) { -      return; -    } -    expect(result.current.query.loading).toBeTruthy(); - -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); - -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      instances: [{ -        id: 'default', -        name: 'instance_name' -      }, { -        id: 'the_id', -        name: 'second_instance' -      }] -    }); - -    env.addRequestExpectation(API_DELETE_INSTANCE('the_id'), {}); - -    act(async () => { -      await result.current?.api.deleteInstance('the_id'); -    }); - -    assertJustExpectedRequestWereMade(env); - -    env.addRequestExpectation(API_LIST_INSTANCES, { -      response: { -        instances: [{ -          id: 'default', -          name: 'instance_name' -        } as MerchantBackend.Instances.Instance] -      }, -    }); - -    expect(result.current.query.loading).toBeFalsy(); - -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current.query.ok).toBeTruthy(); - -    expect(result.current.query.data).toEqual({ -      instances: [{ -        id: 'default', -        name: 'instance_name' -      }] -    }); -  }); -  it("should evict cache when deleting (purge) an instance", async () => { -    const env = new AxiosMockEnvironment(); - -    env.addRequestExpectation(API_LIST_INSTANCES, { -      response: { -        instances: [{ -          id: 'default', -          name: 'instance_name' -        } as MerchantBackend.Instances.Instance, -        { -          id: 'the_id', -          name: 'second_instance' -        } as MerchantBackend.Instances.Instance] -      }, -    }); - -    const { result, waitForNextUpdate } = renderHook( -      () => { -        const api = useAdminAPI(); -        const query = useBackendInstances(); - -        return { query, api }; -      }, -      { wrapper: TestingContext } -    ); - -    expect(result.current).toBeDefined(); -    if (!result.current) { -      return; -    } -    expect(result.current.query.loading).toBeTruthy(); - -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); - -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      instances: [{ -        id: 'default', -        name: 'instance_name' -      }, { -        id: 'the_id', -        name: 'second_instance' -      }] -    }); - -    env.addRequestExpectation(API_DELETE_INSTANCE('the_id'), { -      qparam: { -        purge: 'YES' -      } -    }); - -    act(async () => { -      await result.current?.api.purgeInstance('the_id'); -    }); - -    assertJustExpectedRequestWereMade(env); - -    env.addRequestExpectation(API_LIST_INSTANCES, { -      response: { -        instances: [{ -          id: 'default', -          name: 'instance_name' -        } as MerchantBackend.Instances.Instance] -      }, -    }); - -    expect(result.current.query.loading).toBeFalsy(); - -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current.query.ok).toBeTruthy(); - -    expect(result.current.query.data).toEqual({ -      instances: [{ -        id: 'default', -        name: 'instance_name' -      }] -    }); -  }); -}); - -describe("instance management api interaction with listing", () => { - -  it("should evict cache when updating an instance", async () => { -    const env = new AxiosMockEnvironment(); - -    env.addRequestExpectation(API_LIST_INSTANCES, { -      response: { -        instances: [{ -          id: 'managed', -          name: 'instance_name' -        } as MerchantBackend.Instances.Instance] -      }, -    }); - -    const { result, waitForNextUpdate } = renderHook( -      () => { -        const api = useManagementAPI('managed'); -        const query = useBackendInstances(); - -        return { query, api }; -      }, -      { wrapper: TestingContext } -    ); - -    expect(result.current).toBeDefined(); -    if (!result.current) { -      return; -    } -    expect(result.current.query.loading).toBeTruthy(); - -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); - -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      instances: [{ -        id: 'managed', -        name: 'instance_name' -      }] -    }); - -    env.addRequestExpectation(API_UPDATE_INSTANCE_BY_ID('managed'), { -      request: { -        name: 'other_name' -      } as MerchantBackend.Instances.InstanceReconfigurationMessage, -    }); - -    act(async () => { -      await result.current?.api.updateInstance({ -        name: 'other_name' -      } as MerchantBackend.Instances.InstanceConfigurationMessage); -    }); - -    assertJustExpectedRequestWereMade(env); - -    env.addRequestExpectation(API_LIST_INSTANCES, { -      response: { -        instances: [ -          { -            id: 'managed', -            name: 'other_name' -          } as MerchantBackend.Instances.Instance] -      }, -    }); - -    expect(result.current.query.loading).toBeFalsy(); - -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current.query.ok).toBeTruthy(); - -    expect(result.current.query.data).toEqual({ -      instances: [{ -        id: 'managed', -        name: 'other_name' -      }] -    }); -  }); - -}); - diff --git a/packages/merchant-backoffice-ui/tests/hooks/swr/order.test.ts b/packages/merchant-backoffice-ui/tests/hooks/swr/order.test.ts deleted file mode 100644 index dc6104e43..000000000 --- a/packages/merchant-backoffice-ui/tests/hooks/swr/order.test.ts +++ /dev/null @@ -1,567 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { renderHook } from "@testing-library/preact-hooks"; -import { act } from "preact/test-utils"; -import { TestingContext } from "."; -import { MerchantBackend } from "../../../src/declaration.js"; -import { useInstanceOrders, useOrderAPI, useOrderDetails } from "../../../src/hooks/order.js"; -import { -  API_CREATE_ORDER, -  API_DELETE_ORDER, -  API_FORGET_ORDER_BY_ID, -  API_GET_ORDER_BY_ID, -  API_LIST_ORDERS, API_REFUND_ORDER_BY_ID, assertJustExpectedRequestWereMade, assertNextRequest, assertNoMoreRequestWereMade, AxiosMockEnvironment -} from "../../axiosMock.js"; - -describe("order api interaction with listing", () => { - -  it("should evict cache when creating an order", async () => { -    const env = new AxiosMockEnvironment(); - -    env.addRequestExpectation(API_LIST_ORDERS, { -      qparam: { delta: 0, paid: "yes" }, -      response: { -        orders: [{ order_id: "1" } as MerchantBackend.Orders.OrderHistoryEntry], -      }, -    }); - -    env.addRequestExpectation(API_LIST_ORDERS, { -      qparam: { delta: -20, paid: "yes" }, -      response: { -        orders: [{ order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry], -      }, -    }); - - -    const { result, waitForNextUpdate } = renderHook(() => { -      const newDate = (d: Date) => { -        console.log("new date", d); -      }; -      const query = useInstanceOrders({ paid: "yes" }, newDate); -      const api = useOrderAPI(); - -      return { query, api }; -    }, { wrapper: TestingContext }); - -    expect(result.current).toBeDefined(); -    if (!result.current) { -      return; -    } - -    expect(result.current.query.loading).toBeTruthy(); -    await waitForNextUpdate({ timeout: 1 }); -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      orders: [{ order_id: "1" }, { order_id: "2" }], -    }); - -    env.addRequestExpectation(API_CREATE_ORDER, { -      request: { -        order: { amount: "ARS:12", summary: "pay me" }, -      }, -      response: { order_id: "3" }, -    }); - -    env.addRequestExpectation(API_LIST_ORDERS, { -      qparam: { delta: 0, paid: "yes" }, -      response: { -        orders: [{ order_id: "1" } as any], -      }, -    }); - -    env.addRequestExpectation(API_LIST_ORDERS, { -      qparam: { delta: -20, paid: "yes" }, -      response: { -        orders: [{ order_id: "2" } as any, { order_id: "3" } as any], -      }, -    }); - -    act(async () => { -      await result.current?.api.createOrder({ -        order: { amount: "ARS:12", summary: "pay me" }, -      } as any); -    }); - -    await waitForNextUpdate({ timeout: 1 }); -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      orders: [{ order_id: "1" }, { order_id: "2" }, { order_id: "3" }], -    }); -  }); -  it("should evict cache when doing a refund", async () => { -    const env = new AxiosMockEnvironment(); - -    env.addRequestExpectation(API_LIST_ORDERS, { -      qparam: { delta: 0, paid: "yes" }, -      response: { -        orders: [{ order_id: "1", amount: 'EUR:12', refundable: true } as MerchantBackend.Orders.OrderHistoryEntry], -      }, -    }); - -    env.addRequestExpectation(API_LIST_ORDERS, { -      qparam: { delta: -20, paid: "yes" }, -      response: { orders: [], }, -    }); - - -    const { result, waitForNextUpdate } = renderHook(() => { -      const newDate = (d: Date) => { -        console.log("new date", d); -      }; -      const query = useInstanceOrders({ paid: "yes" }, newDate); -      const api = useOrderAPI(); - -      return { query, api }; -    }, { wrapper: TestingContext }); - -    expect(result.current).toBeDefined(); -    if (!result.current) { -      return; -    } - -    expect(result.current.query.loading).toBeTruthy(); -    await waitForNextUpdate({ timeout: 1 }); -    assertJustExpectedRequestWereMade(env); - - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      orders: [{ -        order_id: "1", -        amount: 'EUR:12', -        refundable: true, -      }], -    }); - -    env.addRequestExpectation(API_REFUND_ORDER_BY_ID('1'), { -      request: { -        reason: 'double pay', -        refund: 'EUR:1' -      }, -    }); - -    env.addRequestExpectation(API_LIST_ORDERS, { -      qparam: { delta: 0, paid: "yes" }, -      response: { -        orders: [{ order_id: "1", amount: 'EUR:12', refundable: false } as any], -      }, -    }); - -    env.addRequestExpectation(API_LIST_ORDERS, { -      qparam: { delta: -20, paid: "yes" }, -      response: { orders: [], }, -    }); - -    act(async () => { -      await result.current?.api.refundOrder('1', { -        reason: 'double pay', -        refund: 'EUR:1' -      }); -    }); - -    await waitForNextUpdate({ timeout: 1 }); -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      orders: [{ -        order_id: "1", -        amount: 'EUR:12', -        refundable: false, -      }], -    }); -  }); -  it("should evict cache when deleting an order", async () => { -    const env = new AxiosMockEnvironment(); - -    env.addRequestExpectation(API_LIST_ORDERS, { -      qparam: { delta: 0, paid: "yes" }, -      response: { -        orders: [{ order_id: "1" } as MerchantBackend.Orders.OrderHistoryEntry], -      }, -    }); - -    env.addRequestExpectation(API_LIST_ORDERS, { -      qparam: { delta: -20, paid: "yes" }, -      response: { -        orders: [{ order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry], -      }, -    }); - - -    const { result, waitForNextUpdate } = renderHook(() => { -      const newDate = (d: Date) => { -        console.log("new date", d); -      }; -      const query = useInstanceOrders({ paid: "yes" }, newDate); -      const api = useOrderAPI(); - -      return { query, api }; -    }, { wrapper: TestingContext }); - -    expect(result.current).toBeDefined(); -    if (!result.current) { -      return; -    } - -    expect(result.current.query.loading).toBeTruthy(); -    await waitForNextUpdate({ timeout: 1 }); -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      orders: [{ order_id: "1" }, { order_id: "2" }], -    }); - -    env.addRequestExpectation(API_DELETE_ORDER('1'), {}); - -    env.addRequestExpectation(API_LIST_ORDERS, { -      qparam: { delta: 0, paid: "yes" }, -      response: { -        orders: [], -      }, -    }); - -    env.addRequestExpectation(API_LIST_ORDERS, { -      qparam: { delta: -20, paid: "yes" }, -      response: { -        orders: [{ order_id: "2" } as any], -      }, -    }); - -    act(async () => { -      await result.current?.api.deleteOrder('1'); -    }); - -    await waitForNextUpdate({ timeout: 1 }); -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      orders: [{ order_id: "2" }], -    }); -  }); - -}); - -describe("order api interaction with details", () => { - -  it("should evict cache when doing a refund", async () => { -    const env = new AxiosMockEnvironment(); - -    env.addRequestExpectation(API_GET_ORDER_BY_ID('1'), { -      // qparam: { delta: 0, paid: "yes" }, -      response: { -        summary: 'description', -        refund_amount: 'EUR:0', -      } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, -    }); - -    const { result, waitForNextUpdate } = renderHook(() => { -      const query = useOrderDetails('1') -      const api = useOrderAPI(); - -      return { query, api }; -    }, { wrapper: TestingContext }); - -    expect(result.current).toBeDefined(); -    if (!result.current) { -      return; -    } - -    expect(result.current.query.loading).toBeTruthy(); -    await waitForNextUpdate({ timeout: 1 }); -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      summary: 'description', -      refund_amount: 'EUR:0', -    }); - -    env.addRequestExpectation(API_REFUND_ORDER_BY_ID('1'), { -      request: { -        reason: 'double pay', -        refund: 'EUR:1' -      }, -    }); - -    env.addRequestExpectation(API_GET_ORDER_BY_ID('1'), { -      response: { -        summary: 'description', -        refund_amount: 'EUR:1', -      } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, -    }); - -    act(async () => { -      await result.current?.api.refundOrder('1', { -        reason: 'double pay', -        refund: 'EUR:1' -      }); -    }); - -    await waitForNextUpdate({ timeout: 1 }); -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      summary: 'description', -      refund_amount: 'EUR:1', -    }); -  }) -  it("should evict cache when doing a forget", async () => { -    const env = new AxiosMockEnvironment(); - -    env.addRequestExpectation(API_GET_ORDER_BY_ID('1'), { -      // qparam: { delta: 0, paid: "yes" }, -      response: { -        summary: 'description', -        refund_amount: 'EUR:0', -      } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, -    }); - -    const { result, waitForNextUpdate } = renderHook(() => { -      const query = useOrderDetails('1') -      const api = useOrderAPI(); - -      return { query, api }; -    }, { wrapper: TestingContext }); - -    expect(result.current).toBeDefined(); -    if (!result.current) { -      return; -    } - -    expect(result.current.query.loading).toBeTruthy(); -    await waitForNextUpdate({ timeout: 1 }); -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      summary: 'description', -      refund_amount: 'EUR:0', -    }); - -    env.addRequestExpectation(API_FORGET_ORDER_BY_ID('1'), { -      request: { -        fields: ['$.summary'] -      }, -    }); - -    env.addRequestExpectation(API_GET_ORDER_BY_ID('1'), { -      response: { -        summary: undefined, -      } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, -    }); - -    act(async () => { -      await result.current?.api.forgetOrder('1', { -        fields: ['$.summary'] -      }); -    }); - -    await waitForNextUpdate({ timeout: 1 }); -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      summary: undefined, -    }); -  }) -}) - -describe("order listing pagination", () => { - -  it("should not load more if has reach the end", async () => { -    const env = new AxiosMockEnvironment(); -    env.addRequestExpectation(API_LIST_ORDERS, { -      qparam: { delta: 20, wired: "yes", date_ms: 12 }, -      response: { -        orders: [{ order_id: "1" } as any], -      }, -    }); - -    env.addRequestExpectation(API_LIST_ORDERS, { -      qparam: { delta: -20, wired: "yes", date_ms: 13 }, -      response: { -        orders: [{ order_id: "2" } as any], -      }, -    }); - - -    const { result, waitForNextUpdate } = renderHook(() => { -      const newDate = (d: Date) => { -        console.log("new date", d); -      }; -      const date = new Date(12); -      const query = useInstanceOrders({ wired: "yes", date }, newDate) -      return { query } -    }, { wrapper: TestingContext }); - -    assertJustExpectedRequestWereMade(env); - -    await waitForNextUpdate(); - -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      orders: [{ order_id: "1" }, { order_id: "2" }], -    }); - -    expect(result.current.query.isReachingEnd).toBeTruthy() -    expect(result.current.query.isReachingStart).toBeTruthy() - -    await act(() => { -      if (!result.current?.query.ok) throw Error("not ok"); -      result.current.query.loadMore(); -    }); -    assertNoMoreRequestWereMade(env); - -    await act(() => { -      if (!result.current?.query.ok) throw Error("not ok"); -      result.current.query.loadMorePrev(); -    }); -    assertNoMoreRequestWereMade(env); - -    expect(result.current.query.data).toEqual({ -      orders: [ -        { order_id: "1" }, -        { order_id: "2" }, -      ], -    }); -  }); - -  it("should load more if result brings more that PAGE_SIZE", async () => { -    const env = new AxiosMockEnvironment(); - -    const ordersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({ order_id: String(i) })) -    const ordersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({ order_id: String(i + 20) })) -    const ordersFrom20to0 = [...ordersFrom0to20].reverse() - -    env.addRequestExpectation(API_LIST_ORDERS, { -      qparam: { delta: 20, wired: "yes", date_ms: 12 }, -      response: { -        orders: ordersFrom0to20, -      }, -    }); - -    env.addRequestExpectation(API_LIST_ORDERS, { -      qparam: { delta: -20, wired: "yes", date_ms: 13 }, -      response: { -        orders: ordersFrom20to40, -      }, -    }); - -    const { result, waitForNextUpdate } = renderHook(() => { -      const newDate = (d: Date) => { -        console.log("new date", d); -      }; -      const date = new Date(12); -      const query = useInstanceOrders({ wired: "yes", date }, newDate) -      return { query } -    }, { wrapper: TestingContext }); - -    assertJustExpectedRequestWereMade(env); - -    await waitForNextUpdate({ timeout: 1 }); - -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      orders: [...ordersFrom20to0, ...ordersFrom20to40], -    }); - -    expect(result.current.query.isReachingEnd).toBeFalsy() -    expect(result.current.query.isReachingStart).toBeFalsy() - -    env.addRequestExpectation(API_LIST_ORDERS, { -      qparam: { delta: -40, wired: "yes", date_ms: 13 }, -      response: { -        orders: [...ordersFrom20to40, { order_id: '41' }], -      }, -    }); - -    await act(() => { -      if (!result.current?.query.ok) throw Error("not ok"); -      result.current.query.loadMore(); -    }); -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    env.addRequestExpectation(API_LIST_ORDERS, { -      qparam: { delta: 40, wired: "yes", date_ms: 12 }, -      response: { -        orders: [...ordersFrom0to20, { order_id: '-1' }], -      }, -    }); - -    await act(() => { -      if (!result.current?.query.ok) throw Error("not ok"); -      result.current.query.loadMorePrev(); -    }); -    await waitForNextUpdate({ timeout: 1 }); -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.data).toEqual({ -      orders: [{ order_id: '-1' }, ...ordersFrom20to0, ...ordersFrom20to40, { order_id: '41' }], -    }); -  }); - - -}); diff --git a/packages/merchant-backoffice-ui/tests/hooks/swr/product.test.ts b/packages/merchant-backoffice-ui/tests/hooks/swr/product.test.ts deleted file mode 100644 index 6e9247839..000000000 --- a/packages/merchant-backoffice-ui/tests/hooks/swr/product.test.ts +++ /dev/null @@ -1,338 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { renderHook } from "@testing-library/preact-hooks"; -import { act } from "preact/test-utils"; -import { TestingContext } from "."; -import { MerchantBackend } from "../../../src/declaration.js"; -import { useInstanceProducts, useProductAPI, useProductDetails } from "../../../src/hooks/product.js"; -import { -  API_CREATE_PRODUCT, -  API_DELETE_PRODUCT, API_GET_PRODUCT_BY_ID, -  API_LIST_PRODUCTS, -  API_UPDATE_PRODUCT_BY_ID, -  assertJustExpectedRequestWereMade, -  assertNextRequest, -  AxiosMockEnvironment -} from "../../axiosMock.js"; - -describe("product api interaction with listing", () => { -  it("should evict cache when creating a product", async () => { -    const env = new AxiosMockEnvironment(); - -    env.addRequestExpectation(API_LIST_PRODUCTS, { -      response: { -        products: [{ product_id: "1234" }], -      }, -    }); -    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { -      response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, -    }); - -    const { result, waitForNextUpdate } = renderHook( -      () => { -        const query = useInstanceProducts(); -        const api = useProductAPI(); -        return { api, query }; -      }, -      { wrapper: TestingContext } -    ); // get products -> loading - -    expect(result.current).toBeDefined(); -    if (!result.current) { -      return; -    } -    expect(result.current.query.loading).toBeTruthy(); -    await waitForNextUpdate({ timeout: 1 }); - -    await waitForNextUpdate({ timeout: 1 }); -    assertJustExpectedRequestWereMade(env); -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual([ -      { id: "1234", price: "ARS:12" }, -    ]); - -    env.addRequestExpectation(API_CREATE_PRODUCT, { -      request: { price: "ARS:23" } as MerchantBackend.Products.ProductAddDetail, -    }); - -    env.addRequestExpectation(API_LIST_PRODUCTS, { -      response: { -        products: [{ product_id: "1234" }, { product_id: "2345" }], -      }, -    }); -    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { -      response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, -    }); -    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { -      response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, -    }); -    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), { -      response: { price: "ARS:23" } as MerchantBackend.Products.ProductDetail, -    }); - -    act(async () => { -      await result.current?.api.createProduct({ -        price: "ARS:23", -      } as any); -    }); - -    assertNextRequest(env); -    await waitForNextUpdate({ timeout: 1 }); -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual([ -      { -        id: "1234", -        price: "ARS:12", -      }, -      { -        id: "2345", -        price: "ARS:23", -      }, -    ]); -  }); - -  it("should evict cache when updating a product", async () => { -    const env = new AxiosMockEnvironment(); - -    env.addRequestExpectation(API_LIST_PRODUCTS, { -      response: { -        products: [{ product_id: "1234" }], -      }, -    }); -    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { -      response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, -    }); - -    const { result, waitForNextUpdate } = renderHook( -      () => { -        const query = useInstanceProducts(); -        const api = useProductAPI(); -        return { api, query }; -      }, -      { wrapper: TestingContext } -    ); // get products -> loading - -    expect(result.current).toBeDefined(); -    if (!result.current) { -      return; -    } -    expect(result.current.query.loading).toBeTruthy(); -    await waitForNextUpdate({ timeout: 1 }); - -    await waitForNextUpdate({ timeout: 1 }); -    assertJustExpectedRequestWereMade(env); -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual([ -      { id: "1234", price: "ARS:12" }, -    ]); - -    env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("1234"), { -      request: { price: "ARS:13" } as MerchantBackend.Products.ProductPatchDetail, -    }); - -    env.addRequestExpectation(API_LIST_PRODUCTS, { -      response: { -        products: [{ product_id: "1234" }], -      }, -    }); -    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { -      response: { price: "ARS:13" } as MerchantBackend.Products.ProductDetail, -    }); - -    act(async () => { -      await result.current?.api.updateProduct("1234", { -        price: "ARS:13", -      } as any); -    }); - -    assertNextRequest(env); -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual([ -      { -        id: "1234", -        price: "ARS:13", -      }, -    ]); -  }); - -  it("should evict cache when deleting a product", async () => { -    const env = new AxiosMockEnvironment(); - -    env.addRequestExpectation(API_LIST_PRODUCTS, { -      response: { -        products: [{ product_id: "1234" }, { product_id: "2345" }], -      }, -    }); -    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { -      response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, -    }); -    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), { -      response: { price: "ARS:23" } as MerchantBackend.Products.ProductDetail, -    }); - -    const { result, waitForNextUpdate } = renderHook( -      () => { -        const query = useInstanceProducts(); -        const api = useProductAPI(); -        return { api, query }; -      }, -      { wrapper: TestingContext } -    ); // get products -> loading - -    expect(result.current).toBeDefined(); -    if (!result.current) { -      return; -    } -    expect(result.current.query.loading).toBeTruthy(); -    await waitForNextUpdate({ timeout: 1 }); - -    await waitForNextUpdate({ timeout: 1 }); -    // await waitForNextUpdate({ timeout: 1 }); -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual([ -      { id: "1234", price: "ARS:12" }, -      { id: "2345", price: "ARS:23" }, -    ]); - -    env.addRequestExpectation(API_DELETE_PRODUCT("2345"), {}); - -    env.addRequestExpectation(API_LIST_PRODUCTS, { -      response: { -        products: [{ product_id: "1234" }], -      }, -    }); -    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { -      response: { price: "ARS:13" } as MerchantBackend.Products.ProductDetail, -    }); - -    act(async () => { -      await result.current?.api.deleteProduct("2345"); -    }); - -    assertNextRequest(env); -    await waitForNextUpdate({ timeout: 1 }); -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual([ -      { -        id: "1234", -        price: "ARS:13", -      }, -    ]); -  }); - -}); - -describe("product api interaction with details", () => { -  it("should evict cache when updating a product", async () => { -    const env = new AxiosMockEnvironment(); - -    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), { -      response: { -        description: "this is a description", -      } as MerchantBackend.Products.ProductDetail, -    }); - -    const { result, waitForNextUpdate } = renderHook(() => { -      const query = useProductDetails("12"); -      const api = useProductAPI(); -      return { query, api }; -    }, { wrapper: TestingContext }); - -    expect(result.current).toBeDefined(); -    if (!result.current) { -      return; -    } -    expect(result.current.query.loading).toBeTruthy(); -    await waitForNextUpdate(); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      description: "this is a description", -    }); - -    env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("12"), { -      request: { description: "other description" } as MerchantBackend.Products.ProductPatchDetail, -    }); - -    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), { -      response: { -        description: "other description", -      } as MerchantBackend.Products.ProductDetail, -    }); - -    act(async () => { -      return await result.current?.api.updateProduct("12", { -        description: "other description", -      } as any); -    }); - -    assertNextRequest(env); -    await waitForNextUpdate(); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      description: "other description", -    }); -  }) -})
\ No newline at end of file diff --git a/packages/merchant-backoffice-ui/tests/hooks/swr/reserve.test.ts b/packages/merchant-backoffice-ui/tests/hooks/swr/reserve.test.ts deleted file mode 100644 index 8ebbee353..000000000 --- a/packages/merchant-backoffice-ui/tests/hooks/swr/reserve.test.ts +++ /dev/null @@ -1,470 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { renderHook } from "@testing-library/preact-hooks"; -import { act } from "preact/test-utils"; -import { MerchantBackend } from "../../../src/declaration.js"; -import { -  useInstanceReserves, -  useReserveDetails, -  useReservesAPI, -  useTipDetails, -} from "../../../src/hooks/reserves.js"; -import { -  API_AUTHORIZE_TIP, -  API_AUTHORIZE_TIP_FOR_RESERVE, -  API_CREATE_RESERVE, -  API_DELETE_RESERVE, -  API_GET_RESERVE_BY_ID, -  API_GET_TIP_BY_ID, -  API_LIST_RESERVES, -  assertJustExpectedRequestWereMade, -  AxiosMockEnvironment, -} from "../../axiosMock.js"; -import { TestingContext } from "./index.js"; - -describe("reserve api interaction with listing", () => { -  it("should evict cache when creating a reserve", async () => { -    const env = new AxiosMockEnvironment(); - -    env.addRequestExpectation(API_LIST_RESERVES, { -      response: { -        reserves: [ -          { -            reserve_pub: "11", -          } as MerchantBackend.Tips.ReserveStatusEntry, -        ], -      }, -    }); - -    const { result, waitForNextUpdate } = renderHook( -      () => { -        const api = useReservesAPI(); -        const query = useInstanceReserves(); - -        return { query, api }; -      }, -      { wrapper: TestingContext } -    ); - -    expect(result.current).toBeDefined(); -    if (!result.current) { -      return; -    } -    expect(result.current.query.loading).toBeTruthy(); - -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      reserves: [{ reserve_pub: "11" }], -    }); - -    env.addRequestExpectation(API_CREATE_RESERVE, { -      request: { -        initial_balance: "ARS:3333", -        exchange_url: "http://url", -        wire_method: "iban", -      }, -      response: { -        reserve_pub: "22", -        payto_uri: "payto", -      }, -    }); - -    act(async () => { -      await result.current?.api.createReserve({ -        initial_balance: "ARS:3333", -        exchange_url: "http://url", -        wire_method: "iban", -      }); -      return; -    }); - -    assertJustExpectedRequestWereMade(env); - -    env.addRequestExpectation(API_LIST_RESERVES, { -      response: { -        reserves: [ -          { -            reserve_pub: "11", -          } as MerchantBackend.Tips.ReserveStatusEntry, -          { -            reserve_pub: "22", -          } as MerchantBackend.Tips.ReserveStatusEntry, -        ], -      }, -    }); - -    expect(result.current.query.loading).toBeFalsy(); - -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current.query.ok).toBeTruthy(); - -    expect(result.current.query.data).toEqual({ -      reserves: [ -        { -          reserve_pub: "11", -        } as MerchantBackend.Tips.ReserveStatusEntry, -        { -          reserve_pub: "22", -        } as MerchantBackend.Tips.ReserveStatusEntry, -      ], -    }); -  }); - -  it("should evict cache when deleting a reserve", async () => { -    const env = new AxiosMockEnvironment(); - -    env.addRequestExpectation(API_LIST_RESERVES, { -      response: { -        reserves: [ -          { -            reserve_pub: "11", -          } as MerchantBackend.Tips.ReserveStatusEntry, -          { -            reserve_pub: "22", -          } as MerchantBackend.Tips.ReserveStatusEntry, -          { -            reserve_pub: "33", -          } as MerchantBackend.Tips.ReserveStatusEntry, -        ], -      }, -    }); - -    const { result, waitForNextUpdate } = renderHook( -      () => { -        const api = useReservesAPI(); -        const query = useInstanceReserves(); - -        return { query, api }; -      }, -      { -        wrapper: TestingContext, -      } -    ); - -    expect(result.current).toBeDefined(); -    if (!result.current) { -      return; -    } -    expect(result.current.query.loading).toBeTruthy(); - -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      reserves: [ -        { reserve_pub: "11" }, -        { reserve_pub: "22" }, -        { reserve_pub: "33" }, -      ], -    }); - -    env.addRequestExpectation(API_DELETE_RESERVE("11"), {}); - -    act(async () => { -      await result.current?.api.deleteReserve("11"); -      return; -    }); - -    assertJustExpectedRequestWereMade(env); - -    env.addRequestExpectation(API_LIST_RESERVES, { -      response: { -        reserves: [ -          { -            reserve_pub: "22", -          } as MerchantBackend.Tips.ReserveStatusEntry, -          { -            reserve_pub: "33", -          } as MerchantBackend.Tips.ReserveStatusEntry, -        ], -      }, -    }); - -    expect(result.current.query.loading).toBeFalsy(); - -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current.query.ok).toBeTruthy(); - -    expect(result.current.query.data).toEqual({ -      reserves: [ -        { -          reserve_pub: "22", -        } as MerchantBackend.Tips.ReserveStatusEntry, -        { -          reserve_pub: "33", -        } as MerchantBackend.Tips.ReserveStatusEntry, -      ], -    }); -  }); -}); - -describe("reserve api interaction with details", () => { -  it("should evict cache when adding a tip for a specific reserve", async () => { -    const env = new AxiosMockEnvironment(); - -    env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { -      response: { -        payto_uri: "payto://here", -        tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }], -      } as MerchantBackend.Tips.ReserveDetail, -    }); - -    const { result, waitForNextUpdate } = renderHook( -      () => { -        const api = useReservesAPI(); -        const query = useReserveDetails("11"); - -        return { query, api }; -      }, -      { -        wrapper: TestingContext, -      } -    ); - -    expect(result.current).toBeDefined(); -    if (!result.current) { -      return; -    } -    expect(result.current.query.loading).toBeTruthy(); - -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      payto_uri: "payto://here", -      tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }], -    }); - -    env.addRequestExpectation(API_AUTHORIZE_TIP_FOR_RESERVE("11"), { -      request: { -        amount: "USD:12", -        justification: "not", -        next_url: "http://taler.net", -      }, -      response: { -        tip_id: "id2", -        taler_tip_uri: "uri", -        tip_expiration: { t_s: 1 }, -        tip_status_url: "url", -      }, -    }); - -    act(async () => { -      await result.current?.api.authorizeTipReserve("11", { -        amount: "USD:12", -        justification: "not", -        next_url: "http://taler.net", -      }); -    }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); - -    env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { -      response: { -        payto_uri: "payto://here", -        tips: [ -          { reason: "why?", tip_id: "id1", total_amount: "USD:10" }, -          { reason: "not", tip_id: "id2", total_amount: "USD:12" }, -        ], -      } as MerchantBackend.Tips.ReserveDetail, -    }); - -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current.query.ok).toBeTruthy(); - -    expect(result.current.query.data).toEqual({ -      payto_uri: "payto://here", -      tips: [ -        { reason: "why?", tip_id: "id1", total_amount: "USD:10" }, -        { reason: "not", tip_id: "id2", total_amount: "USD:12" }, -      ], -    }); -  }); - -  it("should evict cache when adding a tip for a random reserve", async () => { -    const env = new AxiosMockEnvironment(); - -    env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { -      response: { -        payto_uri: "payto://here", -        tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }], -      } as MerchantBackend.Tips.ReserveDetail, -    }); - -    const { result, waitForNextUpdate } = renderHook( -      () => { -        const api = useReservesAPI(); -        const query = useReserveDetails("11"); - -        return { query, api }; -      }, -      { -        wrapper: TestingContext, -      } -    ); - -    expect(result.current).toBeDefined(); -    if (!result.current) { -      return; -    } -    expect(result.current.query.loading).toBeTruthy(); - -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      payto_uri: "payto://here", -      tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }], -    }); - -    env.addRequestExpectation(API_AUTHORIZE_TIP, { -      request: { -        amount: "USD:12", -        justification: "not", -        next_url: "http://taler.net", -      }, -      response: { -        tip_id: "id2", -        taler_tip_uri: "uri", -        tip_expiration: { t_s: 1 }, -        tip_status_url: "url", -      }, -    }); - -    act(async () => { -      await result.current?.api.authorizeTip({ -        amount: "USD:12", -        justification: "not", -        next_url: "http://taler.net", -      }); -    }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); - -    env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { -      response: { -        payto_uri: "payto://here", -        tips: [ -          { reason: "why?", tip_id: "id1", total_amount: "USD:10" }, -          { reason: "not", tip_id: "id2", total_amount: "USD:12" }, -        ], -      } as MerchantBackend.Tips.ReserveDetail, -    }); - -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current.query.ok).toBeTruthy(); - -    expect(result.current.query.data).toEqual({ -      payto_uri: "payto://here", -      tips: [ -        { reason: "why?", tip_id: "id1", total_amount: "USD:10" }, -        { reason: "not", tip_id: "id2", total_amount: "USD:12" }, -      ], -    }); -  }); -}); - -describe("reserve api interaction with tip details", () => { -  it("should list tips", async () => { -    const env = new AxiosMockEnvironment(); - -    env.addRequestExpectation(API_GET_TIP_BY_ID("11"), { -      response: { -        total_picked_up: "USD:12", -        reason: "not", -      } as MerchantBackend.Tips.TipDetails, -    }); - -    const { result, waitForNextUpdate } = renderHook( -      () => { -        // const api = useReservesAPI(); -        const query = useTipDetails("11"); - -        return { query }; -      }, -      { -        wrapper: TestingContext, -      } -    ); - -    expect(result.current).toBeDefined(); -    if (!result.current) { -      return; -    } -    expect(result.current.query.loading).toBeTruthy(); - -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      total_picked_up: "USD:12", -      reason: "not", -    }); -  }); -}); diff --git a/packages/merchant-backoffice-ui/tests/hooks/swr/transfer.test.ts b/packages/merchant-backoffice-ui/tests/hooks/swr/transfer.test.ts deleted file mode 100644 index 0b1f4a968..000000000 --- a/packages/merchant-backoffice-ui/tests/hooks/swr/transfer.test.ts +++ /dev/null @@ -1,268 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021-2023 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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { act, renderHook } from "@testing-library/preact-hooks"; -import { TestingContext } from "./index.js"; -import { useInstanceTransfers, useTransferAPI } from "../../../src/hooks/transfer.js"; -import { -  API_INFORM_TRANSFERS, -  API_LIST_TRANSFERS, -  assertJustExpectedRequestWereMade, -  assertNoMoreRequestWereMade, -  AxiosMockEnvironment, -} from "../../axiosMock.js"; -import { MerchantBackend } from "../../../src/declaration.js"; - -describe("transfer api interaction with listing", () => { - -  it("should evict cache when informing a transfer", async () => { -    const env = new AxiosMockEnvironment(); - -    env.addRequestExpectation(API_LIST_TRANSFERS, { -      qparam: { limit: 0 }, -      response: { -        transfers: [{ wtid: "2" } as MerchantBackend.Transfers.TransferDetails], -      }, -    }); -    // FIXME: is this query really needed? if the hook is rendered without -    // position argument then then backend is returning the newest and no need -    // to this second query  -    env.addRequestExpectation(API_LIST_TRANSFERS, { -      qparam: { limit: -20 }, -      response: { -        transfers: [], -      }, -    }); - -    const { result, waitForNextUpdate } = renderHook(() => { -      const moveCursor = (d: string) => { -        console.log("new position", d); -      }; -      const query = useInstanceTransfers({}, moveCursor); -      const api = useTransferAPI(); - -      return { query, api }; -    }, { wrapper: TestingContext }); - -    expect(result.current).toBeDefined(); -    if (!result.current) { -      return; -    } - -    expect(result.current.query.loading).toBeTruthy(); -    await waitForNextUpdate({ timeout: 1 }); -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current.query.ok).toBeTruthy(); -    if (!result.current.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      transfers: [{ wtid: "2" }], -    }); - -    env.addRequestExpectation(API_INFORM_TRANSFERS, { -      request: { -        wtid: '3', -        credit_amount: 'EUR:1', -        exchange_url: 'exchange.url', -        payto_uri: 'payto://' -      }, -      response: { total: '' } as any, -    }); - -    env.addRequestExpectation(API_LIST_TRANSFERS, { -      qparam: { limit: 0 }, -      response: { -        transfers: [{ wtid: "2" } as any, { wtid: "3" } as any], -      }, -    }); - -    env.addRequestExpectation(API_LIST_TRANSFERS, { -      qparam: { limit: -20 }, -      response: { -        transfers: [], -      }, -    }); - -    act(async () => { -      await result.current?.api.informTransfer({ -        wtid: '3', -        credit_amount: 'EUR:1', -        exchange_url: 'exchange.url', -        payto_uri: 'payto://' -      }); -    }); - -    await waitForNextUpdate({ timeout: 1 }); -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.loading).toBeFalsy(); -    expect(result.current.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      transfers: [{ wtid: "3" }, { wtid: "2" }], -    }); -  }); - -}); - -describe("transfer listing pagination", () => { - -  it("should not load more if has reach the end", async () => { -    const env = new AxiosMockEnvironment(); -    env.addRequestExpectation(API_LIST_TRANSFERS, { -      qparam: { limit: 0, payto_uri: 'payto://' }, -      response: { -        transfers: [{ wtid: "2" } as any], -      }, -    }); - -    env.addRequestExpectation(API_LIST_TRANSFERS, { -      qparam: { limit: -20, payto_uri: 'payto://' }, -      response: { -        transfers: [{ wtid: "1" } as any], -      }, -    }); - - -    const { result, waitForNextUpdate } = renderHook(() => { -      const moveCursor = (d: string) => { -        console.log("new position", d); -      }; -      const query = useInstanceTransfers({ payto_uri: 'payto://' }, moveCursor) -      return { query } -    }, { wrapper: TestingContext }); - -    assertJustExpectedRequestWereMade(env); - -    await waitForNextUpdate(); - -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      transfers: [{ wtid: "2" }, { wtid: "1" }], -    }); - -    expect(result.current.query.isReachingEnd).toBeTruthy() -    expect(result.current.query.isReachingStart).toBeTruthy() - -    await act(() => { -      if (!result.current?.query.ok) throw Error("not ok"); -      result.current.query.loadMore(); -    }); -    assertNoMoreRequestWereMade(env); - -    await act(() => { -      if (!result.current?.query.ok) throw Error("not ok"); -      result.current.query.loadMorePrev(); -    }); -    assertNoMoreRequestWereMade(env); - -    expect(result.current.query.data).toEqual({ -      transfers: [ -        { wtid: "2" }, -        { wtid: "1" }, -      ], -    }); -  }); - -  it("should load more if result brings more that PAGE_SIZE", async () => { -    const env = new AxiosMockEnvironment(); - -    const transfersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({ wtid: String(i) })) -    const transfersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({ wtid: String(i + 20) })) -    const transfersFrom20to0 = [...transfersFrom0to20].reverse() - -    env.addRequestExpectation(API_LIST_TRANSFERS, { -      qparam: { limit: 20, payto_uri: 'payto://' }, -      response: { -        transfers: transfersFrom0to20, -      }, -    }); - -    env.addRequestExpectation(API_LIST_TRANSFERS, { -      qparam: { limit: -20, payto_uri: 'payto://' }, -      response: { -        transfers: transfersFrom20to40, -      }, -    }); - -    const { result, waitForNextUpdate } = renderHook(() => { -      const moveCursor = (d: string) => { -        console.log("new position", d); -      }; -      const query = useInstanceTransfers({ payto_uri: 'payto://', position: '1' }, moveCursor) -      return { query } -    }, { wrapper: TestingContext }); - -    assertJustExpectedRequestWereMade(env); - -    await waitForNextUpdate({ timeout: 1 }); - -    expect(result.current?.query.ok).toBeTruthy(); -    if (!result.current?.query.ok) return; - -    expect(result.current.query.data).toEqual({ -      transfers: [...transfersFrom20to0, ...transfersFrom20to40], -    }); - -    expect(result.current.query.isReachingEnd).toBeFalsy() -    expect(result.current.query.isReachingStart).toBeFalsy() - -    env.addRequestExpectation(API_LIST_TRANSFERS, { -      qparam: { limit: -40, payto_uri: 'payto://', offset: "1" }, -      response: { -        transfers: [...transfersFrom20to40, { wtid: '41' }], -      }, -    }); - -    await act(() => { -      if (!result.current?.query.ok) throw Error("not ok"); -      result.current.query.loadMore(); -    }); -    await waitForNextUpdate({ timeout: 1 }); - -    assertJustExpectedRequestWereMade(env); - -    env.addRequestExpectation(API_LIST_TRANSFERS, { -      qparam: { limit: 40, payto_uri: 'payto://', offset: "1" }, -      response: { -        transfers: [...transfersFrom0to20, { wtid: '-1' }], -      }, -    }); - -    await act(() => { -      if (!result.current?.query.ok) throw Error("not ok"); -      result.current.query.loadMorePrev(); -    }); -    await waitForNextUpdate({ timeout: 1 }); -    assertJustExpectedRequestWereMade(env); - -    expect(result.current.query.data).toEqual({ -      transfers: [{ wtid: '-1' }, ...transfersFrom20to0, ...transfersFrom20to40, { wtid: '41' }], -    }); -  }); - - -});  | 
