merchant-backoffice

ZZZ: Inactive/Deprecated
Log | Files | Refs | Submodules | README

commit 5365512386a26e4f48ac50d3e99ae855ce0c75fa
parent e8bbe2b18187aacb1873d7a336140b1645ec6091
Author: Sebastian <sebasjm@gmail.com>
Date:   Thu, 16 Dec 2021 08:56:03 -0300

more unit test and kyc interfaces

Diffstat:
Mpackages/merchant-backoffice/src/declaration.d.ts | 40++++++++++++++++++++++++++++++++++++++++
Dpackages/merchant-backoffice/src/hooks/admin.ts | 63---------------------------------------------------------------
Mpackages/merchant-backoffice/src/hooks/instance.ts | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mpackages/merchant-backoffice/src/paths/admin/create/index.tsx | 2+-
Mpackages/merchant-backoffice/src/paths/admin/list/index.tsx | 175+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mpackages/merchant-backoffice/tests/axiosMock.ts | 102+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/merchant-backoffice/tests/hooks/async.test.ts | 158+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apackages/merchant-backoffice/tests/hooks/swr/instance.test.ts | 636+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8 files changed, 1093 insertions(+), 170 deletions(-)

diff --git a/packages/merchant-backoffice/src/declaration.d.ts b/packages/merchant-backoffice/src/declaration.d.ts @@ -388,6 +388,45 @@ export namespace MerchantBackend { } + //GET /private/instances/$INSTANCE/kyc + interface AccountKycRedirects { + // Array of pending KYCs. + pending_kycs: MerchantAccountKycRedirect[]; + + // Array of exchanges with no reply. + timeout_kycs: ExchangeKycTimeout[]; + + } + interface MerchantAccountKycRedirect { + + // URL that the user should open in a browser to + // proceed with the KYC process (as returned + // by the exchange's /kyc-check/ endpoint). + kyc_url: string; + + // Base URL of the exchange this is about. + exchange_url: string; + + // Our bank wire account this is about. + payto_uri: string; + + } + interface ExchangeKycTimeout { + + // Base URL of the exchange this is about. + exchange_url: string; + + // Numeric error code indicating errors the exchange + // returned, or TALER_EC_INVALID for none. + exchange_code: number; + + // HTTP status code returned by the exchange when we asked for + // information about the KYC status. + // 0 if there was no response at all. + exchange_http_status: number; + + } + //GET /private/instances/$INSTANCE interface QueryInstancesResponse { // The URI where the wallet will send coins. A merchant may have @@ -433,6 +472,7 @@ export namespace MerchantBackend { // Does not contain the token when token auth is configured. auth: { method: "external" | "token"; + token?: string; }; } diff --git a/packages/merchant-backoffice/src/hooks/admin.ts b/packages/merchant-backoffice/src/hooks/admin.ts @@ -1,63 +0,0 @@ -/* - This file is part of GNU Taler - (C) 2021 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 { MerchantBackend } from "../declaration"; -import { useBackendContext } from "../context/backend"; -import { request, useMatchMutate } from "./backend"; - -export function useAdminAPI(): AdminAPI { - const { url, token } = useBackendContext(); - const mutateAll = useMatchMutate(); - - const createInstance = async ( - instance: MerchantBackend.Instances.InstanceConfigurationMessage - ): Promise<void> => { - await request(`${url}/management/instances`, { - method: "post", - token, - data: instance, - }); - - mutateAll(/@"\/private\/instances"@/); - }; - - const deleteInstance = async (id: string): Promise<void> => { - await request(`${url}/management/instances/${id}`, { - method: "delete", - token, - }); - - mutateAll(/@"\/private\/instances"@/); - }; - - const purgeInstance = async (id: string): Promise<void> => { - await request(`${url}/management/instances/${id}?purge=YES`, { - method: "delete", - token, - }); - - mutateAll(/@"\/private\/instances"@/); - }; - - return { createInstance, deleteInstance, purgeInstance }; -} - -export interface AdminAPI { - createInstance: ( - data: MerchantBackend.Instances.InstanceConfigurationMessage - ) => Promise<void>; - deleteInstance: (id: string) => Promise<void>; - purgeInstance: (id: string) => Promise<void>; -} diff --git a/packages/merchant-backoffice/src/hooks/instance.ts b/packages/merchant-backoffice/src/hooks/instance.ts @@ -13,18 +13,17 @@ 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 { MerchantBackend } from "../declaration"; +import useSWR, { useSWRConfig } from "swr"; import { useBackendContext } from "../context/backend"; +import { useInstanceContext } from "../context/instance"; +import { MerchantBackend } from "../declaration"; import { fetcher, HttpError, HttpResponse, HttpResponseOk, - request, - SwrError, + request, useMatchMutate } from "./backend"; -import useSWR, { useSWRConfig } from "swr"; -import { useInstanceContext } from "../context/instance"; interface InstanceAPI { updateInstance: ( @@ -35,8 +34,56 @@ interface InstanceAPI { setNewToken: (token: string) => Promise<void>; } +export function useAdminAPI(): AdminAPI { + const { url, token } = useBackendContext(); + const mutateAll = useMatchMutate(); + + const createInstance = async ( + instance: MerchantBackend.Instances.InstanceConfigurationMessage + ): Promise<void> => { + await request(`${url}/management/instances`, { + method: "post", + token, + data: instance, + }); + + mutateAll(/\/management\/instances/); + }; + + const deleteInstance = async (id: string): Promise<void> => { + await request(`${url}/management/instances/${id}`, { + method: "delete", + token, + }); + + mutateAll(/\/management\/instances/); + }; + + const purgeInstance = async (id: string): Promise<void> => { + await request(`${url}/management/instances/${id}`, { + method: "delete", + token, + params: { + purge: 'YES' + } + }); + + mutateAll(/\/management\/instances/); + }; + + return { createInstance, deleteInstance, purgeInstance }; +} + +export interface AdminAPI { + createInstance: ( + data: MerchantBackend.Instances.InstanceConfigurationMessage + ) => Promise<void>; + deleteInstance: (id: string) => Promise<void>; + purgeInstance: (id: string) => Promise<void>; +} + export function useManagementAPI(instanceId: string): InstanceAPI { - const { mutate } = useSWRConfig(); + const mutateAll = useMatchMutate(); const { url, token } = useBackendContext(); const updateInstance = async ( @@ -48,7 +95,7 @@ export function useManagementAPI(instanceId: string): InstanceAPI { data: instance, }); - mutate([`/private/`, token, url], null); + mutateAll(/\/management\/instances/); }; const deleteInstance = async (): Promise<void> => { @@ -57,7 +104,7 @@ export function useManagementAPI(instanceId: string): InstanceAPI { token, }); - mutate([`/private/`, token, url], null); + mutateAll(/\/management\/instances/); }; const clearToken = async (): Promise<void> => { @@ -67,7 +114,7 @@ export function useManagementAPI(instanceId: string): InstanceAPI { data: { method: "external" }, }); - mutate([`/private/`, token, url], null); + mutateAll(/\/management\/instances/); }; const setNewToken = async (newToken: string): Promise<void> => { @@ -77,7 +124,7 @@ export function useManagementAPI(instanceId: string): InstanceAPI { data: { method: "token", token: newToken }, }); - mutate([`/private/`, token, url], null); + mutateAll(/\/management\/instances/); }; return { updateInstance, deleteInstance, setNewToken, clearToken }; @@ -89,14 +136,8 @@ export function useInstanceAPI(): InstanceAPI { const { token: instanceToken, id, admin } = useInstanceContext(); const { url, token } = !admin - ? { - url: baseUrl, - token: adminToken, - } - : { - url: `${baseUrl}/instances/${id}`, - token: instanceToken, - }; + ? { url: baseUrl, token: adminToken, } + : { url: `${baseUrl}/instances/${id}`, token: instanceToken, }; const updateInstance = async ( instance: MerchantBackend.Instances.InstanceReconfigurationMessage @@ -149,14 +190,8 @@ export function useInstanceDetails(): HttpResponse<MerchantBackend.Instances.Que const { token: instanceToken, id, admin } = useInstanceContext(); const { url, token } = !admin - ? { - url: baseUrl, - token: baseToken, - } - : { - url: `${baseUrl}/instances/${id}`, - token: instanceToken, - }; + ? { url: baseUrl, token: baseToken, } + : { url: `${baseUrl}/instances/${id}`, token: instanceToken, }; const { data, error, isValidating } = useSWR< HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>, diff --git a/packages/merchant-backoffice/src/paths/admin/create/index.tsx b/packages/merchant-backoffice/src/paths/admin/create/index.tsx @@ -21,7 +21,7 @@ import { Fragment, h, VNode } from "preact"; import { useState } from "preact/hooks"; import { NotificationCard } from "../../../components/menu"; import { MerchantBackend } from "../../../declaration"; -import { useAdminAPI } from "../../../hooks/admin"; +import { useAdminAPI } from "../../../hooks/instance"; import { useTranslator } from "../../../i18n"; import { Notification } from "../../../utils/types"; import { CreatePage } from "./CreatePage"; diff --git a/packages/merchant-backoffice/src/paths/admin/list/index.tsx b/packages/merchant-backoffice/src/paths/admin/list/index.tsx @@ -15,22 +15,21 @@ */ /** -* -* @author Sebastian Javier Marchano (sebasjm) -*/ + * + * @author Sebastian Javier Marchano (sebasjm) + */ -import { Fragment, h, VNode } from 'preact'; -import { useState } from 'preact/hooks'; -import { Loading } from '../../../components/exception/loading'; -import { NotificationCard } from '../../../components/menu'; -import { DeleteModal, PurgeModal } from '../../../components/modal'; -import { MerchantBackend } from '../../../declaration'; -import { useAdminAPI } from "../../../hooks/admin"; -import { HttpError } from '../../../hooks/backend'; -import { useBackendInstances } from '../../../hooks/instance'; -import { useTranslator } from '../../../i18n'; -import { Notification } from '../../../utils/types'; -import { View } from './View'; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { Loading } from "../../../components/exception/loading"; +import { NotificationCard } from "../../../components/menu"; +import { DeleteModal, PurgeModal } from "../../../components/modal"; +import { MerchantBackend } from "../../../declaration"; +import { HttpError } from "../../../hooks/backend"; +import { useAdminAPI, useBackendInstances } from "../../../hooks/instance"; +import { useTranslator } from "../../../i18n"; +import { Notification } from "../../../utils/types"; +import { View } from "./View"; interface Props { onCreate: () => void; @@ -39,73 +38,89 @@ interface Props { onUnauthorized: () => VNode; onNotFound: () => VNode; onLoadError: (error: HttpError) => VNode; - setInstanceName: (s:string) => void; + setInstanceName: (s: string) => void; } -export default function Instances({ onUnauthorized, onLoadError, onNotFound, onCreate, onUpdate, setInstanceName }: Props): VNode { - const result = useBackendInstances() - const [deleting, setDeleting] = useState<MerchantBackend.Instances.Instance | null>(null) - const [purging, setPurging] = useState<MerchantBackend.Instances.Instance | null>(null) - const { deleteInstance, purgeInstance } = useAdminAPI() - const [notif, setNotif] = useState<Notification | undefined>(undefined) - const i18n = useTranslator() +export default function Instances({ + onUnauthorized, + onLoadError, + onNotFound, + onCreate, + onUpdate, + setInstanceName, +}: Props): VNode { + const result = useBackendInstances(); + const [deleting, setDeleting] = + useState<MerchantBackend.Instances.Instance | null>(null); + const [purging, setPurging] = + useState<MerchantBackend.Instances.Instance | null>(null); + const { deleteInstance, purgeInstance } = useAdminAPI(); + const [notif, setNotif] = useState<Notification | undefined>(undefined); + const i18n = useTranslator(); - if (result.clientError && result.isUnauthorized) return onUnauthorized() - if (result.clientError && result.isNotfound) return onNotFound() - if (result.loading) return <Loading /> - if (!result.ok) return onLoadError(result) + if (result.clientError && result.isUnauthorized) return onUnauthorized(); + if (result.clientError && result.isNotfound) return onNotFound(); + if (result.loading) return <Loading />; + if (!result.ok) return onLoadError(result); - return <Fragment> - <NotificationCard notification={notif} /> - <View instances={result.data.instances} - onDelete={setDeleting} - onCreate={onCreate} - onPurge={setPurging} - onUpdate={onUpdate} - setInstanceName={setInstanceName} - selected={!!deleting} - /> - {deleting && <DeleteModal - element={deleting} - onCancel={() => setDeleting(null)} - onConfirm={async (): Promise<void> => { - try { - await deleteInstance(deleting.id) - // pushNotification({ message: 'delete_success', type: 'SUCCESS' }) - setNotif({ - message: i18n`Instance "${deleting.name}" (ID: ${deleting.id}) has been deleted`, - type: 'SUCCESS' - }) - } catch (error) { - setNotif({ - message: i18n`Failed to delete instance`, - type: "ERROR", - description: error instanceof Error ? error.message : undefined - }) - // pushNotification({ message: 'delete_error', type: 'ERROR' }) - } - setDeleting(null) - }} - />} - {purging && <PurgeModal - element={purging} - onCancel={() => setPurging(null)} - onConfirm={async (): Promise<void> => { - try { - await purgeInstance(purging.id) - setNotif({ - message: i18n`Instance "${purging.name}" (ID: ${purging.id}) has been disabled`, - type: 'SUCCESS' - }) - } catch (error) { - setNotif({ - message: i18n`Failed to purge instance`, - type: "ERROR", - description: error instanceof Error ? error.message : undefined - }) - } - setPurging(null) - }} - />} - </Fragment>; + return ( + <Fragment> + <NotificationCard notification={notif} /> + <View + instances={result.data.instances} + onDelete={setDeleting} + onCreate={onCreate} + onPurge={setPurging} + onUpdate={onUpdate} + setInstanceName={setInstanceName} + selected={!!deleting} + /> + {deleting && ( + <DeleteModal + element={deleting} + onCancel={() => setDeleting(null)} + onConfirm={async (): Promise<void> => { + try { + await deleteInstance(deleting.id); + // pushNotification({ message: 'delete_success', type: 'SUCCESS' }) + setNotif({ + message: i18n`Instance "${deleting.name}" (ID: ${deleting.id}) has been deleted`, + type: "SUCCESS", + }); + } catch (error) { + setNotif({ + message: i18n`Failed to delete instance`, + type: "ERROR", + description: error instanceof Error ? error.message : undefined, + }); + // pushNotification({ message: 'delete_error', type: 'ERROR' }) + } + setDeleting(null); + }} + /> + )} + {purging && ( + <PurgeModal + element={purging} + onCancel={() => setPurging(null)} + onConfirm={async (): Promise<void> => { + try { + await purgeInstance(purging.id); + setNotif({ + message: i18n`Instance "${purging.name}" (ID: ${purging.id}) has been disabled`, + type: "SUCCESS", + }); + } catch (error) { + setNotif({ + message: i18n`Failed to purge instance`, + type: "ERROR", + description: error instanceof Error ? error.message : undefined, + }); + } + setPurging(null); + }} + /> + )} + </Fragment> + ); } diff --git a/packages/merchant-backoffice/tests/axiosMock.ts b/packages/merchant-backoffice/tests/axiosMock.ts @@ -330,3 +330,105 @@ export const API_DELETE_RESERVE = ( 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/tests/hooks/async.test.ts b/packages/merchant-backoffice/tests/hooks/async.test.ts @@ -0,0 +1,158 @@ +/* + This file is part of GNU Taler + (C) 2021 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" + +/** +* +* @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/tests/hooks/swr/instance.test.ts b/packages/merchant-backoffice/tests/hooks/swr/instance.test.ts @@ -0,0 +1,636 @@ +/* + This file is part of GNU Taler + (C) 2021 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"; +import { useAdminAPI, useBackendInstances, useInstanceAPI, useInstanceDetails, useManagementAPI } from "../../../src/hooks/instance"; +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"; +import { TestingContext } from "./index"; + +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 } + ); + + if (!result.current) { + expect(result.current).toBeDefined(); + 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 } + ); + + if (!result.current) { + expect(result.current).toBeDefined(); + 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 } + ); + + if (!result.current) { + expect(result.current).toBeDefined(); + 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 } + ); + + if (!result.current) { + expect(result.current).toBeDefined(); + 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 } + ); + + if (!result.current) { + expect(result.current).toBeDefined(); + 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 } + ); + + if (!result.current) { + expect(result.current).toBeDefined(); + 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 } + ); + + if (!result.current) { + expect(result.current).toBeDefined(); + 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' + }] + }); + }); + +}); +