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' }], - }); - }); - - -}); |