merchant-backoffice

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

commit 58d06f9b5eaa41c083c7702d772f174a2048c9c2
parent 1d84a5dceba30dc93e60208e4715bce9af8ac02b
Author: Sebastian <sebasjm@gmail.com>
Date:   Tue, 14 Dec 2021 16:11:53 -0300

test case for orders, products, reserves and transfers

Diffstat:
Mpackages/merchant-backoffice/src/context/backend.ts | 14+++++++-------
Mpackages/merchant-backoffice/src/hooks/backend.ts | 14+++-----------
Mpackages/merchant-backoffice/src/hooks/index.ts | 4++--
Mpackages/merchant-backoffice/src/hooks/order.ts | 32++++++++++++++++++++------------
Mpackages/merchant-backoffice/src/hooks/product.ts | 4++--
Mpackages/merchant-backoffice/src/hooks/reserves.ts | 18++++++++++++------
Mpackages/merchant-backoffice/src/hooks/transfer.ts | 36++++++++++++++++++------------------
Apackages/merchant-backoffice/src/utils/switchableAxios.ts | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/merchant-backoffice/tests/axiosMock.ts | 182++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
Apackages/merchant-backoffice/tests/hooks/swr/index.tsx | 45+++++++++++++++++++++++++++++++++++++++++++++
Dpackages/merchant-backoffice/tests/hooks/swr/order-create.test.tsx | 139-------------------------------------------------------------------------------
Dpackages/merchant-backoffice/tests/hooks/swr/order-pagination.test.tsx | 126-------------------------------------------------------------------------------
Apackages/merchant-backoffice/tests/hooks/swr/order.test.ts | 567+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dpackages/merchant-backoffice/tests/hooks/swr/product-create.test.tsx | 142-------------------------------------------------------------------------------
Dpackages/merchant-backoffice/tests/hooks/swr/product-delete.test.tsx | 120-------------------------------------------------------------------------------
Dpackages/merchant-backoffice/tests/hooks/swr/product-details-update.test.tsx | 113-------------------------------------------------------------------------------
Dpackages/merchant-backoffice/tests/hooks/swr/product-update.test.tsx | 127-------------------------------------------------------------------------------
Apackages/merchant-backoffice/tests/hooks/swr/product.test.ts | 339+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/merchant-backoffice/tests/hooks/swr/reserve.test.ts | 378+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------
Dpackages/merchant-backoffice/tests/hooks/swr/transfer-pagination.test.tsx | 120-------------------------------------------------------------------------------
Apackages/merchant-backoffice/tests/hooks/swr/transfer.test.ts | 268+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
21 files changed, 1832 insertions(+), 1022 deletions(-)

diff --git a/packages/merchant-backoffice/src/context/backend.ts b/packages/merchant-backoffice/src/context/backend.ts @@ -43,10 +43,10 @@ const BackendContext = createContext<BackendContextType>({ updateLoginStatus: () => null, }) -function useBackendContextState(defaultUrl?:string): BackendContextType { +function useBackendContextState(defaultUrl?: string, initialToken?: string): BackendContextType { const [url, triedToLog, changeBackend, resetBackend] = useBackendURL(defaultUrl); - const [token, _updateToken] = useBackendDefaultToken(); - const updateToken = (t?:string) => { + const [token, _updateToken] = useBackendDefaultToken(initialToken); + const updateToken = (t?: string) => { _updateToken(t) } @@ -73,10 +73,10 @@ function useBackendContextState(defaultUrl?:string): BackendContextType { return { url, token, triedToLog, updateLoginStatus, resetBackend, clearAllTokens, addTokenCleaner: addTokenCleanerMemo } } -export const BackendContextProvider = ({children, defaultUrl}:{children:any, defaultUrl?:string}):VNode => { - const value = useBackendContextState(defaultUrl) - - return h(BackendContext.Provider, {value, children}); +export const BackendContextProvider = ({ children, defaultUrl, initialToken }: { children: any, defaultUrl?: string, initialToken?: string }): VNode => { + const value = useBackendContextState(defaultUrl, initialToken) + + return h(BackendContext.Provider, { value, children }); } export const useBackendContext = (): BackendContextType => useContext(BackendContext); diff --git a/packages/merchant-backoffice/src/hooks/backend.ts b/packages/merchant-backoffice/src/hooks/backend.ts @@ -25,6 +25,7 @@ import { MerchantBackend } from "../declaration"; import { useBackendContext } from "../context/backend"; import { useEffect, useState } from "preact/hooks"; import { DEFAULT_REQUEST_TIMEOUT } from "../utils/constants"; +import { axiosHandler, removeAxiosCancelToken } from "../utils/switchableAxios"; export function useMatchMutate(): ( re: RegExp, @@ -226,20 +227,11 @@ function buildRequestFailed( const CancelToken = axios.CancelToken; let source = CancelToken.source(); -export function cancelPendingRequest() { +export function cancelPendingRequest(): void { source.cancel("canceled by the user"); source = CancelToken.source(); } -let removeAxiosCancelToken = false; -/** - * Jest mocking seems to break when using the cancelToken property. - * Using this workaround when testing while finding the correct solution - */ -export function setAxiosRequestAsTestingEnvironment() { - removeAxiosCancelToken = true; -} - export function isAxiosError<T>( error: AxiosError | any ): error is AxiosError<T> { @@ -255,7 +247,7 @@ export async function request<T>( : undefined; try { - const res = await axios({ + const res = await axiosHandler({ url, responseType: "json", headers, diff --git a/packages/merchant-backoffice/src/hooks/index.ts b/packages/merchant-backoffice/src/hooks/index.ts @@ -43,8 +43,8 @@ export function useBackendURL(url?: string): [string, boolean, StateUpdater<stri return [value, !!triedToLog, checkedSetter, resetBackend] } -export function useBackendDefaultToken(): [string | undefined, StateUpdater<string | undefined>] { - return useLocalStorage('backend-token') +export function useBackendDefaultToken(initialValue?: string): [string | undefined, StateUpdater<string | undefined>] { + return useLocalStorage('backend-token', initialValue) } export function useBackendInstanceToken(id: string): [string | undefined, StateUpdater<string | undefined>] { diff --git a/packages/merchant-backoffice/src/hooks/order.ts b/packages/merchant-backoffice/src/hooks/order.ts @@ -106,7 +106,7 @@ export function useOrderAPI(): OrderAPI { data: MerchantBackend.Orders.RefundRequest ): Promise<HttpResponseOk<MerchantBackend.Orders.MerchantRefundResponse>> => { mutateAll(/@"\/private\/orders"@/); - return request<MerchantBackend.Orders.MerchantRefundResponse>( + const res = request<MerchantBackend.Orders.MerchantRefundResponse>( `${url}/private/orders/${orderId}/refund`, { method: "post", @@ -115,7 +115,9 @@ export function useOrderAPI(): OrderAPI { } ); - // return res + // order list returns refundable information, so we must evict everything + await mutateAll(/.*private\/orders.*/); + return res }; const forgetOrder = async ( @@ -123,20 +125,25 @@ export function useOrderAPI(): OrderAPI { data: MerchantBackend.Orders.ForgetRequest ): Promise<HttpResponseOk<void>> => { mutateAll(/@"\/private\/orders"@/); - return request(`${url}/private/orders/${orderId}/forget`, { + const res = request<void>(`${url}/private/orders/${orderId}/forget`, { method: "patch", token, data, }); + // we may be forgetting some fields that are pare of the listing, so we must evict everything + await mutateAll(/.*private\/orders.*/); + return res }; const deleteOrder = async ( orderId: string ): Promise<HttpResponseOk<void>> => { mutateAll(/@"\/private\/orders"@/); - return request(`${url}/private/orders/${orderId}`, { + const res = request<void>(`${url}/private/orders/${orderId}`, { method: "delete", token, }); + await mutateAll(/.*private\/orders.*/); + return res }; const getPaymentURL = async ( @@ -266,18 +273,19 @@ export function useInstanceOrders( if (beforeData) setLastBefore(beforeData); }, [afterData, beforeData]); - // this has problems when there are some ids missing - if (beforeError) return beforeError; if (afterError) return afterError; + // if the query returns less that we ask, then we have reach the end or beginning + const isReachingEnd = afterData && afterData.data.orders.length < totalAfter; + const isReachingStart = args?.date === undefined || + (beforeData && beforeData.data.orders.length < totalBefore); + const pagination = { - isReachingEnd: afterData && afterData.data.orders.length < totalAfter, - isReachingStart: - !args?.date || - (beforeData && beforeData.data.orders.length < totalBefore), + isReachingEnd, + isReachingStart, loadMore: () => { - if (!afterData) return; + if (!afterData || isReachingEnd) return; if (afterData.data.orders.length < MAX_RESULT_SIZE) { setPageAfter(pageAfter + 1); } else { @@ -288,7 +296,7 @@ export function useInstanceOrders( } }, loadMorePrev: () => { - if (!beforeData) return; + if (!beforeData || isReachingStart) return; if (beforeData.data.orders.length < MAX_RESULT_SIZE) { setPageBefore(pageBefore + 1); } else if (beforeData) { diff --git a/packages/merchant-backoffice/src/hooks/product.ts b/packages/merchant-backoffice/src/hooks/product.ts @@ -61,7 +61,7 @@ export function useProductAPI(): ProductAPI { data, }); - return await mutateAll(/.*private\/products.*/); + return await mutateAll(/.*"\/private\/products.*/); }; const updateProduct = async ( @@ -95,7 +95,7 @@ export function useProductAPI(): ProductAPI { data, }); - await mutateAll(/@"\/private\/products"@/); + return await mutateAll(/.*"\/private\/products.*/); }; return { createProduct, updateProduct, deleteProduct, lockProduct }; diff --git a/packages/merchant-backoffice/src/hooks/reserves.ts b/packages/merchant-backoffice/src/hooks/reserves.ts @@ -13,7 +13,7 @@ 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 useSWR from "swr"; +import useSWR, { useSWRConfig } from "swr"; import { useBackendContext } from "../context/backend"; import { useInstanceContext } from "../context/instance"; import { MerchantBackend } from "../declaration"; @@ -28,6 +28,7 @@ import { export function useReservesAPI(): ReserveMutateAPI { const mutateAll = useMatchMutate(); + const { mutate } = useSWRConfig(); const { url: baseUrl, token: adminToken } = useBackendContext(); const { token: instanceToken, id, admin } = useInstanceContext(); @@ -49,6 +50,7 @@ export function useReservesAPI(): ReserveMutateAPI { } ); + //evict reserve list query await mutateAll(/.*private\/reserves.*/); return res; @@ -66,7 +68,9 @@ export function useReservesAPI(): ReserveMutateAPI { data, } ); - await mutateAll(/@"\/private\/reserves"@/); + + //evict reserve details query + await mutate([`/private/reserves/${pub}`, token, url]); return res; }; @@ -83,7 +87,8 @@ export function useReservesAPI(): ReserveMutateAPI { } ); - await mutateAll(/@"\/private\/reserves"@/); + //evict all details query + await mutateAll(/.*private\/reserves\/.*/); return res; }; @@ -94,7 +99,8 @@ export function useReservesAPI(): ReserveMutateAPI { token, }); - await mutateAll(/@"\/private\/reserves"@/); + //evict reserve list query + await mutateAll(/.*private\/reserves.*/); return res; }; @@ -185,7 +191,7 @@ export function useTipDetails( return { loading: true }; } -export function reserveDetailFetcher<T>( +function reserveDetailFetcher<T>( url: string, token: string, backend: string @@ -198,7 +204,7 @@ export function reserveDetailFetcher<T>( }); } -export function tipsDetailFetcher<T>( +function tipsDetailFetcher<T>( url: string, token: string, backend: string diff --git a/packages/merchant-backoffice/src/hooks/transfer.ts b/packages/merchant-backoffice/src/hooks/transfer.ts @@ -68,16 +68,15 @@ export function useTransferAPI(): TransferAPI { ): Promise< HttpResponseOk<MerchantBackend.Transfers.MerchantTrackTransferResponse> > => { - mutateAll(/@"\/private\/transfers"@/); - - return request<MerchantBackend.Transfers.MerchantTrackTransferResponse>( - `${url}/private/transfers`, - { - method: "post", - token, - data, - } - ); + const res = await request<MerchantBackend.Transfers.MerchantTrackTransferResponse>( + `${url}/private/transfers`, { + method: "post", + token, + data, + }); + + await mutateAll(/.*private\/transfers.*/); + return res }; return { informTransfer }; @@ -165,18 +164,19 @@ export function useInstanceTransfers( if (beforeData) setLastBefore(beforeData); }, [afterData, beforeData]); - // this has problems when there are some ids missing - if (beforeError) return beforeError; if (afterError) return afterError; + // if the query returns less that we ask, then we have reach the end or beginning + const isReachingEnd = afterData && afterData.data.transfers.length < totalAfter; + const isReachingStart = args?.position === undefined || + (beforeData && beforeData.data.transfers.length < totalBefore); + const pagination = { - isReachingEnd: afterData && afterData.data.transfers.length < totalAfter, - isReachingStart: - !args?.position || - (beforeData && beforeData.data.transfers.length < totalBefore), + isReachingEnd, + isReachingStart, loadMore: () => { - if (!afterData) return; + if (!afterData || isReachingEnd) return; if (afterData.data.transfers.length < MAX_RESULT_SIZE) { setPageAfter(pageAfter + 1); } else { @@ -188,7 +188,7 @@ export function useInstanceTransfers( } }, loadMorePrev: () => { - if (!beforeData) return; + if (!beforeData || isReachingStart) return; if (beforeData.data.transfers.length < MAX_RESULT_SIZE) { setPageBefore(pageBefore + 1); } else if (beforeData) { diff --git a/packages/merchant-backoffice/src/utils/switchableAxios.ts b/packages/merchant-backoffice/src/utils/switchableAxios.ts @@ -0,0 +1,66 @@ +/* + 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 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/tests/axiosMock.ts b/packages/merchant-backoffice/tests/axiosMock.ts @@ -20,7 +20,8 @@ */ import * as axios from 'axios'; import { MerchantBackend } from '../src/declaration'; -import { setAxiosRequestAsTestingEnvironment } from "../src/hooks/backend"; +import { mockAxiosOnce, setAxiosRequestAsTestingEnvironment } from '../src/utils/switchableAxios'; +// import { mockAxiosOnce, setAxiosRequestAsTestingEnvironment } from "../src/hooks/backend"; export type Query<Req, Res> = (GetQuery | PostQuery | DeleteQuery | PatchQuery) & RequestResponse<Req, Res> @@ -32,29 +33,28 @@ interface PostQuery { post: string } interface DeleteQuery { delete: string } interface PatchQuery { patch: string } -setAxiosRequestAsTestingEnvironment(); const JEST_DEBUG_LOG = process.env['JEST_DEBUG_LOG'] !== undefined type TestValues = [axios.AxiosRequestConfig | undefined, { query: Query<any, any>; params?: { request?: any, qparam?: any, response?: any } } | undefined] -const defaultCallback = (actualQuery?: axios.AxiosRequestConfig) => { +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') } -export class AxiosMockEnvironment { - expectations: Array<{ query: Query<any, any>, params?: { request?: any, qparam?: any, response?: any } } | undefined> = [] - axiosMock: jest.MockedFunction<axios.AxiosStatic> +setAxiosRequestAsTestingEnvironment( + defaultCallback +); - constructor() { - this.axiosMock = (axios.default as jest.MockedFunction<axios.AxiosStatic>).mockImplementation(defaultCallback as any) - } +export class AxiosMockEnvironment { + expectations: Array<{ query: Query<any, any>, 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: { request?: RequestType, qparam?: any, response?: ResponseType }): void { - this.expectations.push(expectedQuery ? { query: expectedQuery, params } : undefined) - this.axiosMock = this.axiosMock.mockImplementationOnce(function (actualQuery?: axios.AxiosRequestConfig): axios.AxiosPromise { + const result = mockAxiosOnce(function (actualQuery?: axios.AxiosRequestConfig): axios.AxiosPromise { if (JEST_DEBUG_LOG) { console.log('query to the backend is made', actualQuery) @@ -91,23 +91,15 @@ export class AxiosMockEnvironment { }) } as any) + + this.expectations.push(expectedQuery ? { query: expectedQuery, params, result } : undefined) } getLastTestValues(): TestValues { - const lastCall = this.axiosMock.mock.calls[0] - if (lastCall === undefined) { - const expectedQuery = this.expectations.shift() - return [undefined, expectedQuery] - } - const actualQuery = lastCall[0] as axios.AxiosRequestConfig - - //Remove values from the last call const expectedQuery = this.expectations.shift() - this.axiosMock.mock.calls.shift() - this.axiosMock.mock.results.shift() return [ - actualQuery, expectedQuery + expectedQuery?.result.args, expectedQuery ] } @@ -170,19 +162,25 @@ export function assertNextRequest(env: AxiosMockEnvironment): void { } -export const API_LIST_PRODUCTS: Query< - unknown, - MerchantBackend.Products.InventorySummaryResponse +//////////////////// +// ORDER +//////////////////// + +export const API_CREATE_ORDER: Query< + MerchantBackend.Orders.PostOrderRequest, + MerchantBackend.Orders.PostOrderResponse > = { - get: "http://backend/instances/default/private/products", + post: "http://backend/instances/default/private/orders", }; -export const API_LIST_RESERVES: Query< +export const API_GET_ORDER_BY_ID = ( + id: string +): Query< unknown, - MerchantBackend.Tips.TippingReserveStatus -> = { - get: "http://backend/instances/default/private/reserves", -}; + MerchantBackend.Orders.MerchantOrderStatusResponse +> => ({ + get: `http://backend/instances/default/private/orders/${id}`, +}); export const API_LIST_ORDERS: Query< unknown, @@ -191,6 +189,37 @@ export const API_LIST_ORDERS: Query< 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 @@ -198,31 +227,29 @@ export const API_LIST_TRANSFERS: Query< 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_CREATE_RESERVE: Query< - MerchantBackend.Tips.ReserveCreateRequest, - MerchantBackend.Tips.ReserveCreateConfirmation -> = { - post: "http://backend/instances/default/private/reserves", -}; -export const API_DELETE_RESERVE: Query< - MerchantBackend.Tips.ReserveCreateRequest, - MerchantBackend.Tips.ReserveCreateConfirmation -> = { - delete: "http://backend/instances/default/private/reserves", -}; - -export const API_CREATE_ORDER: Query< - MerchantBackend.Orders.PostOrderRequest, - MerchantBackend.Orders.PostOrderResponse +export const API_LIST_PRODUCTS: Query< + unknown, + MerchantBackend.Products.InventorySummaryResponse > = { - post: "http://backend/instances/default/private/orders", + get: "http://backend/instances/default/private/products", }; export const API_GET_PRODUCT_BY_ID = ( @@ -234,15 +261,72 @@ export const API_GET_PRODUCT_BY_ID = ( export const API_UPDATE_PRODUCT_BY_ID = ( id: string ): Query< - Partial<MerchantBackend.Products.ProductPatchDetail>, + 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}`, +}); + diff --git a/packages/merchant-backoffice/tests/hooks/swr/index.tsx b/packages/merchant-backoffice/tests/hooks/swr/index.tsx @@ -0,0 +1,45 @@ +/* + 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 { ComponentChildren, h, VNode } from "preact"; +import { SWRConfig } from "swr"; +import { BackendContextProvider } from "../../../src/context/backend"; +import { InstanceContextProvider } from "../../../src/context/instance"; + +interface TestingContextProps { + children?: ComponentChildren; +} +export function TestingContext({ children }: TestingContextProps): VNode { + return ( + <BackendContextProvider defaultUrl="http://backend" initialToken="token"> + <InstanceContextProvider + value={{ + token: "token", + id: "default", + admin: true, + changeToken: () => null, + }} + > + <SWRConfig value={{ provider: () => new Map() }}>{children}</SWRConfig> + </InstanceContextProvider> + </BackendContextProvider> + ); +} diff --git a/packages/merchant-backoffice/tests/hooks/swr/order-create.test.tsx b/packages/merchant-backoffice/tests/hooks/swr/order-create.test.tsx @@ -1,139 +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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { renderHook } from "@testing-library/preact-hooks"; -import { act } from "preact/test-utils"; -import * as backend from "../../../src/context/backend"; -import * as instance from "../../../src/context/instance"; -import { MerchantBackend } from "../../../src/declaration"; -import { useInstanceOrders, useOrderAPI } from "../../../src/hooks/order"; -import { - API_CREATE_ORDER, - API_LIST_ORDERS, - AxiosMockEnvironment, - assertNoMoreRequestWereMade, - assertNextRequest, -} from "../../axiosMock"; - -jest.mock("axios"); - -describe("order api", () => { - beforeEach(() => { - jest - .spyOn(backend, "useBackendContext") - .mockImplementation( - () => ({ url: "http://backend", token: "token" } as any) - ); - jest - .spyOn(instance, "useInstanceContext") - .mockImplementation( - () => ({ token: "token", id: "default", admin: true } as any) - ); - }); - - it("should not have problem with cache after an creation", 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 newDate = (d: Date) => { - console.log("new date", d); - }; - - const { result, waitForNextUpdate } = renderHook(() => { - const query = useInstanceOrders({ paid: "yes" }, newDate); - const api = useOrderAPI(); - - return { query, api }; - }); // get products -> loading - - if (!result.current) { - expect(result.current).toBeDefined(); - return; - } - - expect(result.current.query.loading).toBeTruthy(); - await waitForNextUpdate(); - assertNextRequest(env); - assertNextRequest(env); - assertNoMoreRequestWereMade(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); - }); - - assertNextRequest(env); //post - await waitForNextUpdate(); - assertNextRequest(env); //get - assertNextRequest(env); //get - assertNoMoreRequestWereMade(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" }], - }); - }); -}); diff --git a/packages/merchant-backoffice/tests/hooks/swr/order-pagination.test.tsx b/packages/merchant-backoffice/tests/hooks/swr/order-pagination.test.tsx @@ -1,126 +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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { act, renderHook } from "@testing-library/preact-hooks"; -import * as backend from "../../../src/context/backend"; -import * as instance from "../../../src/context/instance"; -import { useInstanceOrders } from "../../../src/hooks/order"; -import { - API_LIST_ORDERS, - AxiosMockEnvironment, - assertNoMoreRequestWereMade, - assertNextRequest, -} from "../../axiosMock"; - -jest.mock("axios"); - -describe("order pagination", () => { - beforeEach(() => { - jest - .spyOn(backend, "useBackendContext") - .mockImplementation( - () => ({ url: "http://backend", token: "token" } as any) - ); - jest - .spyOn(instance, "useInstanceContext") - .mockImplementation( - () => ({ token: "token", id: "default", admin: true } as any) - ); - }); - - it("should change pagination", 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 newDate = (d: Date) => { - console.log("new date", d); - }; - - const date = new Date(12); - const { result, waitForNextUpdate } = renderHook(() => - useInstanceOrders({ wired: "yes", date }, newDate) - ); - - assertNextRequest(env); - assertNextRequest(env); - assertNoMoreRequestWereMade(env); - - await waitForNextUpdate(); - - expect(result.current?.ok).toBeTruthy(); - if (!result.current?.ok) return; - - expect(result.current.data).toEqual({ - orders: [{ order_id: "1" }, { order_id: "2" }], - }); - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: -40, wired: "yes", date_ms: 13 }, - response: { - orders: [{ order_id: "2" } as any, { order_id: "3" } as any], - }, - }); - - await act(() => { - if (!result.current?.ok) throw Error("not ok"); - result.current.loadMore(); - }); - await waitForNextUpdate(); - - assertNextRequest(env); - assertNoMoreRequestWereMade(env); - - env.addRequestExpectation(API_LIST_ORDERS, { - qparam: { delta: 40, wired: "yes", date_ms: 12 }, - response: { - orders: [{ order_id: "1" } as any, { order_id: "0" } as any], - }, - }); - await act(() => { - if (!result.current?.ok) throw Error("not ok"); - result.current.loadMorePrev(); - }); - await waitForNextUpdate(); - assertNextRequest(env); - assertNoMoreRequestWereMade(env); - - expect(result.current.data).toEqual({ - orders: [ - { order_id: "0" }, - { order_id: "1" }, - { order_id: "2" }, - { order_id: "3" }, - ], - }); - }); -}); diff --git a/packages/merchant-backoffice/tests/hooks/swr/order.test.ts b/packages/merchant-backoffice/tests/hooks/swr/order.test.ts @@ -0,0 +1,567 @@ +/* + 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 { TestingContext } from "."; +import { MerchantBackend } from "../../../src/declaration"; +import { useInstanceOrders, useOrderAPI, useOrderDetails } from "../../../src/hooks/order"; +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"; + +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 }); + + 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({ + 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 }); + + 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({ + 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 }); + + 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({ + 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 }); + + 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({ + 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 }); + + 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({ + 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/tests/hooks/swr/product-create.test.tsx b/packages/merchant-backoffice/tests/hooks/swr/product-create.test.tsx @@ -1,142 +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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { renderHook } from "@testing-library/preact-hooks"; -import { act } from "preact/test-utils"; -import * as backend from "../../../src/context/backend"; -import * as instance from "../../../src/context/instance"; -import { MerchantBackend } from "../../../src/declaration"; -import { useInstanceProducts, useProductAPI } from "../../../src/hooks/product"; -import { - API_CREATE_PRODUCT, - API_LIST_PRODUCTS, - API_GET_PRODUCT_BY_ID, - AxiosMockEnvironment, - assertJustExpectedRequestWereMade, -} from "../../axiosMock"; - -jest.mock("axios"); - -describe("product create api", () => { - beforeEach(() => { - jest - .spyOn(backend, "useBackendContext") - .mockImplementation( - () => ({ url: "http://backend", token: "token" } as any) - ); - jest - .spyOn(instance, "useInstanceContext") - .mockImplementation( - () => ({ token: "token", id: "default", admin: true } as any) - ); - }); - - it("should not have problem with cache after an creation", 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:33" } as MerchantBackend.Products.ProductDetail, - }); - - const { result, waitForNextUpdate } = renderHook(() => { - const api = useProductAPI(); - const query = useInstanceProducts(); - - return { query, api }; - }); - if (!result.current) { - expect(result.current).toBeDefined(); - return; - } - expect(result.current.query.loading).toBeTruthy(); - - await waitForNextUpdate(); // first query to list products - expect(result.current.query.loading).toBeTruthy(); - - await waitForNextUpdate(); // second query to get product details - 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:33" }, - ]); - - env.addRequestExpectation(API_CREATE_PRODUCT, { - request: { - price: "ARS:3333", - } as MerchantBackend.Products.ProductAddDetail, - }); - - act(async () => { - return await result.current?.api.createProduct({ - price: "ARS:3333", - } as any); - }); - - assertJustExpectedRequestWereMade(env); - - env.addRequestExpectation(API_LIST_PRODUCTS, { - response: { - products: [{ product_id: "1234" }, { product_id: "2222" }], - }, - }); - - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { price: "ARS:33" } as MerchantBackend.Products.ProductDetail, - }); - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { - response: { price: "ARS:33" } as MerchantBackend.Products.ProductDetail, - }); - - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2222"), { - response: { price: "ARS:3333" } as MerchantBackend.Products.ProductDetail, - }); - expect(result.current.query.loading).toBeFalsy(); - - await waitForNextUpdate(); // loading product -> products - await waitForNextUpdate(); // loading product -> products - - assertJustExpectedRequestWereMade(env); - - expect(result.current.query.loading).toBeFalsy(); - expect(result.current.query.ok).toBeTruthy(); - - expect(result.current.query.data).toEqual([ - { - id: "1234", - price: "ARS:33", - }, - { - id: "2222", - price: "ARS:3333", - }, - ]); - }); -}); diff --git a/packages/merchant-backoffice/tests/hooks/swr/product-delete.test.tsx b/packages/merchant-backoffice/tests/hooks/swr/product-delete.test.tsx @@ -1,120 +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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { renderHook } from "@testing-library/preact-hooks"; -import { act } from "preact/test-utils"; -import * as backend from "../../../src/context/backend"; -import * as instance from "../../../src/context/instance"; -import { MerchantBackend } from "../../../src/declaration"; -import { useInstanceProducts, useProductAPI } from "../../../src/hooks/product"; -import { - API_LIST_PRODUCTS, - API_GET_PRODUCT_BY_ID, - AxiosMockEnvironment, - assertNextRequest, - assertJustExpectedRequestWereMade, -} from "../../axiosMock"; - -jest.mock("axios"); - -describe("product delete api", () => { - beforeEach(() => { - jest - .spyOn(backend, "useBackendContext") - .mockImplementation( - () => ({ url: "http://backend", token: "token" } as any) - ); - jest - .spyOn(instance, "useInstanceContext") - .mockImplementation( - () => ({ token: "token", id: "default", admin: true } as any) - ); - }); - - it("should not have problem with cache after a delete", 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 { query, api }; - }); - - await waitForNextUpdate(); - await waitForNextUpdate(); - assertJustExpectedRequestWereMade(env); - - 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( - { - delete: "http://backend/instances/default/private/products/1234", - }, - {} - ); - - env.addRequestExpectation(API_LIST_PRODUCTS, { - response: { - products: [{ product_id: "2345" }], - }, - }); - - env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), { - response: { price: "ARS:23" } as MerchantBackend.Products.ProductDetail, - }); - - act(async () => { - await result.current?.api.deleteProduct("1234"); - }); - - assertNextRequest(env); - await waitForNextUpdate(); - await waitForNextUpdate(); - assertJustExpectedRequestWereMade(env); - - expect(result.current.query.data).toEqual([ - { - id: "2345", - price: "ARS:23", - }, - ]); - }); -}); diff --git a/packages/merchant-backoffice/tests/hooks/swr/product-details-update.test.tsx b/packages/merchant-backoffice/tests/hooks/swr/product-details-update.test.tsx @@ -1,113 +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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { renderHook, act } from "@testing-library/preact-hooks"; - -import * as backend from "../../../src/context/backend"; -import * as instance from "../../../src/context/instance"; -import { MerchantBackend } from "../../../src/declaration"; -import { useProductAPI, useProductDetails } from "../../../src/hooks/product"; -import { - API_GET_PRODUCT_BY_ID, - API_UPDATE_PRODUCT_BY_ID, - AxiosMockEnvironment, - assertNextRequest, - assertJustExpectedRequestWereMade, -} from "../../axiosMock"; - -jest.mock("axios"); - -describe("product details api", () => { - beforeEach(() => { - jest - .spyOn(backend, "useBackendContext") - .mockImplementation( - () => ({ url: "http://backend", token: "token" } as any) - ); - jest - .spyOn(instance, "useInstanceContext") - .mockImplementation( - () => ({ token: "token", id: "default", admin: true } as any) - ); - }); - - it("should not have problem with cache after an update", 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 }; - }); - - if (!result.current) { - expect(result.current).toBeDefined(); - 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" }, - }); - - 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", - }); - }); -}); diff --git a/packages/merchant-backoffice/tests/hooks/swr/product-update.test.tsx b/packages/merchant-backoffice/tests/hooks/swr/product-update.test.tsx @@ -1,127 +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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { renderHook } from "@testing-library/preact-hooks"; -import { act } from "preact/test-utils"; -import * as backend from "../../../src/context/backend"; -import * as instance from "../../../src/context/instance"; -import { MerchantBackend } from "../../../src/declaration"; -import { useInstanceProducts, useProductAPI } from "../../../src/hooks/product"; -import { - API_GET_PRODUCT_BY_ID, - API_LIST_PRODUCTS, - API_UPDATE_PRODUCT_BY_ID, - AxiosMockEnvironment, - assertJustExpectedRequestWereMade, - assertNextRequest, -} from "../../axiosMock"; - -jest.mock("axios"); - -describe("product list api", () => { - beforeEach(() => { - jest - .spyOn(backend, "useBackendContext") - .mockImplementation( - () => ({ url: "http://backend", token: "token" } as any) - ); - jest - .spyOn(instance, "useInstanceContext") - .mockImplementation( - () => ({ token: "token", id: "default", admin: true } as any) - ); - }); - - it("should not have problem with cache after an update", 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 }; - }); // get products -> loading - - if (!result.current) { - expect(result.current).toBeDefined(); - return; - } - expect(result.current.query.loading).toBeTruthy(); - await waitForNextUpdate(); - - 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([ - { id: "1234", price: "ARS:12" }, - ]); - - env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("1234"), { - request: { price: "ARS:13" }, - }); - - 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(); - // await waitForNextUpdate(); - - assertJustExpectedRequestWereMade(env); - // await waitForNextUpdate(); - - 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", - }, - ]); - }); -}); diff --git a/packages/merchant-backoffice/tests/hooks/swr/product.test.ts b/packages/merchant-backoffice/tests/hooks/swr/product.test.ts @@ -0,0 +1,338 @@ +/* + 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 { TestingContext } from "."; +import { MerchantBackend } from "../../../src/declaration"; +import { useInstanceProducts, useProductAPI, useProductDetails } from "../../../src/hooks/product"; +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"; + +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 + + if (!result.current) { + expect(result.current).toBeDefined(); + 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 + + if (!result.current) { + expect(result.current).toBeDefined(); + 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 + + if (!result.current) { + expect(result.current).toBeDefined(); + 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 }); + + if (!result.current) { + expect(result.current).toBeDefined(); + 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/tests/hooks/swr/reserve.test.ts b/packages/merchant-backoffice/tests/hooks/swr/reserve.test.ts @@ -21,34 +21,28 @@ import { renderHook } from "@testing-library/preact-hooks"; import { act } from "preact/test-utils"; -import * as backend from "../../../src/context/backend"; -import * as instance from "../../../src/context/instance"; import { MerchantBackend } from "../../../src/declaration"; import { useInstanceReserves, - useReservesAPI + useReserveDetails, + useReservesAPI, + useTipDetails, } from "../../../src/hooks/reserves"; import { - API_CREATE_RESERVE, API_LIST_RESERVES, assertJustExpectedRequestWereMade, AxiosMockEnvironment + 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"; +import { TestingContext } from "./index"; -jest.mock("axios"); - -describe("reserve api", () => { - beforeEach(() => { - jest - .spyOn(backend, "useBackendContext") - .mockImplementation( - () => ({ url: "http://backend", token: "token" } as any) - ); - jest - .spyOn(instance, "useInstanceContext") - .mockImplementation( - () => ({ token: "token", id: "default", admin: true } as any) - ); - }); - - it("should mutate list cache when creating a reserve", async () => { +describe("reserve api interaction with listing ", () => { + it("should evict cache when creating a reserve", async () => { const env = new AxiosMockEnvironment(); env.addRequestExpectation(API_LIST_RESERVES, { @@ -61,12 +55,15 @@ describe("reserve api", () => { }, }); - const { result, waitForNextUpdate } = renderHook(() => { - const api = useReservesAPI(); - const query = useInstanceReserves(); + const { result, waitForNextUpdate } = renderHook( + () => { + const api = useReservesAPI(); + const query = useInstanceReserves(); - return { query, api }; - }); + return { query, api }; + }, + { wrapper: TestingContext } + ); if (!result.current) { expect(result.current).toBeDefined(); @@ -74,7 +71,7 @@ describe("reserve api", () => { } expect(result.current.query.loading).toBeTruthy(); - await waitForNextUpdate(); + await waitForNextUpdate({ timeout: 1 }); assertJustExpectedRequestWereMade(env); @@ -124,7 +121,7 @@ describe("reserve api", () => { expect(result.current.query.loading).toBeFalsy(); - await waitForNextUpdate(); + await waitForNextUpdate({ timeout: 1 }); assertJustExpectedRequestWereMade(env); @@ -143,6 +140,331 @@ describe("reserve api", () => { }); }); + 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, + } + ); + + 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({ + 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, + } + ); + + 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({ + 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_ms: 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, + } + ); + + 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({ + 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_ms: 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, + } + ); + + 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({ + total_picked_up: "USD:12", + reason: "not", + }); + }); }); diff --git a/packages/merchant-backoffice/tests/hooks/swr/transfer-pagination.test.tsx b/packages/merchant-backoffice/tests/hooks/swr/transfer-pagination.test.tsx @@ -1,120 +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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { act, renderHook } from "@testing-library/preact-hooks"; -import * as backend from "../../../src/context/backend"; -import * as instance from "../../../src/context/instance"; -import { useInstanceTransfers } from "../../../src/hooks/transfer"; -import { - API_LIST_TRANSFERS, - assertNextRequest, - assertNoMoreRequestWereMade, - AxiosMockEnvironment, -} from "../../axiosMock"; - -jest.mock("axios"); - -describe("transfer pagination", () => { - beforeEach(() => { - jest - .spyOn(backend, "useBackendContext") - .mockImplementation( - () => ({ url: "http://backend", token: "token" } as any) - ); - jest - .spyOn(instance, "useInstanceContext") - .mockImplementation( - () => ({ token: "token", id: "default", admin: true } as any) - ); - }); - - it("should change pagination", async () => { - const env = new AxiosMockEnvironment(); - env.addRequestExpectation(API_LIST_TRANSFERS, { - qparam: { limit: 20, verified: "yes" }, - response: { - transfers: [{ wtid: "1" } as any], - }, - }); - - env.addRequestExpectation(API_LIST_TRANSFERS, { - qparam: { limit: -20, verified: "yes" }, - response: { - transfers: [{ wtid: "3" } as any], - }, - }); - - const updatePosition = (d: string) => { - console.log("updatePosition", d); - }; - - const { result, waitForNextUpdate } = renderHook(() => - useInstanceTransfers({ verified: "yes", position: "" }, updatePosition) - ); - - assertNextRequest(env); - assertNextRequest(env); - assertNoMoreRequestWereMade(env); - - await waitForNextUpdate(); // get info of every product, -> loading - - expect(result.current?.ok).toBeTruthy(); - if (!result.current?.ok) return; - - expect(result.current.data).toEqual({ - transfers: [{ wtid: "1" }, { wtid: "3" }], - }); - - env.addRequestExpectation(API_LIST_TRANSFERS, { - qparam: { limit: -40, verified: "yes" }, - response: { - transfers: [{ wtid: "4" } as any, { wtid: "3" } as any], - }, - }); - - await act(() => { - if (!result.current?.ok) throw Error("not ok"); - result.current.loadMore(); - }); - await waitForNextUpdate(); - assertNextRequest(env); - assertNoMoreRequestWereMade(env); - - env.addRequestExpectation(API_LIST_TRANSFERS, { - qparam: { limit: 40, verified: "yes" }, - response: { - transfers: [{ wtid: "1" } as any, { wtid: "2" } as any], - }, - }); - - await act(() => { - if (!result.current?.ok) throw Error("not ok"); - result.current.loadMorePrev(); - }); - await waitForNextUpdate(); - assertNextRequest(env); - assertNoMoreRequestWereMade(env); - - expect(result.current.data).toEqual({ - transfers: [{ wtid: "2" }, { wtid: "1" }, { wtid: "4" }, { wtid: "3" }], - }); - }); -}); diff --git a/packages/merchant-backoffice/tests/hooks/swr/transfer.test.ts b/packages/merchant-backoffice/tests/hooks/swr/transfer.test.ts @@ -0,0 +1,268 @@ +/* + 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 { act, renderHook } from "@testing-library/preact-hooks"; +import { TestingContext } from "./index"; +import { useInstanceTransfers, useTransferAPI } from "../../../src/hooks/transfer"; +import { + API_INFORM_TRANSFERS, + API_LIST_TRANSFERS, + assertJustExpectedRequestWereMade, + assertNoMoreRequestWereMade, + AxiosMockEnvironment, +} from "../../axiosMock"; +import { MerchantBackend } from "../../../src/declaration"; + +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 }); + + 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({ + 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' }], + }); + }); + + +});