refactor better QA

removed axios, use fetch
removed jest, added mocha and chai
moved the default request handler to runtime dependency (so it can be replaced for testing)
refactored ALL the test to the standard web-utils
all hooks now use ONE request handler
moved the tests from test folder to src
This commit is contained in:
Sebastian 2023-01-03 01:57:39 -03:00
parent d1aa79eae8
commit a2668c22f0
No known key found for this signature in database
GPG Key ID: BE4FF68352439FC1
59 changed files with 3567 additions and 4300 deletions

View File

@ -35,7 +35,6 @@
"dependencies": {
"@gnu-taler/taler-util": "workspace:*",
"@gnu-taler/web-util": "workspace:*",
"axios": "^0.21.1",
"date-fns": "2.29.3",
"history": "4.10.1",
"jed": "1.1.1",
@ -48,10 +47,8 @@
"devDependencies": {
"@creativebulma/bulma-tooltip": "^1.2.0",
"@gnu-taler/pogen": "^0.0.5",
"@testing-library/preact": "^2.0.1",
"@testing-library/preact-hooks": "^1.1.0",
"@types/chai": "^4.3.0",
"@types/history": "^4.7.8",
"@types/jest": "^26.0.23",
"@types/mocha": "^8.2.3",
"@types/node": "^18.11.17",
"@typescript-eslint/eslint-plugin": "^4.22.0",
@ -64,6 +61,7 @@
"bulma-switch-control": "^1.1.1",
"bulma-timeline": "^3.0.4",
"bulma-upload-control": "^1.2.0",
"chai": "^4.3.6",
"dotenv": "^8.2.0",
"eslint": "^7.25.0",
"eslint-config-preact": "^1.1.4",
@ -72,13 +70,12 @@
"html-webpack-inline-source-plugin": "0.0.10",
"html-webpack-skip-assets-plugin": "^1.0.1",
"inline-chunk-html-plugin": "^1.1.1",
"jest": "^26.6.3",
"jest-preset-preact": "^4.0.2",
"mocha": "^9.2.0",
"preact-render-to-string": "^5.2.6",
"rimraf": "^3.0.2",
"sass": "1.56.1",
"source-map-support": "^0.5.21",
"typedoc": "^0.20.36",
"typescript": "4.8.4"
}
}
}

View File

@ -28,7 +28,7 @@ import { Loading } from "./components/exception/loading.js";
import { Menu, NotificationCard } from "./components/menu/index.js";
import { useBackendContext } from "./context/backend.js";
import { InstanceContextProvider } from "./context/instance.js";
import { HttpError } from "./hooks/backend.js";
import { HttpError } from "./utils/request.js";
import {
useBackendDefaultToken,
useBackendInstanceToken,
@ -484,7 +484,7 @@ export function Redirect({ to }: { to: string }): null {
function AdminInstanceUpdatePage({
id,
...rest
}: { id: string } & InstanceUpdatePageProps) {
}: { id: string } & InstanceUpdatePageProps): VNode {
const [token, changeToken] = useBackendInstanceToken(id);
const { updateLoginStatus: changeBackend } = useBackendContext();
const updateLoginStatus = (url: string, token?: string) => {

View File

@ -195,7 +195,7 @@ export function InputPaytoForm<T>({
if (opt_value) url.searchParams.set(opt_key, opt_value);
});
}
const paytoURL = !url ? "" : url.toString();
const paytoURL = !url ? "" : url.href;
const errors: FormErrors<Entity> = {
target: value.target === noTargetValue ? i18n.str`required` : undefined,

View File

@ -14,29 +14,30 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
// Mock Browser API's which are not supported by JSDOM, e.g. ServiceWorker, LocalStorage
/**
* An example how to mock localStorage is given below 👇
*/
import { ComponentChildren, createContext, h, VNode } from "preact";
import { useContext } from "preact/hooks";
import { defaultRequestHandler } from "../utils/request.js";
/*
// Mocks localStorage
const localStorageMock = (function() {
let store = {};
interface Type {
request: typeof defaultRequestHandler;
}
return {
getItem: (key) => store[key] || null,
setItem: (key, value) => store[key] = value.toString(),
clear: () => store = {}
};
const Context = createContext<Type>({
request: defaultRequestHandler,
});
})();
Object.defineProperty(window, 'localStorage', {
value: localStorageMock
}); */
export const useApiContext = (): Type => useContext(Context);
export const ApiContextProvider = ({
children,
value,
}: {
value: Type;
children: ComponentChildren;
}): VNode => {
return h(Context.Provider, { value, children });
};

View File

@ -0,0 +1,131 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { tests } from "@gnu-taler/web-util/lib/index.browser";
import { ComponentChildren, h, VNode } from "preact";
import { MerchantBackend } from "../declaration.js";
import {
useAdminAPI,
useInstanceAPI,
useManagementAPI,
} from "../hooks/instance.js";
import { expect } from "chai";
import { ApiMockEnvironment } from "../hooks/testing.js";
import { API_CREATE_INSTANCE, API_UPDATE_CURRENT_INSTANCE_AUTH, API_UPDATE_INSTANCE_AUTH_BY_ID } from "../hooks/urls.js";
interface TestingContextProps {
children?: ComponentChildren;
}
describe("backend context api ", () => {
it("should use new token after updating the instance token in the settings as user", async () => {
const env = new ApiMockEnvironment();
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const instance = useInstanceAPI();
const management = useManagementAPI("default");
const admin = useAdminAPI();
return { instance, management, admin };
},
{},
[
({ instance, management, admin }) => {
env.addRequestExpectation(API_UPDATE_INSTANCE_AUTH_BY_ID("default"), {
request: {
method: "token",
token: "another_token",
},
response: {
name: "instance_name",
} as MerchantBackend.Instances.QueryInstancesResponse,
});
management.setNewToken("another_token")
},
({ instance, management, admin }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
env.addRequestExpectation(API_CREATE_INSTANCE, {
auth: "another_token",
request: {
id: "new_instance_id",
} as MerchantBackend.Instances.InstanceConfigurationMessage,
});
admin.createInstance({
id: "new_instance_id",
} as MerchantBackend.Instances.InstanceConfigurationMessage);
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
});
it("should use new token after updating the instance token in the settings as admin", async () => {
const env = new ApiMockEnvironment();
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const instance = useInstanceAPI();
const management = useManagementAPI("default");
const admin = useAdminAPI();
return { instance, management, admin };
},
{},
[
({ instance, management, admin }) => {
env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, {
request: {
method: "token",
token: "another_token",
},
response: {
name: "instance_name",
} as MerchantBackend.Instances.QueryInstancesResponse,
});
instance.setNewToken("another_token");
},
({ instance, management, admin }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
env.addRequestExpectation(API_CREATE_INSTANCE, {
auth: "another_token",
request: {
id: "new_instance_id",
} as MerchantBackend.Instances.InstanceConfigurationMessage,
});
admin.createInstance({
id: "new_instance_id",
} as MerchantBackend.Instances.InstanceConfigurationMessage);
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
});
});

View File

@ -31,6 +31,7 @@ interface BackendContextType {
clearAllTokens: () => void;
addTokenCleaner: (c: () => void) => void;
updateLoginStatus: (url: string, token?: string) => void;
updateToken: (token?: string) => void;
}
const BackendContext = createContext<BackendContextType>({
@ -41,6 +42,7 @@ const BackendContext = createContext<BackendContextType>({
clearAllTokens: () => null,
addTokenCleaner: () => null,
updateLoginStatus: () => null,
updateToken: () => null,
});
function useBackendContextState(
@ -87,6 +89,7 @@ function useBackendContextState(
updateLoginStatus,
resetBackend,
clearAllTokens,
updateToken,
addTokenCleaner: addTokenCleanerMemo,
};
}

View File

@ -1,54 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { h, createContext, VNode, ComponentChildren } from "preact";
import { useContext } from "preact/hooks";
import useSWR from "swr";
import useSWRInfinite from "swr/infinite";
interface Type {
useSWR: typeof useSWR;
useSWRInfinite: typeof useSWRInfinite;
}
const Context = createContext<Type>({} as Type);
export const useFetchContext = (): Type => useContext(Context);
export const FetchContextProvider = ({
children,
}: {
children: ComponentChildren;
}): VNode => {
return h(Context.Provider, { value: { useSWR, useSWRInfinite }, children });
};
export const FetchContextProviderTesting = ({
children,
data,
}: {
children: ComponentChildren;
data: any;
}): VNode => {
return h(Context.Provider, {
value: { useSWR: () => data, useSWRInfinite },
children,
});
};

View File

@ -19,7 +19,6 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
import { useState } from "preact/hooks";
import { cancelPendingRequest } from "./backend.js";
export interface Options {
slowTolerance: number;
@ -62,8 +61,7 @@ export function useAsync<T>(
clearTimeout(handler);
};
function cancel() {
cancelPendingRequest();
function cancel(): void {
setLoading(false);
setSlow(false);
}

View File

@ -20,15 +20,16 @@
*/
import { useSWRConfig } from "swr";
import axios, { AxiosError, AxiosResponse } from "axios";
import { MerchantBackend } from "../declaration.js";
import { useBackendContext } from "../context/backend.js";
import { useEffect, useState } from "preact/hooks";
import { DEFAULT_REQUEST_TIMEOUT } from "../utils/constants.js";
import { useCallback, useEffect, useState } from "preact/hooks";
import { useInstanceContext } from "../context/instance.js";
import {
axiosHandler,
removeAxiosCancelToken,
} from "../utils/switchableAxios.js";
HttpResponse,
HttpResponseOk,
RequestOptions,
} from "../utils/request.js";
import { useApiContext } from "../context/api.js";
export function useMatchMutate(): (
re: RegExp,
@ -44,9 +45,7 @@ export function useMatchMutate(): (
return function matchRegexMutate(re: RegExp, value?: unknown) {
const allKeys = Array.from(cache.keys());
// console.log(allKeys)
const keys = allKeys.filter((key) => re.test(key));
// console.log(allKeys.length, keys.length)
const mutations = keys.map((key) => {
// console.log(key)
mutate(key, value, true);
@ -55,268 +54,234 @@ export function useMatchMutate(): (
};
}
export type HttpResponse<T> =
| HttpResponseOk<T>
| HttpResponseLoading<T>
| HttpError;
export type HttpResponsePaginated<T> =
| HttpResponseOkPaginated<T>
| HttpResponseLoading<T>
| HttpError;
export interface RequestInfo {
url: string;
hasToken: boolean;
params: unknown;
data: unknown;
status: number;
}
interface HttpResponseLoading<T> {
ok?: false;
loading: true;
clientError?: false;
serverError?: false;
data?: T;
}
export interface HttpResponseOk<T> {
ok: true;
loading?: false;
clientError?: false;
serverError?: false;
data: T;
info?: RequestInfo;
}
export type HttpResponseOkPaginated<T> = HttpResponseOk<T> & WithPagination;
export interface WithPagination {
loadMore: () => void;
loadMorePrev: () => void;
isReachingEnd?: boolean;
isReachingStart?: boolean;
}
export type HttpError =
| HttpResponseClientError
| HttpResponseServerError
| HttpResponseUnexpectedError;
export interface SwrError {
info: unknown;
status: number;
message: string;
}
export interface HttpResponseServerError {
ok?: false;
loading?: false;
clientError?: false;
serverError: true;
error?: MerchantBackend.ErrorDetail;
status: number;
message: string;
info?: RequestInfo;
}
interface HttpResponseClientError {
ok?: false;
loading?: false;
clientError: true;
serverError?: false;
info?: RequestInfo;
isUnauthorized: boolean;
isNotfound: boolean;
status: number;
error?: MerchantBackend.ErrorDetail;
message: string;
}
interface HttpResponseUnexpectedError {
ok?: false;
loading?: false;
clientError?: false;
serverError?: false;
info?: RequestInfo;
status?: number;
error: unknown;
message: string;
}
type Methods = "get" | "post" | "patch" | "delete" | "put";
interface RequestOptions {
method?: Methods;
token?: string;
data?: unknown;
params?: unknown;
}
function buildRequestOk<T>(
res: AxiosResponse<T>,
url: string,
hasToken: boolean,
): HttpResponseOk<T> {
return {
ok: true,
data: res.data,
info: {
params: res.config.params,
data: res.config.data,
url,
hasToken,
status: res.status,
},
};
}
// function buildResponse<T>(data?: T, error?: MerchantBackend.ErrorDetail, isValidating?: boolean): HttpResponse<T> {
// if (isValidating) return {loading: true}
// if (error) return buildRequestFailed()
// }
function buildRequestFailed(
ex: AxiosError<MerchantBackend.ErrorDetail>,
url: string,
hasToken: boolean,
):
| HttpResponseClientError
| HttpResponseServerError
| HttpResponseUnexpectedError {
const status = ex.response?.status;
const info: RequestInfo = {
data: ex.request?.data,
params: ex.request?.params,
url,
hasToken,
status: status || 0,
};
if (status && status >= 400 && status < 500) {
const error: HttpResponseClientError = {
clientError: true,
isNotfound: status === 404,
isUnauthorized: status === 401,
status,
info,
message: ex.response?.data?.hint || ex.message,
error: ex.response?.data,
};
return error;
}
if (status && status >= 500 && status < 600) {
const error: HttpResponseServerError = {
serverError: true,
status,
info,
message:
`${ex.response?.data?.hint} (code ${ex.response?.data?.code})` ||
ex.message,
error: ex.response?.data,
};
return error;
}
const error: HttpResponseUnexpectedError = {
info,
status,
error: ex,
message: ex.message,
};
return error;
}
const CancelToken = axios.CancelToken;
let source = CancelToken.source();
export function cancelPendingRequest(): void {
source.cancel("canceled by the user");
source = CancelToken.source();
}
export function isAxiosError<T>(
error: AxiosError | any,
): error is AxiosError<T> {
return error && error.isAxiosError;
}
export async function request<T>(
url: string,
options: RequestOptions = {},
): Promise<HttpResponseOk<T>> {
const headers = options.token
? { Authorization: `Bearer ${options.token}` }
: undefined;
try {
const res = await axiosHandler({
url,
responseType: "json",
headers,
cancelToken: !removeAxiosCancelToken ? source.token : undefined,
method: options.method || "get",
data: options.data,
params: options.params,
timeout: DEFAULT_REQUEST_TIMEOUT * 1000,
});
return buildRequestOk<T>(res, url, !!options.token);
} catch (e) {
if (isAxiosError<MerchantBackend.ErrorDetail>(e)) {
const error = buildRequestFailed(e, url, !!options.token);
throw error;
}
throw e;
}
}
export function multiFetcher<T>(
urls: string[],
token: string,
backend: string,
): Promise<HttpResponseOk<T>[]> {
return Promise.all(urls.map((url) => fetcher<T>(url, token, backend)));
}
export function fetcher<T>(
url: string,
token: string,
backend: string,
): Promise<HttpResponseOk<T>> {
return request<T>(`${backend}${url}`, { token });
}
export function useBackendInstancesTestForAdmin(): HttpResponse<MerchantBackend.Instances.InstancesResponse> {
const { url, token } = useBackendContext();
const { request } = useBackendBaseRequest();
type Type = MerchantBackend.Instances.InstancesResponse;
const [result, setResult] = useState<HttpResponse<Type>>({ loading: true });
useEffect(() => {
request<Type>(`${url}/management/instances`, { token })
request<Type>(`/management/instances`)
.then((data) => setResult(data))
.catch((error) => setResult(error));
}, [url, token]);
}, [request]);
return result;
}
export function useBackendConfig(): HttpResponse<MerchantBackend.VersionResponse> {
const { url, token } = useBackendContext();
const { request } = useBackendBaseRequest();
type Type = MerchantBackend.VersionResponse;
const [result, setResult] = useState<HttpResponse<Type>>({ loading: true });
useEffect(() => {
request<Type>(`${url}/config`, { token })
request<Type>(`/config`)
.then((data) => setResult(data))
.catch((error) => setResult(error));
}, [url, token]);
}, [request]);
return result;
}
interface useBackendInstanceRequestType {
request: <T>(
path: string,
options?: RequestOptions,
) => Promise<HttpResponseOk<T>>;
fetcher: <T>(path: string) => Promise<HttpResponseOk<T>>;
reserveDetailFetcher: <T>(path: string) => Promise<HttpResponseOk<T>>;
tipsDetailFetcher: <T>(path: string) => Promise<HttpResponseOk<T>>;
multiFetcher: <T>(url: string[]) => Promise<HttpResponseOk<T>[]>;
orderFetcher: <T>(
path: string,
paid?: YesOrNo,
refunded?: YesOrNo,
wired?: YesOrNo,
searchDate?: Date,
delta?: number,
) => Promise<HttpResponseOk<T>>;
transferFetcher: <T>(
path: string,
payto_uri?: string,
verified?: string,
position?: string,
delta?: number,
) => Promise<HttpResponseOk<T>>;
templateFetcher: <T>(
path: string,
position?: string,
delta?: number,
) => Promise<HttpResponseOk<T>>;
}
interface useBackendBaseRequestType {
request: <T>(
path: string,
options?: RequestOptions,
) => Promise<HttpResponseOk<T>>;
}
type YesOrNo = "yes" | "no";
/**
*
* @param root the request is intended to the base URL and no the instance URL
* @returns request handler to
*/
export function useBackendBaseRequest(): useBackendBaseRequestType {
const { url: backend, token } = useBackendContext();
const { request: requestHandler } = useApiContext();
const request = useCallback(
function requestImpl<T>(
path: string,
options: RequestOptions = {},
): Promise<HttpResponseOk<T>> {
return requestHandler<T>(backend, path, { token, ...options });
},
[backend, token],
);
return { request };
}
export function useBackendInstanceRequest(): useBackendInstanceRequestType {
const { url: baseUrl, token: baseToken } = useBackendContext();
const { token: instanceToken, id, admin } = useInstanceContext();
const { request: requestHandler } = useApiContext();
const { backend, token } = !admin
? { backend: baseUrl, token: baseToken }
: { backend: `${baseUrl}/instances/${id}`, token: instanceToken };
const request = useCallback(
function requestImpl<T>(
path: string,
options: RequestOptions = {},
): Promise<HttpResponseOk<T>> {
return requestHandler<T>(backend, path, { token, ...options });
},
[backend, token],
);
const multiFetcher = useCallback(
function multiFetcherImpl<T>(
paths: string[],
): Promise<HttpResponseOk<T>[]> {
return Promise.all(
paths.map((path) => requestHandler<T>(backend, path, { token })),
);
},
[backend, token],
);
const fetcher = useCallback(
function fetcherImpl<T>(path: string): Promise<HttpResponseOk<T>> {
return requestHandler<T>(backend, path, { token });
},
[backend, token],
);
const orderFetcher = useCallback(
function orderFetcherImpl<T>(
path: string,
paid?: YesOrNo,
refunded?: YesOrNo,
wired?: YesOrNo,
searchDate?: Date,
delta?: number,
): Promise<HttpResponseOk<T>> {
const date_ms =
delta && delta < 0 && searchDate
? searchDate.getTime() + 1
: searchDate?.getTime();
const params: any = {};
if (paid !== undefined) params.paid = paid;
if (delta !== undefined) params.delta = delta;
if (refunded !== undefined) params.refunded = refunded;
if (wired !== undefined) params.wired = wired;
if (date_ms !== undefined) params.date_ms = date_ms;
return requestHandler<T>(backend, path, { params, token });
},
[backend, token],
);
const reserveDetailFetcher = useCallback(
function reserveDetailFetcherImpl<T>(
path: string,
): Promise<HttpResponseOk<T>> {
return requestHandler<T>(backend, path, {
params: {
tips: "yes",
},
token,
});
},
[backend, token],
);
const tipsDetailFetcher = useCallback(
function tipsDetailFetcherImpl<T>(
path: string,
): Promise<HttpResponseOk<T>> {
return requestHandler<T>(backend, path, {
params: {
pickups: "yes",
},
token,
});
},
[backend, token],
);
const transferFetcher = useCallback(
function transferFetcherImpl<T>(
path: string,
payto_uri?: string,
verified?: string,
position?: string,
delta?: number,
): Promise<HttpResponseOk<T>> {
const params: any = {};
if (payto_uri !== undefined) params.payto_uri = payto_uri;
if (verified !== undefined) params.verified = verified;
if (delta !== undefined) {
params.limit = delta;
}
if (position !== undefined) params.offset = position;
return requestHandler<T>(backend, path, { params, token });
},
[backend, token],
);
const templateFetcher = useCallback(
function templateFetcherImpl<T>(
path: string,
position?: string,
delta?: number,
): Promise<HttpResponseOk<T>> {
const params: any = {};
if (delta !== undefined) {
params.limit = delta;
}
if (position !== undefined) params.offset = position;
return requestHandler<T>(backend, path, { params, token });
},
[backend, token],
);
return {
request,
fetcher,
multiFetcher,
orderFetcher,
reserveDetailFetcher,
tipsDetailFetcher,
transferFetcher,
templateFetcher,
};
}

View File

@ -59,6 +59,7 @@ export function useBackendDefaultToken(
export function useBackendInstanceToken(
id: string,
): [string | undefined, StateUpdater<string | undefined>] {
const [random, setRandom] = useState(0);
const [token, setToken] = useLocalStorage(`backend-token-${id}`);
const [defaultToken, defaultSetToken] = useBackendDefaultToken();
@ -66,8 +67,20 @@ export function useBackendInstanceToken(
if (id === "default") {
return [defaultToken, defaultSetToken];
}
function updateToken(
value:
| (string | undefined)
| ((s: string | undefined) => string | undefined),
): void {
setToken((p) => {
const toStore = value instanceof Function ? value(p) : value;
// setToken(value)
setRandom(new Date().getTime());
return toStore;
});
}
return [token, setToken];
return [token, updateToken];
}
export function useLang(initial?: string): [string, StateUpdater<string>] {

View File

@ -0,0 +1,660 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { tests } from "@gnu-taler/web-util/lib/index.browser";
import { expect } from "chai";
import { MerchantBackend } from "../declaration.js";
import { useAdminAPI, useBackendInstances, useInstanceAPI, useInstanceDetails, useManagementAPI } from "./instance.js";
import { ApiMockEnvironment } from "./testing.js";
import {
API_CREATE_INSTANCE,
API_DELETE_INSTANCE,
API_GET_CURRENT_INSTANCE,
API_LIST_INSTANCES,
API_UPDATE_CURRENT_INSTANCE,
API_UPDATE_CURRENT_INSTANCE_AUTH, API_UPDATE_INSTANCE_BY_ID
} from "./urls.js";
describe("instance api interaction with details", () => {
it("should evict cache when updating an instance", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
response: {
name: 'instance_name'
} as MerchantBackend.Instances.QueryInstancesResponse,
});
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const api = useInstanceAPI();
const query = useInstanceDetails();
return { query, api };
},
{},
[
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
name: 'instance_name'
});
env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE, {
request: {
name: 'other_name'
} as MerchantBackend.Instances.InstanceReconfigurationMessage,
});
env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
response: {
name: 'other_name'
} as MerchantBackend.Instances.QueryInstancesResponse,
});
api.updateInstance({
name: 'other_name'
} as MerchantBackend.Instances.InstanceReconfigurationMessage);
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
name: 'other_name'
});
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
});
it("should evict cache when setting the instance's token", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
response: {
name: 'instance_name',
auth: {
method: 'token',
token: 'not-secret',
}
} as MerchantBackend.Instances.QueryInstancesResponse,
});
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const api = useInstanceAPI();
const query = useInstanceDetails();
return { query, api };
},
{},
[
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
name: 'instance_name',
auth: {
method: 'token',
token: 'not-secret',
}
});
env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, {
request: {
method: 'token',
token: 'secret'
} as MerchantBackend.Instances.InstanceAuthConfigurationMessage,
});
env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
response: {
name: 'instance_name',
auth: {
method: 'token',
token: 'secret',
}
} as MerchantBackend.Instances.QueryInstancesResponse,
});
api.setNewToken('secret')
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
name: 'instance_name',
auth: {
method: 'token',
token: 'secret',
}
});
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
});
it("should evict cache when clearing the instance's token", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
response: {
name: 'instance_name',
auth: {
method: 'token',
token: 'not-secret',
}
} as MerchantBackend.Instances.QueryInstancesResponse,
});
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const api = useInstanceAPI();
const query = useInstanceDetails();
return { query, api };
},
{},
[
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
name: 'instance_name',
auth: {
method: 'token',
token: 'not-secret',
}
});
env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, {
request: {
method: 'external',
} as MerchantBackend.Instances.InstanceAuthConfigurationMessage,
});
env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
response: {
name: 'instance_name',
auth: {
method: 'external',
}
} as MerchantBackend.Instances.QueryInstancesResponse,
});
api.clearToken();
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
name: 'instance_name',
auth: {
method: 'external',
}
});
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
// const { result, waitForNextUpdate } = renderHook(
// () => {
// const api = useInstanceAPI();
// const query = useInstanceDetails();
// return { query, api };
// },
// { wrapper: TestingContext }
// );
// expect(result.current).not.undefined;
// if (!result.current) {
// return;
// }
// expect(result.current.query.loading).true;
// await waitForNextUpdate({ timeout: 1 });
// expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
// expect(result.current.query.loading).false;
// expect(result.current?.query.ok).true;
// if (!result.current?.query.ok) return;
// expect(result.current.query.data).equals({
// name: 'instance_name',
// auth: {
// method: 'token',
// token: 'not-secret',
// }
// });
// act(async () => {
// await result.current?.api.clearToken();
// });
// expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
// expect(result.current.query.loading).false;
// await waitForNextUpdate({ timeout: 1 });
// expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
// expect(result.current.query.loading).false;
// expect(result.current.query.ok).true;
// expect(result.current.query.data).equals({
// name: 'instance_name',
// auth: {
// method: 'external',
// }
// });
});
});
describe("instance admin api interaction with listing", () => {
it("should evict cache when creating a new instance", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_LIST_INSTANCES, {
response: {
instances: [{
name: 'instance_name'
} as MerchantBackend.Instances.Instance]
},
});
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const api = useAdminAPI();
const query = useBackendInstances();
return { query, api };
},
{},
[
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
instances: [{
name: 'instance_name'
}]
});
env.addRequestExpectation(API_CREATE_INSTANCE, {
request: {
name: 'other_name'
} as MerchantBackend.Instances.InstanceConfigurationMessage,
});
env.addRequestExpectation(API_LIST_INSTANCES, {
response: {
instances: [{
name: 'instance_name'
} as MerchantBackend.Instances.Instance,
{
name: 'other_name'
} as MerchantBackend.Instances.Instance]
},
});
api.createInstance({
name: 'other_name'
} as MerchantBackend.Instances.InstanceConfigurationMessage);
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
instances: [{
name: 'instance_name'
}, {
name: 'other_name'
}]
});
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
});
it("should evict cache when deleting an instance", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_LIST_INSTANCES, {
response: {
instances: [{
id: 'default',
name: 'instance_name'
} as MerchantBackend.Instances.Instance,
{
id: 'the_id',
name: 'second_instance'
} as MerchantBackend.Instances.Instance]
},
});
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const api = useAdminAPI();
const query = useBackendInstances();
return { query, api };
},
{},
[
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
instances: [{
id: 'default',
name: 'instance_name'
}, {
id: 'the_id',
name: 'second_instance'
}]
});
env.addRequestExpectation(API_DELETE_INSTANCE('the_id'), {});
env.addRequestExpectation(API_LIST_INSTANCES, {
response: {
instances: [{
id: 'default',
name: 'instance_name'
} as MerchantBackend.Instances.Instance]
},
});
api.deleteInstance('the_id');
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
instances: [{
id: 'default',
name: 'instance_name'
}]
});
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
// const { result, waitForNextUpdate } = renderHook(
// () => {
// const api = useAdminAPI();
// const query = useBackendInstances();
// return { query, api };
// },
// { wrapper: TestingContext }
// );
// expect(result.current).not.undefined;
// if (!result.current) {
// return;
// }
// expect(result.current.query.loading).true;
// await waitForNextUpdate({ timeout: 1 });
// expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
// expect(result.current.query.loading).false;
// expect(result.current?.query.ok).true;
// if (!result.current?.query.ok) return;
// expect(result.current.query.data).equals({
// instances: [{
// id: 'default',
// name: 'instance_name'
// }, {
// id: 'the_id',
// name: 'second_instance'
// }]
// });
// env.addRequestExpectation(API_DELETE_INSTANCE('the_id'), {});
// act(async () => {
// await result.current?.api.deleteInstance('the_id');
// });
// expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
// env.addRequestExpectation(API_LIST_INSTANCES, {
// response: {
// instances: [{
// id: 'default',
// name: 'instance_name'
// } as MerchantBackend.Instances.Instance]
// },
// });
// expect(result.current.query.loading).false;
// await waitForNextUpdate({ timeout: 1 });
// expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
// expect(result.current.query.loading).false;
// expect(result.current.query.ok).true;
// expect(result.current.query.data).equals({
// instances: [{
// id: 'default',
// name: 'instance_name'
// }]
// });
});
it("should evict cache when deleting (purge) an instance", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_LIST_INSTANCES, {
response: {
instances: [{
id: 'default',
name: 'instance_name'
} as MerchantBackend.Instances.Instance,
{
id: 'the_id',
name: 'second_instance'
} as MerchantBackend.Instances.Instance]
},
});
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const api = useAdminAPI();
const query = useBackendInstances();
return { query, api };
},
{},
[
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
instances: [{
id: 'default',
name: 'instance_name'
}, {
id: 'the_id',
name: 'second_instance'
}]
});
env.addRequestExpectation(API_DELETE_INSTANCE('the_id'), {
qparam: {
purge: 'YES'
}
});
env.addRequestExpectation(API_LIST_INSTANCES, {
response: {
instances: [{
id: 'default',
name: 'instance_name'
} as MerchantBackend.Instances.Instance]
},
});
api.purgeInstance('the_id')
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
instances: [{
id: 'default',
name: 'instance_name'
}]
});
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
});
});
describe("instance management api interaction with listing", () => {
it("should evict cache when updating an instance", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_LIST_INSTANCES, {
response: {
instances: [{
id: 'managed',
name: 'instance_name'
} as MerchantBackend.Instances.Instance]
},
});
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const api = useManagementAPI('managed');
const query = useBackendInstances();
return { query, api };
},
{},
[
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
instances: [{
id: 'managed',
name: 'instance_name'
}]
});
env.addRequestExpectation(API_UPDATE_INSTANCE_BY_ID('managed'), {
request: {
name: 'other_name'
} as MerchantBackend.Instances.InstanceReconfigurationMessage,
});
env.addRequestExpectation(API_LIST_INSTANCES, {
response: {
instances: [
{
id: 'managed',
name: 'other_name'
} as MerchantBackend.Instances.Instance]
},
});
api.updateInstance({
name: 'other_name'
} as MerchantBackend.Instances.InstanceConfigurationMessage);
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
instances: [{
id: 'managed',
name: 'other_name'
}]
});
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
});
});

View File

@ -15,14 +15,11 @@
*/
import useSWR, { useSWRConfig } from "swr";
import { useBackendContext } from "../context/backend.js";
import { useInstanceContext } from "../context/instance.js";
import { MerchantBackend } from "../declaration.js";
import { HttpError, HttpResponse, HttpResponseOk } from "../utils/request.js";
import {
fetcher,
HttpError,
HttpResponse,
HttpResponseOk,
request,
useBackendBaseRequest,
useBackendInstanceRequest,
useMatchMutate,
} from "./backend.js";
@ -36,15 +33,14 @@ interface InstanceAPI {
}
export function useAdminAPI(): AdminAPI {
const { url, token } = useBackendContext();
const { request } = useBackendBaseRequest();
const mutateAll = useMatchMutate();
const createInstance = async (
instance: MerchantBackend.Instances.InstanceConfigurationMessage,
): Promise<void> => {
await request(`${url}/management/instances`, {
method: "post",
token,
await request(`/management/instances`, {
method: "POST",
data: instance,
});
@ -52,18 +48,16 @@ export function useAdminAPI(): AdminAPI {
};
const deleteInstance = async (id: string): Promise<void> => {
await request(`${url}/management/instances/${id}`, {
method: "delete",
token,
await request(`/management/instances/${id}`, {
method: "DELETE",
});
mutateAll(/\/management\/instances/);
};
const purgeInstance = async (id: string): Promise<void> => {
await request(`${url}/management/instances/${id}`, {
method: "delete",
token,
await request(`/management/instances/${id}`, {
method: "DELETE",
params: {
purge: "YES",
},
@ -85,14 +79,14 @@ export interface AdminAPI {
export function useManagementAPI(instanceId: string): InstanceAPI {
const mutateAll = useMatchMutate();
const { url, token, updateLoginStatus } = useBackendContext();
const { updateToken } = useBackendContext();
const { request } = useBackendBaseRequest();
const updateInstance = async (
instance: MerchantBackend.Instances.InstanceReconfigurationMessage,
): Promise<void> => {
await request(`${url}/management/instances/${instanceId}`, {
method: "patch",
token,
await request(`/management/instances/${instanceId}`, {
method: "PATCH",
data: instance,
});
@ -100,18 +94,16 @@ export function useManagementAPI(instanceId: string): InstanceAPI {
};
const deleteInstance = async (): Promise<void> => {
await request(`${url}/management/instances/${instanceId}`, {
method: "delete",
token,
await request(`/management/instances/${instanceId}`, {
method: "DELETE",
});
mutateAll(/\/management\/instances/);
};
const clearToken = async (): Promise<void> => {
await request(`${url}/management/instances/${instanceId}/auth`, {
method: "post",
token,
await request(`/management/instances/${instanceId}/auth`, {
method: "POST",
data: { method: "external" },
});
@ -119,13 +111,12 @@ export function useManagementAPI(instanceId: string): InstanceAPI {
};
const setNewToken = async (newToken: string): Promise<void> => {
await request(`${url}/management/instances/${instanceId}/auth`, {
method: "post",
token,
await request(`/management/instances/${instanceId}/auth`, {
method: "POST",
data: { method: "token", token: newToken },
});
updateLoginStatus(url, newToken);
updateToken(newToken);
mutateAll(/\/management\/instances/);
};
@ -139,71 +130,59 @@ export function useInstanceAPI(): InstanceAPI {
token: adminToken,
updateLoginStatus,
} = useBackendContext();
const { token: instanceToken, id, admin } = useInstanceContext();
const { url, token } = !admin
? { url: baseUrl, token: adminToken }
: { url: `${baseUrl}/instances/${id}`, token: instanceToken };
const { request } = useBackendInstanceRequest();
const updateInstance = async (
instance: MerchantBackend.Instances.InstanceReconfigurationMessage,
): Promise<void> => {
await request(`${url}/private/`, {
method: "patch",
token,
await request(`/private/`, {
method: "PATCH",
data: instance,
});
if (adminToken) mutate(["/private/instances", adminToken, baseUrl], null);
mutate([`/private/`, token, url], null);
mutate([`/private/`], null);
};
const deleteInstance = async (): Promise<void> => {
await request(`${url}/private/`, {
method: "delete",
token: adminToken,
await request(`/private/`, {
method: "DELETE",
// token: adminToken,
});
if (adminToken) mutate(["/private/instances", adminToken, baseUrl], null);
mutate([`/private/`, token, url], null);
mutate([`/private/`], null);
};
const clearToken = async (): Promise<void> => {
await request(`${url}/private/auth`, {
method: "post",
token,
await request(`/private/auth`, {
method: "POST",
data: { method: "external" },
});
mutate([`/private/`, token, url], null);
mutate([`/private/`], null);
};
const setNewToken = async (newToken: string): Promise<void> => {
await request(`${url}/private/auth`, {
method: "post",
token,
await request(`/private/auth`, {
method: "POST",
data: { method: "token", token: newToken },
});
updateLoginStatus(baseUrl, newToken);
mutate([`/private/`, token, url], null);
mutate([`/private/`], null);
};
return { updateInstance, deleteInstance, setNewToken, clearToken };
}
export function useInstanceDetails(): HttpResponse<MerchantBackend.Instances.QueryInstancesResponse> {
const { url: baseUrl, token: baseToken } = useBackendContext();
const { token: instanceToken, id, admin } = useInstanceContext();
const { url, token } = !admin
? { url: baseUrl, token: baseToken }
: { url: `${baseUrl}/instances/${id}`, token: instanceToken };
const { fetcher } = useBackendInstanceRequest();
const { data, error, isValidating } = useSWR<
HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>,
HttpError
>([`/private/`, token, url], fetcher, {
>([`/private/`], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
@ -225,17 +204,12 @@ type KYCStatus =
| { type: "redirect"; status: MerchantBackend.Instances.AccountKycRedirects };
export function useInstanceKYCDetails(): HttpResponse<KYCStatus> {
const { url: baseUrl, token: baseToken } = useBackendContext();
const { token: instanceToken, id, admin } = useInstanceContext();
const { url, token } = !admin
? { url: baseUrl, token: baseToken }
: { url: `${baseUrl}/instances/${id}`, token: instanceToken };
const { fetcher } = useBackendInstanceRequest();
const { data, error } = useSWR<
HttpResponseOk<MerchantBackend.Instances.AccountKycRedirects>,
HttpError
>([`/private/kyc`, token, url], fetcher, {
>([`/private/kyc`], fetcher, {
refreshInterval: 5000,
refreshWhenHidden: false,
revalidateOnFocus: false,
@ -258,12 +232,12 @@ export function useInstanceKYCDetails(): HttpResponse<KYCStatus> {
export function useManagedInstanceDetails(
instanceId: string,
): HttpResponse<MerchantBackend.Instances.QueryInstancesResponse> {
const { url, token } = useBackendContext();
const { request } = useBackendBaseRequest();
const { data, error, isValidating } = useSWR<
HttpResponseOk<MerchantBackend.Instances.QueryInstancesResponse>,
HttpError
>([`/management/instances/${instanceId}`, token, url], fetcher, {
>([`/management/instances/${instanceId}`], request, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
@ -281,13 +255,12 @@ export function useManagedInstanceDetails(
}
export function useBackendInstances(): HttpResponse<MerchantBackend.Instances.InstancesResponse> {
const { url } = useBackendContext();
const { token } = useInstanceContext();
const { request } = useBackendBaseRequest();
const { data, error, isValidating } = useSWR<
HttpResponseOk<MerchantBackend.Instances.InstancesResponse>,
HttpError
>(["/management/instances", token, url], fetcher);
>(["/management/instances"], request);
if (isValidating) return { loading: true, data: data?.data };
if (data) return data;

View File

@ -0,0 +1,579 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { tests } from "@gnu-taler/web-util/lib/index.browser";
import { expect } from "chai";
import { MerchantBackend } from "../declaration.js";
import { useInstanceOrders, useOrderAPI, useOrderDetails } from "./order.js";
import { ApiMockEnvironment } from "./testing.js";
import {
API_CREATE_ORDER,
API_DELETE_ORDER,
API_FORGET_ORDER_BY_ID,
API_GET_ORDER_BY_ID,
API_LIST_ORDERS, API_REFUND_ORDER_BY_ID
} from "./urls.js";
describe("order api interaction with listing", () => {
it("should evict cache when creating an order", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: 0, paid: "yes" },
response: {
orders: [{ order_id: "1" } as MerchantBackend.Orders.OrderHistoryEntry],
},
});
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: -20, paid: "yes" },
response: {
orders: [{ order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry],
},
});
const newDate = (d: Date) => {
//console.log("new date", d);
};
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const query = useInstanceOrders({ paid: "yes" }, newDate);
const api = useOrderAPI();
return { query, api };
},
{},
[
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).undefined;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
orders: [{ order_id: "1" }, { order_id: "2" }],
});
env.addRequestExpectation(API_CREATE_ORDER, {
request: {
order: { amount: "ARS:12", summary: "pay me" },
},
response: { order_id: "3" },
});
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: 0, paid: "yes" },
response: {
orders: [{ order_id: "1" } as any],
},
});
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: -20, paid: "yes" },
response: {
orders: [{ order_id: "2" } as any, { order_id: "3" } as any],
},
});
api.createOrder({
order: { amount: "ARS:12", summary: "pay me" },
} as any);
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).undefined;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
orders: [{ order_id: "1" }, { order_id: "2" }, { order_id: "3" }],
});
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
});
it("should evict cache when doing a refund", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: 0, paid: "yes" },
response: {
orders: [{ order_id: "1", amount: 'EUR:12', refundable: true } as MerchantBackend.Orders.OrderHistoryEntry],
},
});
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: -20, paid: "yes" },
response: { orders: [], },
});
const newDate = (d: Date) => {
//console.log("new date", d);
};
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const query = useInstanceOrders({ paid: "yes" }, newDate);
const api = useOrderAPI();
return { query, api };
},
{},
[
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).undefined;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
orders: [{
order_id: "1",
amount: 'EUR:12',
refundable: true,
}],
});
env.addRequestExpectation(API_REFUND_ORDER_BY_ID('1'), {
request: {
reason: 'double pay',
refund: 'EUR:1'
},
});
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: 0, paid: "yes" },
response: {
orders: [{ order_id: "1", amount: 'EUR:12', refundable: false } as any],
},
});
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: -20, paid: "yes" },
response: { orders: [], },
});
api.refundOrder('1', {
reason: 'double pay',
refund: 'EUR:1'
});
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).undefined;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
orders: [{
order_id: "1",
amount: 'EUR:12',
refundable: false,
}],
});
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
});
it("should evict cache when deleting an order", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: 0, paid: "yes" },
response: {
orders: [{ order_id: "1" } as MerchantBackend.Orders.OrderHistoryEntry],
},
});
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: -20, paid: "yes" },
response: {
orders: [{ order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry],
},
});
const newDate = (d: Date) => {
//console.log("new date", d);
};
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const query = useInstanceOrders({ paid: "yes" }, newDate);
const api = useOrderAPI();
return { query, api };
},
{},
[
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).undefined;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
orders: [{ order_id: "1" }, { order_id: "2" }],
});
env.addRequestExpectation(API_DELETE_ORDER('1'), {});
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: 0, paid: "yes" },
response: {
orders: [],
},
});
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: -20, paid: "yes" },
response: {
orders: [{ order_id: "2" } as any],
},
});
api.deleteOrder('1');
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).undefined;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
orders: [{ order_id: "2" }],
});
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
});
});
describe("order api interaction with details", () => {
it("should evict cache when doing a refund", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_GET_ORDER_BY_ID('1'), {
// qparam: { delta: 0, paid: "yes" },
response: {
summary: 'description',
refund_amount: 'EUR:0',
} as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse,
});
const newDate = (d: Date) => {
//console.log("new date", d);
};
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const query = useOrderDetails('1')
const api = useOrderAPI();
return { query, api };
},
{},
[
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
summary: 'description',
refund_amount: 'EUR:0',
});
env.addRequestExpectation(API_REFUND_ORDER_BY_ID('1'), {
request: {
reason: 'double pay',
refund: 'EUR:1'
},
});
env.addRequestExpectation(API_GET_ORDER_BY_ID('1'), {
response: {
summary: 'description',
refund_amount: 'EUR:1',
} as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse,
});
api.refundOrder('1', {
reason: 'double pay',
refund: 'EUR:1'
})
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
summary: 'description',
refund_amount: 'EUR:1',
});
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
})
it("should evict cache when doing a forget", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_GET_ORDER_BY_ID('1'), {
// qparam: { delta: 0, paid: "yes" },
response: {
summary: 'description',
refund_amount: 'EUR:0',
} as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse,
});
const newDate = (d: Date) => {
//console.log("new date", d);
};
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const query = useOrderDetails('1')
const api = useOrderAPI();
return { query, api };
},
{},
[
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
summary: 'description',
refund_amount: 'EUR:0',
});
env.addRequestExpectation(API_FORGET_ORDER_BY_ID('1'), {
request: {
fields: ['$.summary']
},
});
env.addRequestExpectation(API_GET_ORDER_BY_ID('1'), {
response: {
summary: undefined,
} as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse,
});
api.forgetOrder('1', {
fields: ['$.summary']
})
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
summary: undefined,
});
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
})
})
describe("order listing pagination", () => {
it("should not load more if has reach the end", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: 20, wired: "yes", date_ms: 12 },
response: {
orders: [{ order_id: "1" } as any],
},
});
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: -20, wired: "yes", date_ms: 13 },
response: {
orders: [{ order_id: "2" } as any],
},
});
const newDate = (d: Date) => {
//console.log("new date", d);
};
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const date = new Date(12);
const query = useInstanceOrders({ wired: "yes", date }, newDate)
const api = useOrderAPI();
return { query, api };
},
{},
[
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).undefined;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
orders: [{ order_id: "1" }, { order_id: "2" }],
});
expect(query.isReachingEnd).true
expect(query.isReachingStart).true
// should not trigger new state update or query
query.loadMore()
query.loadMorePrev();
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
});
it("should load more if result brings more that PAGE_SIZE", async () => {
const env = new ApiMockEnvironment();
const ordersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({ order_id: String(i) }))
const ordersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({ order_id: String(i + 20) }))
const ordersFrom20to0 = [...ordersFrom0to20].reverse()
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: 20, wired: "yes", date_ms: 12 },
response: {
orders: ordersFrom0to20,
},
});
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: -20, wired: "yes", date_ms: 13 },
response: {
orders: ordersFrom20to40,
},
});
const newDate = (d: Date) => {
//console.log("new date", d);
};
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const date = new Date(12);
const query = useInstanceOrders({ wired: "yes", date }, newDate)
const api = useOrderAPI();
return { query, api };
},
{},
[
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).undefined;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
orders: [...ordersFrom20to0, ...ordersFrom20to40],
});
expect(query.isReachingEnd).false
expect(query.isReachingStart).false
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: -40, wired: "yes", date_ms: 13 },
response: {
orders: [...ordersFrom20to40, { order_id: '41' }],
},
});
query.loadMore()
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).true;
},
({ query, api }) => {
expect(query.loading).undefined;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
orders: [...ordersFrom20to0, ...ordersFrom20to40, { order_id: '41' }],
});
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: 40, wired: "yes", date_ms: 12 },
response: {
orders: [...ordersFrom0to20, { order_id: '-1' }],
},
});
query.loadMorePrev()
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).true;
},
({ query, api }) => {
expect(query.loading).undefined;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
orders: [{ order_id: '-1' }, ...ordersFrom20to0, ...ordersFrom20to40, { order_id: '41' }],
});
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
});
});

View File

@ -14,20 +14,16 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { useEffect, useState } from "preact/hooks";
import useSWR, { useSWRConfig } from "swr";
import { useBackendContext } from "../context/backend.js";
import { useInstanceContext } from "../context/instance.js";
import useSWR from "swr";
import { MerchantBackend } from "../declaration.js";
import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
import {
fetcher,
HttpError,
HttpResponse,
HttpResponseOk,
HttpResponsePaginated,
request,
useMatchMutate,
} from "./backend.js";
} from "../utils/request.js";
import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
export interface OrderAPI {
//FIXME: add OutOfStockResponse on 410
@ -48,52 +44,17 @@ export interface OrderAPI {
type YesOrNo = "yes" | "no";
export function orderFetcher<T>(
url: string,
token: string,
backend: string,
paid?: YesOrNo,
refunded?: YesOrNo,
wired?: YesOrNo,
searchDate?: Date,
delta?: number,
): Promise<HttpResponseOk<T>> {
const date_ms =
delta && delta < 0 && searchDate
? searchDate.getTime() + 1
: searchDate?.getTime();
const params: any = {};
if (paid !== undefined) params.paid = paid;
if (delta !== undefined) params.delta = delta;
if (refunded !== undefined) params.refunded = refunded;
if (wired !== undefined) params.wired = wired;
if (date_ms !== undefined) params.date_ms = date_ms;
return request<T>(`${backend}${url}`, { token, params });
}
export function useOrderAPI(): OrderAPI {
const mutateAll = useMatchMutate();
const { url: baseUrl, token: adminToken } = useBackendContext();
const { token: instanceToken, id, admin } = useInstanceContext();
const { url, token } = !admin
? {
url: baseUrl,
token: adminToken,
}
: {
url: `${baseUrl}/instances/${id}`,
token: instanceToken,
};
const { request } = useBackendInstanceRequest();
const createOrder = async (
data: MerchantBackend.Orders.PostOrderRequest,
): Promise<HttpResponseOk<MerchantBackend.Orders.PostOrderResponse>> => {
const res = await request<MerchantBackend.Orders.PostOrderResponse>(
`${url}/private/orders`,
`/private/orders`,
{
method: "post",
token,
method: "POST",
data,
},
);
@ -107,10 +68,9 @@ export function useOrderAPI(): OrderAPI {
): Promise<HttpResponseOk<MerchantBackend.Orders.MerchantRefundResponse>> => {
mutateAll(/@"\/private\/orders"@/);
const res = request<MerchantBackend.Orders.MerchantRefundResponse>(
`${url}/private/orders/${orderId}/refund`,
`/private/orders/${orderId}/refund`,
{
method: "post",
token,
method: "POST",
data,
},
);
@ -125,9 +85,8 @@ export function useOrderAPI(): OrderAPI {
data: MerchantBackend.Orders.ForgetRequest,
): Promise<HttpResponseOk<void>> => {
mutateAll(/@"\/private\/orders"@/);
const res = request<void>(`${url}/private/orders/${orderId}/forget`, {
method: "patch",
token,
const res = request<void>(`/private/orders/${orderId}/forget`, {
method: "PATCH",
data,
});
// we may be forgetting some fields that are pare of the listing, so we must evict everything
@ -138,9 +97,8 @@ export function useOrderAPI(): OrderAPI {
orderId: string,
): Promise<HttpResponseOk<void>> => {
mutateAll(/@"\/private\/orders"@/);
const res = request<void>(`${url}/private/orders/${orderId}`, {
method: "delete",
token,
const res = request<void>(`/private/orders/${orderId}`, {
method: "DELETE",
});
await mutateAll(/.*private\/orders.*/);
return res;
@ -150,10 +108,9 @@ export function useOrderAPI(): OrderAPI {
orderId: string,
): Promise<HttpResponseOk<string>> => {
return request<MerchantBackend.Orders.MerchantOrderStatusResponse>(
`${url}/private/orders/${orderId}`,
`/private/orders/${orderId}`,
{
method: "get",
token,
method: "GET",
},
).then((res) => {
const url =
@ -172,17 +129,12 @@ export function useOrderAPI(): OrderAPI {
export function useOrderDetails(
oderId: string,
): HttpResponse<MerchantBackend.Orders.MerchantOrderStatusResponse> {
const { url: baseUrl, token: baseToken } = useBackendContext();
const { token: instanceToken, id, admin } = useInstanceContext();
const { url, token } = !admin
? { url: baseUrl, token: baseToken }
: { url: `${baseUrl}/instances/${id}`, token: instanceToken };
const { fetcher } = useBackendInstanceRequest();
const { data, error, isValidating } = useSWR<
HttpResponseOk<MerchantBackend.Orders.MerchantOrderStatusResponse>,
HttpError
>([`/private/orders/${oderId}`, token, url], fetcher, {
>([`/private/orders/${oderId}`], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
@ -207,12 +159,7 @@ export function useInstanceOrders(
args?: InstanceOrderFilter,
updateFilter?: (d: Date) => void,
): HttpResponsePaginated<MerchantBackend.Orders.OrderHistory> {
const { url: baseUrl, token: baseToken } = useBackendContext();
const { token: instanceToken, id, admin } = useInstanceContext();
const { url, token } = !admin
? { url: baseUrl, token: baseToken }
: { url: `${baseUrl}/instances/${id}`, token: instanceToken };
const { orderFetcher } = useBackendInstanceRequest();
const [pageBefore, setPageBefore] = useState(1);
const [pageAfter, setPageAfter] = useState(1);
@ -233,8 +180,6 @@ export function useInstanceOrders(
} = useSWR<HttpResponseOk<MerchantBackend.Orders.OrderHistory>, HttpError>(
[
`/private/orders`,
token,
url,
args?.paid,
args?.refunded,
args?.wired,
@ -250,8 +195,6 @@ export function useInstanceOrders(
} = useSWR<HttpResponseOk<MerchantBackend.Orders.OrderHistory>, HttpError>(
[
`/private/orders`,
token,
url,
args?.paid,
args?.refunded,
args?.wired,
@ -314,9 +257,9 @@ export function useInstanceOrders(
!beforeData || !afterData
? []
: (beforeData || lastBefore).data.orders
.slice()
.reverse()
.concat((afterData || lastAfter).data.orders);
.slice()
.reverse()
.concat((afterData || lastAfter).data.orders);
if (loadingAfter || loadingBefore) return { loading: true, data: { orders } };
if (beforeData && afterData) {
return { ok: true, data: { orders }, ...pagination };

View File

@ -0,0 +1,326 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { tests } from "@gnu-taler/web-util/lib/index.browser";
import { expect } from "chai";
import { MerchantBackend } from "../declaration.js";
import { useInstanceProducts, useProductAPI, useProductDetails } from "./product.js";
import { ApiMockEnvironment } from "./testing.js";
import {
API_CREATE_PRODUCT,
API_DELETE_PRODUCT, API_GET_PRODUCT_BY_ID,
API_LIST_PRODUCTS,
API_UPDATE_PRODUCT_BY_ID
} from "./urls.js";
describe("product api interaction with listing", () => {
it("should evict cache when creating a product", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_LIST_PRODUCTS, {
response: {
products: [{ product_id: "1234" }],
},
});
env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail,
});
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const query = useInstanceProducts();
const api = useProductAPI();
return { query, api };
},
{},
[
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).true;
},
({ query, api }) => {
expect(query.loading).undefined;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals([
{ id: "1234", price: "ARS:12" },
]);
env.addRequestExpectation(API_CREATE_PRODUCT, {
request: { price: "ARS:23" } as MerchantBackend.Products.ProductAddDetail,
});
env.addRequestExpectation(API_LIST_PRODUCTS, {
response: {
products: [{ product_id: "1234" }, { product_id: "2345" }],
},
});
env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail,
});
env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail,
});
env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), {
response: { price: "ARS:23" } as MerchantBackend.Products.ProductDetail,
});
api.createProduct({
price: "ARS:23",
} as any)
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).undefined;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals([
{
id: "1234",
price: "ARS:12",
},
{
id: "2345",
price: "ARS:23",
},
]);
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
});
it("should evict cache when updating a product", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_LIST_PRODUCTS, {
response: {
products: [{ product_id: "1234" }],
},
});
env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail,
});
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const query = useInstanceProducts();
const api = useProductAPI();
return { query, api };
},
{},
[
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).undefined;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals([
{ id: "1234", price: "ARS:12" },
]);
env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("1234"), {
request: { price: "ARS:13" } as MerchantBackend.Products.ProductPatchDetail,
});
env.addRequestExpectation(API_LIST_PRODUCTS, {
response: {
products: [{ product_id: "1234" }],
},
});
env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
response: { price: "ARS:13" } as MerchantBackend.Products.ProductDetail,
});
api.updateProduct("1234", {
price: "ARS:13",
} as any)
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).undefined;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals([
{
id: "1234",
price: "ARS:13",
},
]);
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
});
it("should evict cache when deleting a product", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_LIST_PRODUCTS, {
response: {
products: [{ product_id: "1234" }, { product_id: "2345" }],
},
});
env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail,
});
env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), {
response: { price: "ARS:23" } as MerchantBackend.Products.ProductDetail,
});
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const query = useInstanceProducts();
const api = useProductAPI();
return { query, api };
},
{},
[
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).true;
},
({ query, api }) => {
expect(query.loading).undefined;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals([
{ id: "1234", price: "ARS:12" },
{ id: "2345", price: "ARS:23" },
]);
env.addRequestExpectation(API_DELETE_PRODUCT("2345"), {});
env.addRequestExpectation(API_LIST_PRODUCTS, {
response: {
products: [{ product_id: "1234" }],
},
});
env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail,
});
api.deleteProduct("2345");
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).undefined;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals([
{ id: "1234", price: "ARS:12" },
]);
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
});
});
describe("product api interaction with details", () => {
it("should evict cache when updating a product", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), {
response: {
description: "this is a description",
} as MerchantBackend.Products.ProductDetail,
});
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const query = useProductDetails("12");
const api = useProductAPI();
return { query, api };
},
{},
[
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
description: "this is a description",
});
env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("12"), {
request: { description: "other description" } as MerchantBackend.Products.ProductPatchDetail,
});
env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), {
response: {
description: "other description",
} as MerchantBackend.Products.ProductDetail,
});
api.updateProduct("12", {
description: "other description",
} as any);
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
description: "other description",
});
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
})
})

View File

@ -14,18 +14,9 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import useSWR, { useSWRConfig } from "swr";
import { useBackendContext } from "../context/backend.js";
import { useInstanceContext } from "../context/instance.js";
import { MerchantBackend, WithId } from "../declaration.js";
import {
fetcher,
HttpError,
HttpResponse,
HttpResponseOk,
multiFetcher,
request,
useMatchMutate,
} from "./backend.js";
import { HttpError, HttpResponse, HttpResponseOk } from "../utils/request.js";
import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
export interface ProductAPI {
createProduct: (
@ -45,19 +36,14 @@ export interface ProductAPI {
export function useProductAPI(): ProductAPI {
const mutateAll = useMatchMutate();
const { mutate } = useSWRConfig();
const { url: baseUrl, token: adminToken } = useBackendContext();
const { token: instanceToken, id, admin } = useInstanceContext();
const { url, token } = !admin
? { url: baseUrl, token: adminToken }
: { url: `${baseUrl}/instances/${id}`, token: instanceToken };
const { request } = useBackendInstanceRequest();
const createProduct = async (
data: MerchantBackend.Products.ProductAddDetail,
): Promise<void> => {
const res = await request(`${url}/private/products`, {
method: "post",
token,
const res = await request(`/private/products`, {
method: "POST",
data,
});
@ -68,9 +54,8 @@ export function useProductAPI(): ProductAPI {
productId: string,
data: MerchantBackend.Products.ProductPatchDetail,
): Promise<void> => {
const r = await request(`${url}/private/products/${productId}`, {
method: "patch",
token,
const r = await request(`/private/products/${productId}`, {
method: "PATCH",
data,
});
@ -78,20 +63,18 @@ export function useProductAPI(): ProductAPI {
};
const deleteProduct = async (productId: string): Promise<void> => {
await request(`${url}/private/products/${productId}`, {
method: "delete",
token,
await request(`/private/products/${productId}`, {
method: "DELETE",
});
await mutate([`/private/products`, token, url]);
await mutate([`/private/products`]);
};
const lockProduct = async (
productId: string,
data: MerchantBackend.Products.LockRequest,
): Promise<void> => {
await request(`${url}/private/products/${productId}/lock`, {
method: "post",
token,
await request(`/private/products/${productId}/lock`, {
method: "POST",
data,
});
@ -104,17 +87,12 @@ export function useProductAPI(): ProductAPI {
export function useInstanceProducts(): HttpResponse<
(MerchantBackend.Products.ProductDetail & WithId)[]
> {
const { url: baseUrl, token: baseToken } = useBackendContext();
const { token: instanceToken, id, admin } = useInstanceContext();
const { url, token } = !admin
? { url: baseUrl, token: baseToken }
: { url: `${baseUrl}/instances/${id}`, token: instanceToken };
const { fetcher, multiFetcher } = useBackendInstanceRequest();
const { data: list, error: listError } = useSWR<
HttpResponseOk<MerchantBackend.Products.InventorySummaryResponse>,
HttpError
>([`/private/products`, token, url], fetcher, {
>([`/private/products`], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
@ -128,7 +106,7 @@ export function useInstanceProducts(): HttpResponse<
const { data: products, error: productError } = useSWR<
HttpResponseOk<MerchantBackend.Products.ProductDetail>[],
HttpError
>([paths, token, url], multiFetcher, {
>([paths], multiFetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
@ -144,7 +122,7 @@ export function useInstanceProducts(): HttpResponse<
//take the id from the queried url
return {
...d.data,
id: d.info?.url.replace(/.*\/private\/products\//, "") || "",
id: d.info?.url.href.replace(/.*\/private\/products\//, "") || "",
};
});
return { ok: true, data: dataWithId };
@ -155,23 +133,12 @@ export function useInstanceProducts(): HttpResponse<
export function useProductDetails(
productId: string,
): HttpResponse<MerchantBackend.Products.ProductDetail> {
const { url: baseUrl, token: baseToken } = useBackendContext();
const { token: instanceToken, id, admin } = useInstanceContext();
const { url, token } = !admin
? {
url: baseUrl,
token: baseToken,
}
: {
url: `${baseUrl}/instances/${id}`,
token: instanceToken,
};
const { fetcher } = useBackendInstanceRequest();
const { data, error, isValidating } = useSWR<
HttpResponseOk<MerchantBackend.Products.ProductDetail>,
HttpError
>([`/private/products/${productId}`, token, url], fetcher, {
>([`/private/products/${productId}`], fetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,

View File

@ -0,0 +1,431 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { expect } from "chai";
import { MerchantBackend } from "../declaration.js";
import {
useInstanceReserves,
useReserveDetails,
useReservesAPI,
useTipDetails
} from "./reserves.js";
import { ApiMockEnvironment } from "./testing.js";
import {
API_AUTHORIZE_TIP,
API_AUTHORIZE_TIP_FOR_RESERVE,
API_CREATE_RESERVE,
API_DELETE_RESERVE,
API_GET_RESERVE_BY_ID,
API_GET_TIP_BY_ID,
API_LIST_RESERVES
} from "./urls.js";
import { tests } from "@gnu-taler/web-util/lib/index.browser";
describe("reserve api interaction with listing", () => {
it("should evict cache when creating a reserve", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_LIST_RESERVES, {
response: {
reserves: [
{
reserve_pub: "11",
} as MerchantBackend.Tips.ReserveStatusEntry,
],
},
});
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const api = useReservesAPI();
const query = useInstanceReserves();
return { query, api };
},
{},
[
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
reserves: [{ reserve_pub: "11" }],
});
env.addRequestExpectation(API_CREATE_RESERVE, {
request: {
initial_balance: "ARS:3333",
exchange_url: "http://url",
wire_method: "iban",
},
response: {
reserve_pub: "22",
payto_uri: "payto",
},
});
env.addRequestExpectation(API_LIST_RESERVES, {
response: {
reserves: [
{
reserve_pub: "11",
} as MerchantBackend.Tips.ReserveStatusEntry,
{
reserve_pub: "22",
} as MerchantBackend.Tips.ReserveStatusEntry,
],
},
});
api.createReserve({
initial_balance: "ARS:3333",
exchange_url: "http://url",
wire_method: "iban",
})
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true;
if (!query.ok) return;
expect(query.data).deep.equals({
reserves: [
{
reserve_pub: "11",
} as MerchantBackend.Tips.ReserveStatusEntry,
{
reserve_pub: "22",
} as MerchantBackend.Tips.ReserveStatusEntry,
],
});
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
});
it("should evict cache when deleting a reserve", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_LIST_RESERVES, {
response: {
reserves: [
{
reserve_pub: "11",
} as MerchantBackend.Tips.ReserveStatusEntry,
{
reserve_pub: "22",
} as MerchantBackend.Tips.ReserveStatusEntry,
{
reserve_pub: "33",
} as MerchantBackend.Tips.ReserveStatusEntry,
],
},
});
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const api = useReservesAPI();
const query = useInstanceReserves();
return { query, api };
},
{},
[
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" })
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
reserves: [
{ reserve_pub: "11" },
{ reserve_pub: "22" },
{ reserve_pub: "33" },
],
});
env.addRequestExpectation(API_DELETE_RESERVE("11"), {});
env.addRequestExpectation(API_LIST_RESERVES, {
response: {
reserves: [
{
reserve_pub: "22",
} as MerchantBackend.Tips.ReserveStatusEntry,
{
reserve_pub: "33",
} as MerchantBackend.Tips.ReserveStatusEntry,
],
},
});
api.deleteReserve("11")
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" })
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
reserves: [
{ reserve_pub: "22" },
{ reserve_pub: "33" },
],
});
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
});
});
describe("reserve api interaction with details", () => {
it("should evict cache when adding a tip for a specific reserve", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
response: {
payto_uri: "payto://here",
tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }],
} as MerchantBackend.Tips.ReserveDetail,
qparam: {
tips: "yes"
}
});
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const api = useReservesAPI();
const query = useReserveDetails("11");
return { query, api };
},
{},
[
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
payto_uri: "payto://here",
tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }],
});
env.addRequestExpectation(API_AUTHORIZE_TIP_FOR_RESERVE("11"), {
request: {
amount: "USD:12",
justification: "not",
next_url: "http://taler.net",
},
response: {
tip_id: "id2",
taler_tip_uri: "uri",
tip_expiration: { t_s: 1 },
tip_status_url: "url",
}
},);
env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
response: {
payto_uri: "payto://here",
tips: [
{ reason: "why?", tip_id: "id1", total_amount: "USD:10" },
{ reason: "not", tip_id: "id2", total_amount: "USD:12" },
],
} as MerchantBackend.Tips.ReserveDetail,
qparam: {
tips: "yes"
}
});
api.authorizeTipReserve("11", {
amount: "USD:12",
justification: "not",
next_url: "http://taler.net",
})
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.loading).false;
expect(query.ok).true;
if (!query.ok) return;
expect(query.data).deep.equals({
payto_uri: "payto://here",
tips: [
{ reason: "why?", tip_id: "id1", total_amount: "USD:10" },
{ reason: "not", tip_id: "id2", total_amount: "USD:12" },
],
});
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
});
it("should evict cache when adding a tip for a random reserve", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
response: {
payto_uri: "payto://here",
tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }],
} as MerchantBackend.Tips.ReserveDetail,
qparam: {
tips: "yes"
}
});
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const api = useReservesAPI();
const query = useReserveDetails("11");
return { query, api };
},
{},
[
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
payto_uri: "payto://here",
tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }],
});
env.addRequestExpectation(API_AUTHORIZE_TIP, {
request: {
amount: "USD:12",
justification: "not",
next_url: "http://taler.net",
},
response: {
tip_id: "id2",
taler_tip_uri: "uri",
tip_expiration: { t_s: 1 },
tip_status_url: "url",
},
});
env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
response: {
payto_uri: "payto://here",
tips: [
{ reason: "why?", tip_id: "id1", total_amount: "USD:10" },
{ reason: "not", tip_id: "id2", total_amount: "USD:12" },
],
} as MerchantBackend.Tips.ReserveDetail,
qparam: {
tips: "yes"
}
});
api.authorizeTip({
amount: "USD:12",
justification: "not",
next_url: "http://taler.net",
});
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).false;
expect(query.ok).true;
if (!query.ok) return;
expect(query.data).deep.equals({
payto_uri: "payto://here",
tips: [
{ reason: "why?", tip_id: "id1", total_amount: "USD:10" },
{ reason: "not", tip_id: "id2", total_amount: "USD:12" },
],
});
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
});
});
describe("reserve api interaction with tip details", () => {
it("should list tips", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_GET_TIP_BY_ID("11"), {
response: {
total_picked_up: "USD:12",
reason: "not",
} as MerchantBackend.Tips.TipDetails,
qparam: {
pickups: "yes"
}
});
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const query = useTipDetails("11");
return { query };
},
{},
[
({ query }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(query.loading).true;
},
({ query }) => {
expect(query.loading).false;
expect(query.ok).true
if (!query.ok) return;
expect(query.data).deep.equals({
total_picked_up: "USD:12",
reason: "not",
});
},
], env.buildTestingContext());
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
});
});

View File

@ -14,27 +14,14 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import useSWR, { useSWRConfig } from "swr";
import { useBackendContext } from "../context/backend.js";
import { useInstanceContext } from "../context/instance.js";
import { MerchantBackend } from "../declaration.js";
import {
fetcher,
HttpError,
HttpResponse,
HttpResponseOk,
request,
useMatchMutate,
} from "./backend.js";
import { HttpError, HttpResponse, HttpResponseOk } from "../utils/request.js";
import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
export function useReservesAPI(): ReserveMutateAPI {
const mutateAll = useMatchMutate();
const { mutate } = useSWRConfig();
const { url: baseUrl, token: adminToken } = useBackendContext();
const { token: instanceToken, id, admin } = useInstanceContext();
const { url, token } = !admin
? { url: baseUrl, token: adminToken }
: { url: `${baseUrl}/instances/${id}`, token: instanceToken };
const { request } = useBackendInstanceRequest();
const createReserve = async (
data: MerchantBackend.Tips.ReserveCreateRequest,
@ -42,10 +29,9 @@ export function useReservesAPI(): ReserveMutateAPI {
HttpResponseOk<MerchantBackend.Tips.ReserveCreateConfirmation>
> => {
const res = await request<MerchantBackend.Tips.ReserveCreateConfirmation>(
`${url}/private/reserves`,
`/private/reserves`,
{
method: "post",
token,
method: "POST",
data,
},
);
@ -61,16 +47,15 @@ export function useReservesAPI(): ReserveMutateAPI {
data: MerchantBackend.Tips.TipCreateRequest,
): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => {
const res = await request<MerchantBackend.Tips.TipCreateConfirmation>(
`${url}/private/reserves/${pub}/authorize-tip`,
`/private/reserves/${pub}/authorize-tip`,
{
method: "post",
token,
method: "POST",
data,
},
);
//evict reserve details query
await mutate([`/private/reserves/${pub}`, token, url]);
await mutate([`/private/reserves/${pub}`]);
return res;
};
@ -79,10 +64,9 @@ export function useReservesAPI(): ReserveMutateAPI {
data: MerchantBackend.Tips.TipCreateRequest,
): Promise<HttpResponseOk<MerchantBackend.Tips.TipCreateConfirmation>> => {
const res = await request<MerchantBackend.Tips.TipCreateConfirmation>(
`${url}/private/tips`,
`/private/tips`,
{
method: "post",
token,
method: "POST",
data,
},
);
@ -94,9 +78,8 @@ export function useReservesAPI(): ReserveMutateAPI {
};
const deleteReserve = async (pub: string): Promise<HttpResponse<void>> => {
const res = await request<void>(`${url}/private/reserves/${pub}`, {
method: "delete",
token,
const res = await request<void>(`/private/reserves/${pub}`, {
method: "DELETE",
});
//evict reserve list query
@ -123,17 +106,12 @@ export interface ReserveMutateAPI {
}
export function useInstanceReserves(): HttpResponse<MerchantBackend.Tips.TippingReserveStatus> {
const { url: baseUrl, token: baseToken } = useBackendContext();
const { token: instanceToken, id, admin } = useInstanceContext();
const { url, token } = !admin
? { url: baseUrl, token: baseToken }
: { url: `${baseUrl}/instances/${id}`, token: instanceToken };
const { fetcher } = useBackendInstanceRequest();
const { data, error, isValidating } = useSWR<
HttpResponseOk<MerchantBackend.Tips.TippingReserveStatus>,
HttpError
>([`/private/reserves`, token, url], fetcher);
>([`/private/reserves`], fetcher);
if (isValidating) return { loading: true, data: data?.data };
if (data) return data;
@ -144,15 +122,12 @@ export function useInstanceReserves(): HttpResponse<MerchantBackend.Tips.Tipping
export function useReserveDetails(
reserveId: string,
): HttpResponse<MerchantBackend.Tips.ReserveDetail> {
const { url: baseUrl } = useBackendContext();
const { token, id: instanceId, admin } = useInstanceContext();
const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}`;
const { reserveDetailFetcher } = useBackendInstanceRequest();
const { data, error, isValidating } = useSWR<
HttpResponseOk<MerchantBackend.Tips.ReserveDetail>,
HttpError
>([`/private/reserves/${reserveId}`, token, url], reserveDetailFetcher, {
>([`/private/reserves/${reserveId}`], reserveDetailFetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
@ -169,15 +144,12 @@ export function useReserveDetails(
export function useTipDetails(
tipId: string,
): HttpResponse<MerchantBackend.Tips.TipDetails> {
const { url: baseUrl } = useBackendContext();
const { token, id: instanceId, admin } = useInstanceContext();
const url = !admin ? baseUrl : `${baseUrl}/instances/${instanceId}`;
const { tipsDetailFetcher } = useBackendInstanceRequest();
const { data, error, isValidating } = useSWR<
HttpResponseOk<MerchantBackend.Tips.TipDetails>,
HttpError
>([`/private/tips/${tipId}`, token, url], tipsDetailFetcher, {
>([`/private/tips/${tipId}`], tipsDetailFetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,
@ -190,29 +162,3 @@ export function useTipDetails(
if (error) return error;
return { loading: true };
}
function reserveDetailFetcher<T>(
url: string,
token: string,
backend: string,
): Promise<HttpResponseOk<T>> {
return request<T>(`${backend}${url}`, {
token,
params: {
tips: "yes",
},
});
}
function tipsDetailFetcher<T>(
url: string,
token: string,
backend: string,
): Promise<HttpResponseOk<T>> {
return request<T>(`${backend}${url}`, {
token,
params: {
pickups: "yes",
},
});
}

View File

@ -14,57 +14,26 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { MerchantBackend } from "../declaration.js";
import { useBackendContext } from "../context/backend.js";
import {
request,
HttpResponse,
HttpError,
HttpResponseOk,
HttpResponsePaginated,
useMatchMutate,
} from "./backend.js";
import { useMatchMutate, useBackendInstanceRequest } from "./backend.js";
import useSWR from "swr";
import { useInstanceContext } from "../context/instance.js";
import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
import { useEffect, useState } from "preact/hooks";
async function templateFetcher<T>(
url: string,
token: string,
backend: string,
position?: string,
delta?: number,
): Promise<HttpResponseOk<T>> {
const params: any = {};
if (delta !== undefined) {
params.limit = delta;
}
if (position !== undefined) params.offset = position;
return request<T>(`${backend}${url}`, { token, params });
}
import {
HttpError,
HttpResponse,
HttpResponseOk,
HttpResponsePaginated,
} from "../utils/request.js";
export function useTemplateAPI(): TemplateAPI {
const mutateAll = useMatchMutate();
const { url: baseUrl, token: adminToken } = useBackendContext();
const { token: instanceToken, id, admin } = useInstanceContext();
const { url, token } = !admin
? {
url: baseUrl,
token: adminToken,
}
: {
url: `${baseUrl}/instances/${id}`,
token: instanceToken,
};
const { request } = useBackendInstanceRequest();
const createTemplate = async (
data: MerchantBackend.Template.TemplateAddDetails,
): Promise<HttpResponseOk<void>> => {
const res = await request<void>(`${url}/private/templates`, {
method: "post",
token,
const res = await request<void>(`/private/templates`, {
method: "POST",
data,
});
await mutateAll(/.*private\/templates.*/);
@ -75,9 +44,8 @@ export function useTemplateAPI(): TemplateAPI {
templateId: string,
data: MerchantBackend.Template.TemplatePatchDetails,
): Promise<HttpResponseOk<void>> => {
const res = await request<void>(`${url}/private/templates/${templateId}`, {
method: "patch",
token,
const res = await request<void>(`/private/templates/${templateId}`, {
method: "PATCH",
data,
});
await mutateAll(/.*private\/templates.*/);
@ -87,9 +55,8 @@ export function useTemplateAPI(): TemplateAPI {
const deleteTemplate = async (
templateId: string,
): Promise<HttpResponseOk<void>> => {
const res = await request<void>(`${url}/private/templates/${templateId}`, {
method: "delete",
token,
const res = await request<void>(`/private/templates/${templateId}`, {
method: "DELETE",
});
await mutateAll(/.*private\/templates.*/);
return res;
@ -102,10 +69,9 @@ export function useTemplateAPI(): TemplateAPI {
HttpResponseOk<MerchantBackend.Template.UsingTemplateResponse>
> => {
const res = await request<MerchantBackend.Template.UsingTemplateResponse>(
`${url}/private/templates/${templateId}`,
`/private/templates/${templateId}`,
{
method: "post",
token,
method: "POST",
data,
},
);
@ -140,12 +106,7 @@ export function useInstanceTemplates(
args?: InstanceTemplateFilter,
updatePosition?: (id: string) => void,
): HttpResponsePaginated<MerchantBackend.Template.TemplateSummaryResponse> {
const { url: baseUrl, token: baseToken } = useBackendContext();
const { token: instanceToken, id, admin } = useInstanceContext();
const { url, token } = !admin
? { url: baseUrl, token: baseToken }
: { url: `${baseUrl}/instances/${id}`, token: instanceToken };
const { templateFetcher } = useBackendInstanceRequest();
// const [pageBefore, setPageBefore] = useState(1);
const [pageAfter, setPageAfter] = useState(1);
@ -180,10 +141,7 @@ export function useInstanceTemplates(
} = useSWR<
HttpResponseOk<MerchantBackend.Template.TemplateSummaryResponse>,
HttpError
>(
[`/private/templates`, token, url, args?.position, -totalAfter],
templateFetcher,
);
>([`/private/templates`, args?.position, -totalAfter], templateFetcher);
//this will save last result
// const [lastBefore, setLastBefore] = useState<
@ -216,10 +174,9 @@ export function useInstanceTemplates(
if (afterData.data.templates.length < MAX_RESULT_SIZE) {
setPageAfter(pageAfter + 1);
} else {
const from = `${
afterData.data.templates[afterData.data.templates.length - 1]
.template_id
}`;
const from = `${afterData.data.templates[afterData.data.templates.length - 1]
.template_id
}`;
if (from && updatePosition) updatePosition(from);
}
},
@ -255,17 +212,12 @@ export function useInstanceTemplates(
export function useTemplateDetails(
templateId: string,
): HttpResponse<MerchantBackend.Template.TemplateDetails> {
const { url: baseUrl, token: baseToken } = useBackendContext();
const { token: instanceToken, id, admin } = useInstanceContext();
const { url, token } = !admin
? { url: baseUrl, token: baseToken }
: { url: `${baseUrl}/instances/${id}`, token: instanceToken };
const { templateFetcher } = useBackendInstanceRequest();
const { data, error, isValidating } = useSWR<
HttpResponseOk<MerchantBackend.Template.TemplateDetails>,
HttpError
>([`/private/templates/${templateId}`, token, url], templateFetcher, {
>([`/private/templates/${templateId}`], templateFetcher, {
refreshInterval: 0,
refreshWhenHidden: false,
revalidateOnFocus: false,

View File

@ -0,0 +1,120 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { MockEnvironment } from "@gnu-taler/web-util/lib/tests/mock";
import { ComponentChildren, FunctionalComponent, h, VNode } from "preact";
import { SWRConfig } from "swr";
import { ApiContextProvider } from "../context/api.js";
import { BackendContextProvider } from "../context/backend.js";
import { InstanceContextProvider } from "../context/instance.js";
import { HttpResponseOk, RequestOptions } from "../utils/request.js";
export class ApiMockEnvironment extends MockEnvironment {
constructor(debug = false) {
super(debug);
}
mockApiIfNeeded(): void {
null; // do nothing
}
public buildTestingContext(): FunctionalComponent<{
children: ComponentChildren;
}> {
const __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE =
this.saveRequestAndGetMockedResponse.bind(this);
return function TestingContext({
children,
}: {
children: ComponentChildren;
}): VNode {
async function request<T>(
base: string,
path: string,
options: RequestOptions = {},
): Promise<HttpResponseOk<T>> {
const _url = new URL(`${base}${path}`);
// Object.entries(options.params ?? {}).forEach(([key, value]) => {
// _url.searchParams.set(key, String(value));
// });
const mocked = __SAVE_REQUEST_AND_GET_MOCKED_RESPONSE(
{
method: options.method ?? "GET",
url: _url.href,
},
{
qparam: options.params,
auth: options.token,
request: options.data,
},
);
return {
ok: true,
data: (!mocked ? undefined : mocked.payload) as T,
loading: false,
clientError: false,
serverError: false,
info: {
hasToken: !!options.token,
status: !mocked ? 200 : mocked.status,
url: _url,
payload: options.data,
},
};
}
const SC: any = SWRConfig;
return (
<BackendContextProvider
defaultUrl="http://backend"
initialToken={undefined}
>
<InstanceContextProvider
value={{
token: undefined,
id: "default",
admin: true,
changeToken: () => null,
}}
>
<ApiContextProvider value={{ request }}>
<SC
value={{
loadingTimeout: 0,
dedupingInterval: 0,
shouldRetryOnError: false,
errorRetryInterval: 0,
errorRetryCount: 0,
provider: () => new Map(),
}}
>
{children}
</SC>
</ApiContextProvider>
</InstanceContextProvider>
</BackendContextProvider>
);
};
}
}

View File

@ -0,0 +1,277 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { tests } from "@gnu-taler/web-util/lib/index.browser";
import { expect } from "chai";
import { MerchantBackend } from "../declaration.js";
import { API_INFORM_TRANSFERS, API_LIST_TRANSFERS } from "./urls.js";
import { ApiMockEnvironment } from "./testing.js";
import { useInstanceTransfers, useTransferAPI } from "./transfer.js";
describe("transfer api interaction with listing", () => {
it("should evict cache when informing a transfer", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_LIST_TRANSFERS, {
qparam: { limit: 0 },
response: {
transfers: [{ wtid: "2" } as MerchantBackend.Transfers.TransferDetails],
},
});
// FIXME: is this query really needed? if the hook is rendered without
// position argument then then backend is returning the newest and no need
// to this second query
env.addRequestExpectation(API_LIST_TRANSFERS, {
qparam: { limit: -20 },
response: {
transfers: [],
},
});
const moveCursor = (d: string) => {
console.log("new position", d);
};
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
const query = useInstanceTransfers({}, moveCursor);
const api = useTransferAPI();
return { query, api };
},
{},
[
({ query, api }) => {
expect(query.loading).true;
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
expect(query.loading).undefined;
expect(query.ok).true;
if (!query.ok) return;
expect(query.data).deep.equals({
transfers: [{ wtid: "2" }],
});
env.addRequestExpectation(API_INFORM_TRANSFERS, {
request: {
wtid: "3",
credit_amount: "EUR:1",
exchange_url: "exchange.url",
payto_uri: "payto://",
},
response: { total: "" } as any,
});
env.addRequestExpectation(API_LIST_TRANSFERS, {
qparam: { limit: 0 },
response: {
transfers: [{ wtid: "2" } as any, { wtid: "3" } as any],
},
});
env.addRequestExpectation(API_LIST_TRANSFERS, {
qparam: { limit: -20 },
response: {
transfers: [],
},
});
api.informTransfer({
wtid: "3",
credit_amount: "EUR:1",
exchange_url: "exchange.url",
payto_uri: "payto://",
});
},
({ query, api }) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
expect(query.loading).undefined;
expect(query.ok).true;
if (!query.ok) return;
expect(query.data).deep.equals({
transfers: [{ wtid: "3" }, { wtid: "2" }],
});
},
],
env.buildTestingContext(),
);
expect(hookBehavior).deep.eq({ result: "ok" });
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
});
});
describe("transfer listing pagination", () => {
it("should not load more if has reach the end", async () => {
const env = new ApiMockEnvironment();
env.addRequestExpectation(API_LIST_TRANSFERS, {
qparam: { limit: 0, payto_uri: "payto://" },
response: {
transfers: [{ wtid: "2" } as any],
},
});
env.addRequestExpectation(API_LIST_TRANSFERS, {
qparam: { limit: -20, payto_uri: "payto://" },
response: {
transfers: [{ wtid: "1" } as any],
},
});
const moveCursor = (d: string) => {
console.log("new position", d);
};
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
return useInstanceTransfers({ payto_uri: "payto://" }, moveCursor);
},
{},
[
(query) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
expect(query.loading).true;
},
(query) => {
expect(query.loading).undefined;
expect(query.ok).true;
if (!query.ok) return;
expect(query.data).deep.equals({
transfers: [{ wtid: "2" }, { wtid: "1" }],
});
expect(query.isReachingEnd).true;
expect(query.isReachingStart).true;
//check that this button won't trigger more updates since
//has reach end and start
query.loadMore();
query.loadMorePrev();
},
],
env.buildTestingContext(),
);
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(hookBehavior).deep.eq({ result: "ok" });
});
it("should load more if result brings more that PAGE_SIZE", async () => {
const env = new ApiMockEnvironment();
const transfersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({
wtid: String(i),
}));
const transfersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({
wtid: String(i + 20),
}));
const transfersFrom20to0 = [...transfersFrom0to20].reverse();
env.addRequestExpectation(API_LIST_TRANSFERS, {
qparam: { limit: 20, payto_uri: "payto://", offset: "1" },
response: {
transfers: transfersFrom0to20,
},
});
env.addRequestExpectation(API_LIST_TRANSFERS, {
qparam: { limit: -20, payto_uri: "payto://", offset: "1" },
response: {
transfers: transfersFrom20to40,
},
});
const moveCursor = (d: string) => {
console.log("new position", d);
};
const hookBehavior = await tests.hookBehaveLikeThis(
() => {
return useInstanceTransfers(
{ payto_uri: "payto://", position: "1" },
moveCursor,
);
},
{},
[
(result) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
expect(result.loading).true;
},
(result) => {
expect(result.loading).undefined;
expect(result.ok).true;
if (!result.ok) return;
expect(result.data).deep.equals({
transfers: [...transfersFrom20to0, ...transfersFrom20to40],
});
expect(result.isReachingEnd).false;
expect(result.isReachingStart).false;
//query more
env.addRequestExpectation(API_LIST_TRANSFERS, {
qparam: { limit: -40, payto_uri: "payto://", offset: "1" },
response: {
transfers: [...transfersFrom20to40, { wtid: "41" }],
},
});
result.loadMore();
},
(result) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
expect(result.loading).true;
},
(result) => {
expect(env.assertJustExpectedRequestWereMade()).deep.eq({
result: "ok",
});
expect(result.loading).undefined;
expect(result.ok).true;
if (!result.ok) return;
expect(result.data).deep.equals({
transfers: [
...transfersFrom20to0,
...transfersFrom20to40,
{ wtid: "41" },
],
});
expect(result.isReachingEnd).true;
expect(result.isReachingStart).false;
},
],
env.buildTestingContext(),
);
expect(env.assertJustExpectedRequestWereMade()).deep.eq({ result: "ok" });
expect(hookBehavior).deep.eq({ result: "ok" });
});
});

View File

@ -13,55 +13,21 @@
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { useEffect, useState } from "preact/hooks";
import useSWR from "swr";
import { MerchantBackend } from "../declaration.js";
import { useBackendContext } from "../context/backend.js";
import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
import {
request,
HttpResponse,
HttpError,
HttpResponse,
HttpResponseOk,
HttpResponsePaginated,
useMatchMutate,
} from "./backend.js";
import useSWR from "swr";
import { useInstanceContext } from "../context/instance.js";
import { MAX_RESULT_SIZE, PAGE_SIZE } from "../utils/constants.js";
import { useEffect, useState } from "preact/hooks";
async function transferFetcher<T>(
url: string,
token: string,
backend: string,
payto_uri?: string,
verified?: string,
position?: string,
delta?: number,
): Promise<HttpResponseOk<T>> {
const params: any = {};
if (payto_uri !== undefined) params.payto_uri = payto_uri;
if (verified !== undefined) params.verified = verified;
if (delta !== undefined) {
params.limit = delta;
}
if (position !== undefined) params.offset = position;
return request<T>(`${backend}${url}`, { token, params });
}
} from "../utils/request.js";
import { useBackendInstanceRequest, useMatchMutate } from "./backend.js";
export function useTransferAPI(): TransferAPI {
const mutateAll = useMatchMutate();
const { url: baseUrl, token: adminToken } = useBackendContext();
const { token: instanceToken, id, admin } = useInstanceContext();
const { url, token } = !admin
? {
url: baseUrl,
token: adminToken,
}
: {
url: `${baseUrl}/instances/${id}`,
token: instanceToken,
};
const { request } = useBackendInstanceRequest();
const informTransfer = async (
data: MerchantBackend.Transfers.TransferInformation,
@ -70,10 +36,9 @@ export function useTransferAPI(): TransferAPI {
> => {
const res =
await request<MerchantBackend.Transfers.MerchantTrackTransferResponse>(
`${url}/private/transfers`,
`/private/transfers`,
{
method: "post",
token,
method: "POST",
data,
},
);
@ -103,12 +68,7 @@ export function useInstanceTransfers(
args?: InstanceTransferFilter,
updatePosition?: (id: string) => void,
): HttpResponsePaginated<MerchantBackend.Transfers.TransferList> {
const { url: baseUrl, token: baseToken } = useBackendContext();
const { token: instanceToken, id, admin } = useInstanceContext();
const { url, token } = !admin
? { url: baseUrl, token: baseToken }
: { url: `${baseUrl}/instances/${id}`, token: instanceToken };
const { transferFetcher } = useBackendInstanceRequest();
const [pageBefore, setPageBefore] = useState(1);
const [pageAfter, setPageAfter] = useState(1);
@ -129,8 +89,6 @@ export function useInstanceTransfers(
} = useSWR<HttpResponseOk<MerchantBackend.Transfers.TransferList>, HttpError>(
[
`/private/transfers`,
token,
url,
args?.payto_uri,
args?.verified,
args?.position,
@ -145,8 +103,6 @@ export function useInstanceTransfers(
} = useSWR<HttpResponseOk<MerchantBackend.Transfers.TransferList>, HttpError>(
[
`/private/transfers`,
token,
url,
args?.payto_uri,
args?.verified,
args?.position,
@ -185,10 +141,9 @@ export function useInstanceTransfers(
if (afterData.data.transfers.length < MAX_RESULT_SIZE) {
setPageAfter(pageAfter + 1);
} else {
const from = `${
afterData.data.transfers[afterData.data.transfers.length - 1]
const from = `${afterData.data.transfers[afterData.data.transfers.length - 1]
.transfer_serial_id
}`;
}`;
if (from && updatePosition) updatePosition(from);
}
},
@ -197,10 +152,9 @@ export function useInstanceTransfers(
if (beforeData.data.transfers.length < MAX_RESULT_SIZE) {
setPageBefore(pageBefore + 1);
} else if (beforeData) {
const from = `${
beforeData.data.transfers[beforeData.data.transfers.length - 1]
const from = `${beforeData.data.transfers[beforeData.data.transfers.length - 1]
.transfer_serial_id
}`;
}`;
if (from && updatePosition) updatePosition(from);
}
},
@ -210,9 +164,9 @@ export function useInstanceTransfers(
!beforeData || !afterData
? []
: (beforeData || lastBefore).data.transfers
.slice()
.reverse()
.concat((afterData || lastAfter).data.transfers);
.slice()
.reverse()
.concat((afterData || lastAfter).data.transfers);
if (loadingAfter || loadingBefore)
return { loading: true, data: { transfers } };
if (beforeData && afterData) {

View File

@ -0,0 +1,291 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { Query } from "@gnu-taler/web-util/lib/tests/mock";
import { MerchantBackend } from "../declaration.js";
////////////////////
// ORDER
////////////////////
export const API_CREATE_ORDER: Query<
MerchantBackend.Orders.PostOrderRequest,
MerchantBackend.Orders.PostOrderResponse
> = {
method: "POST",
url: "http://backend/instances/default/private/orders",
};
export const API_GET_ORDER_BY_ID = (
id: string,
): Query<unknown, MerchantBackend.Orders.MerchantOrderStatusResponse> => ({
method: "GET",
url: `http://backend/instances/default/private/orders/${id}`,
});
export const API_LIST_ORDERS: Query<
unknown,
MerchantBackend.Orders.OrderHistory
> = {
method: "GET",
url: "http://backend/instances/default/private/orders",
};
export const API_REFUND_ORDER_BY_ID = (
id: string,
): Query<
MerchantBackend.Orders.RefundRequest,
MerchantBackend.Orders.MerchantRefundResponse
> => ({
method: "POST",
url: `http://backend/instances/default/private/orders/${id}/refund`,
});
export const API_FORGET_ORDER_BY_ID = (
id: string,
): Query<MerchantBackend.Orders.ForgetRequest, unknown> => ({
method: "PATCH",
url: `http://backend/instances/default/private/orders/${id}/forget`,
});
export const API_DELETE_ORDER = (
id: string,
): Query<MerchantBackend.Orders.ForgetRequest, unknown> => ({
method: "DELETE",
url: `http://backend/instances/default/private/orders/${id}`,
});
////////////////////
// TRANSFER
////////////////////
export const API_LIST_TRANSFERS: Query<
unknown,
MerchantBackend.Transfers.TransferList
> = {
method: "GET",
url: "http://backend/instances/default/private/transfers",
};
export const API_INFORM_TRANSFERS: Query<
MerchantBackend.Transfers.TransferInformation,
MerchantBackend.Transfers.MerchantTrackTransferResponse
> = {
method: "POST",
url: "http://backend/instances/default/private/transfers",
};
////////////////////
// PRODUCT
////////////////////
export const API_CREATE_PRODUCT: Query<
MerchantBackend.Products.ProductAddDetail,
unknown
> = {
method: "POST",
url: "http://backend/instances/default/private/products",
};
export const API_LIST_PRODUCTS: Query<
unknown,
MerchantBackend.Products.InventorySummaryResponse
> = {
method: "GET",
url: "http://backend/instances/default/private/products",
};
export const API_GET_PRODUCT_BY_ID = (
id: string,
): Query<unknown, MerchantBackend.Products.ProductDetail> => ({
method: "GET",
url: `http://backend/instances/default/private/products/${id}`,
});
export const API_UPDATE_PRODUCT_BY_ID = (
id: string,
): Query<
MerchantBackend.Products.ProductPatchDetail,
MerchantBackend.Products.InventorySummaryResponse
> => ({
method: "PATCH",
url: `http://backend/instances/default/private/products/${id}`,
});
export const API_DELETE_PRODUCT = (id: string): Query<unknown, unknown> => ({
method: "DELETE",
url: `http://backend/instances/default/private/products/${id}`,
});
////////////////////
// RESERVES
////////////////////
export const API_CREATE_RESERVE: Query<
MerchantBackend.Tips.ReserveCreateRequest,
MerchantBackend.Tips.ReserveCreateConfirmation
> = {
method: "POST",
url: "http://backend/instances/default/private/reserves",
};
export const API_LIST_RESERVES: Query<
unknown,
MerchantBackend.Tips.TippingReserveStatus
> = {
method: "GET",
url: "http://backend/instances/default/private/reserves",
};
export const API_GET_RESERVE_BY_ID = (
pub: string,
): Query<unknown, MerchantBackend.Tips.ReserveDetail> => ({
method: "GET",
url: `http://backend/instances/default/private/reserves/${pub}`,
});
export const API_GET_TIP_BY_ID = (
pub: string,
): Query<unknown, MerchantBackend.Tips.TipDetails> => ({
method: "GET",
url: `http://backend/instances/default/private/tips/${pub}`,
});
export const API_AUTHORIZE_TIP_FOR_RESERVE = (
pub: string,
): Query<
MerchantBackend.Tips.TipCreateRequest,
MerchantBackend.Tips.TipCreateConfirmation
> => ({
method: "POST",
url: `http://backend/instances/default/private/reserves/${pub}/authorize-tip`,
});
export const API_AUTHORIZE_TIP: Query<
MerchantBackend.Tips.TipCreateRequest,
MerchantBackend.Tips.TipCreateConfirmation
> = {
method: "POST",
url: `http://backend/instances/default/private/tips`,
};
export const API_DELETE_RESERVE = (id: string): Query<unknown, unknown> => ({
method: "DELETE",
url: `http://backend/instances/default/private/reserves/${id}`,
});
////////////////////
// INSTANCE ADMIN
////////////////////
export const API_CREATE_INSTANCE: Query<
MerchantBackend.Instances.InstanceConfigurationMessage,
unknown
> = {
method: "POST",
url: "http://backend/management/instances",
};
export const API_GET_INSTANCE_BY_ID = (
id: string,
): Query<unknown, MerchantBackend.Instances.QueryInstancesResponse> => ({
method: "GET",
url: `http://backend/management/instances/${id}`,
});
export const API_GET_INSTANCE_KYC_BY_ID = (
id: string,
): Query<unknown, MerchantBackend.Instances.AccountKycRedirects> => ({
method: "GET",
url: `http://backend/management/instances/${id}/kyc`,
});
export const API_LIST_INSTANCES: Query<
unknown,
MerchantBackend.Instances.InstancesResponse
> = {
method: "GET",
url: "http://backend/management/instances",
};
export const API_UPDATE_INSTANCE_BY_ID = (
id: string,
): Query<
MerchantBackend.Instances.InstanceReconfigurationMessage,
unknown
> => ({
method: "PATCH",
url: `http://backend/management/instances/${id}`,
});
export const API_UPDATE_INSTANCE_AUTH_BY_ID = (
id: string,
): Query<
MerchantBackend.Instances.InstanceAuthConfigurationMessage,
unknown
> => ({
method: "POST",
url: `http://backend/management/instances/${id}/auth`,
});
export const API_DELETE_INSTANCE = (id: string): Query<unknown, unknown> => ({
method: "DELETE",
url: `http://backend/management/instances/${id}`,
});
////////////////////
// INSTANCE
////////////////////
export const API_GET_CURRENT_INSTANCE: Query<
unknown,
MerchantBackend.Instances.QueryInstancesResponse
> = {
method: "GET",
url: `http://backend/instances/default/private/`,
};
export const API_GET_CURRENT_INSTANCE_KYC: Query<
unknown,
MerchantBackend.Instances.AccountKycRedirects
> = {
method: "GET",
url: `http://backend/instances/default/private/kyc`,
};
export const API_UPDATE_CURRENT_INSTANCE: Query<
MerchantBackend.Instances.InstanceReconfigurationMessage,
unknown
> = {
method: "PATCH",
url: `http://backend/instances/default/private/`,
};
export const API_UPDATE_CURRENT_INSTANCE_AUTH: Query<
MerchantBackend.Instances.InstanceAuthConfigurationMessage,
unknown
> = {
method: "POST",
url: `http://backend/instances/default/private/auth`,
};
export const API_DELETE_CURRENT_INSTANCE: Query<unknown, unknown> = {
method: "DELETE",
url: `http://backend/instances/default/private`,
};

View File

@ -1,21 +0,0 @@
{
"name": "backoffice-preact",
"short_name": "backoffice-preact",
"start_url": "/",
"display": "standalone",
"orientation": "portrait",
"background_color": "#fff",
"theme_color": "#673ab8",
"icons": [
{
"src": "/assets/icons/android-chrome-192x192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "/assets/icons/android-chrome-512x512.png",
"type": "image/png",
"sizes": "512x512"
}
]
}

View File

@ -26,7 +26,7 @@ import { Loading } from "../../../components/exception/loading.js";
import { NotificationCard } from "../../../components/menu/index.js";
import { DeleteModal, PurgeModal } from "../../../components/modal/index.js";
import { MerchantBackend } from "../../../declaration.js";
import { HttpError } from "../../../hooks/backend.js";
import { HttpError } from "../../../utils/request.js";
import { useAdminAPI, useBackendInstances } from "../../../hooks/instance.js";
import { Notification } from "../../../utils/types.js";
import { View } from "./View.js";

View File

@ -18,7 +18,7 @@ import { useState } from "preact/hooks";
import { Loading } from "../../../components/exception/loading.js";
import { DeleteModal } from "../../../components/modal/index.js";
import { useInstanceContext } from "../../../context/instance.js";
import { HttpError } from "../../../hooks/backend.js";
import { HttpError } from "../../../utils/request.js";
import { useInstanceAPI, useInstanceDetails } from "../../../hooks/instance.js";
import { DetailPage } from "./DetailPage.js";

View File

@ -21,7 +21,7 @@
import { h, VNode } from "preact";
import { Loading } from "../../../../components/exception/loading.js";
import { HttpError } from "../../../../hooks/backend.js";
import { HttpError } from "../../../../utils/request.js";
import { useInstanceKYCDetails } from "../../../../hooks/instance.js";
import { ListPage } from "./ListPage.js";

View File

@ -24,7 +24,7 @@ import { useState } from "preact/hooks";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
import { MerchantBackend } from "../../../../declaration.js";
import { HttpError } from "../../../../hooks/backend.js";
import { HttpError } from "../../../../utils/request.js";
import { useInstanceDetails } from "../../../../hooks/instance.js";
import { useOrderAPI } from "../../../../hooks/order.js";
import { useInstanceProducts } from "../../../../hooks/product.js";

View File

@ -18,7 +18,7 @@ import { Fragment, h, VNode } from "preact";
import { useState } from "preact/hooks";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
import { HttpError } from "../../../../hooks/backend.js";
import { HttpError } from "../../../../utils/request.js";
import { useOrderAPI, useOrderDetails } from "../../../../hooks/order.js";
import { Notification } from "../../../../utils/types.js";
import { DetailPage } from "./DetailPage.js";

View File

@ -25,7 +25,7 @@ import { useState } from "preact/hooks";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
import { MerchantBackend } from "../../../../declaration.js";
import { HttpError } from "../../../../hooks/backend.js";
import { HttpError } from "../../../../utils/request.js";
import {
InstanceOrderFilter,
useInstanceOrders,

View File

@ -25,7 +25,7 @@ import { useState } from "preact/hooks";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
import { MerchantBackend, WithId } from "../../../../declaration.js";
import { HttpError } from "../../../../hooks/backend.js";
import { HttpError } from "../../../../utils/request.js";
import {
useInstanceProducts,
useProductAPI,

View File

@ -25,7 +25,7 @@ import { useState } from "preact/hooks";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
import { MerchantBackend } from "../../../../declaration.js";
import { HttpError } from "../../../../hooks/backend.js";
import { HttpError } from "../../../../utils/request.js";
import { useProductAPI, useProductDetails } from "../../../../hooks/product.js";
import { Notification } from "../../../../utils/types.js";
import { UpdatePage } from "./UpdatePage.js";

View File

@ -31,7 +31,7 @@ import { Input } from "../../../../components/form/Input.js";
import { InputCurrency } from "../../../../components/form/InputCurrency.js";
import { InputSelector } from "../../../../components/form/InputSelector.js";
import { ExchangeBackend, MerchantBackend } from "../../../../declaration.js";
import { request } from "../../../../hooks/backend.js";
// import { request } from "../../../../utils/request.js";
import {
PAYTO_WIRE_METHOD_LOOKUP,
URL_REGEX,
@ -124,11 +124,10 @@ function ViewStep({
<AsyncButton
class="has-tooltip-left"
onClick={() => {
return request<ExchangeBackend.WireResponse>(
`${reserve.exchange_url}wire`,
)
return fetch(`${reserve.exchange_url}wire`)
.then((r) => r.json())
.then((r) => {
const wireMethods = r.data.accounts.map((a) => {
const wireMethods = r.data.accounts.map((a: any) => {
const match = PAYTO_WIRE_METHOD_LOOKUP.exec(a.payto_uri);
return (match && match[1]) || "";
});

View File

@ -21,7 +21,7 @@
import { Fragment, h, VNode } from "preact";
import { Loading } from "../../../../components/exception/loading.js";
import { HttpError } from "../../../../hooks/backend.js";
import { HttpError } from "../../../../utils/request.js";
import { useReserveDetails } from "../../../../hooks/reserves.js";
import { DetailPage } from "./DetailPage.js";

View File

@ -25,7 +25,7 @@ import { useState } from "preact/hooks";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
import { MerchantBackend } from "../../../../declaration.js";
import { HttpError } from "../../../../hooks/backend.js";
import { HttpError } from "../../../../utils/request.js";
import {
useInstanceReserves,
useReservesAPI,

View File

@ -25,7 +25,7 @@ import { useState } from "preact/hooks";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
import { MerchantBackend } from "../../../../declaration.js";
import { HttpError } from "../../../../hooks/backend.js";
import { HttpError } from "../../../../utils/request.js";
import {
useInstanceTemplates,
useTemplateAPI,

View File

@ -25,7 +25,7 @@ import { useState } from "preact/hooks";
import { Loading } from "../../../../components/exception/loading.js";
import { NotificationCard } from "../../../../components/menu/index.js";
import { MerchantBackend, WithId } from "../../../../declaration.js";
import { HttpError } from "../../../../hooks/backend.js";
import { HttpError } from "../../../../utils/request.js";
import {
useTemplateAPI,
useTemplateDetails,

View File

@ -23,7 +23,7 @@ import { h, VNode } from "preact";
import { useState } from "preact/hooks";
import { Loading } from "../../../../components/exception/loading.js";
import { MerchantBackend } from "../../../../declaration.js";
import { HttpError } from "../../../../hooks/backend.js";
import { HttpError } from "../../../../utils/request.js";
import { useInstanceDetails } from "../../../../hooks/instance.js";
import { useInstanceTransfers } from "../../../../hooks/transfer.js";
import { ListPage } from "./ListPage.js";

View File

@ -20,13 +20,13 @@ import { Loading } from "../../../components/exception/loading.js";
import { NotificationCard } from "../../../components/menu/index.js";
import { useInstanceContext } from "../../../context/instance.js";
import { MerchantBackend } from "../../../declaration.js";
import { HttpError, HttpResponse } from "../../../hooks/backend.js";
import {
useInstanceAPI,
useInstanceDetails,
useManagedInstanceDetails,
useManagementAPI,
} from "../../../hooks/instance.js";
import { HttpError, HttpResponse } from "../../../utils/request.js";
import { Notification } from "../../../utils/types.js";
import { UpdatePage } from "./UpdatePage.js";

View File

@ -19,6 +19,7 @@
* @author Sebastian Javier Marchano (sebasjm)
*/
import { expect } from "chai";
import { AMOUNT_REGEX, PAYTO_REGEX } from "../../src/utils/constants.js";
describe('payto uri format', () => {
@ -31,7 +32,7 @@ describe('payto uri format', () => {
]
it('should be valid', () => {
valids.forEach(v => expect(v).toMatch(PAYTO_REGEX))
valids.forEach(v => expect(v).match(PAYTO_REGEX))
});
const invalids = [
@ -48,7 +49,7 @@ describe('payto uri format', () => {
]
it('should not be valid', () => {
invalids.forEach(v => expect(v).not.toMatch(PAYTO_REGEX))
invalids.forEach(v => expect(v).not.match(PAYTO_REGEX))
});
})
@ -64,7 +65,7 @@ describe('amount format', () => {
]
it('should be valid', () => {
valids.forEach(v => expect(v).toMatch(AMOUNT_REGEX))
valids.forEach(v => expect(v).match(AMOUNT_REGEX))
});
const invalids = [
@ -81,7 +82,7 @@ describe('amount format', () => {
]
it('should not be valid', () => {
invalids.forEach(v => expect(v).not.toMatch(AMOUNT_REGEX))
invalids.forEach(v => expect(v).not.match(AMOUNT_REGEX))
});
})

View File

@ -0,0 +1,282 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
// import axios, { AxiosError, AxiosResponse } from "axios";
import { MerchantBackend } from "../declaration.js";
export async function defaultRequestHandler<T>(
base: string,
path: string,
options: RequestOptions = {},
): Promise<HttpResponseOk<T>> {
const requestHeaders = options.token
? { Authorization: `Bearer ${options.token}` }
: undefined;
const requestMethod = options?.method ?? "GET";
const requestBody = options?.data;
const requestTimeout = 2 * 1000;
const requestParams = options.params ?? {};
const _url = new URL(`${base}${path}`);
Object.entries(requestParams).forEach(([key, value]) => {
_url.searchParams.set(key, String(value));
});
let payload: BodyInit | undefined = undefined;
if (requestBody != null) {
if (typeof requestBody === "string") {
payload = requestBody;
} else if (requestBody instanceof ArrayBuffer) {
payload = requestBody;
} else if (ArrayBuffer.isView(requestBody)) {
payload = requestBody;
} else if (typeof requestBody === "object") {
payload = JSON.stringify(requestBody);
} else {
throw Error("unsupported request body type");
}
}
const controller = new AbortController();
const timeoutId = setTimeout(() => {
controller.abort("HTTP_REQUEST_TIMEOUT");
}, requestTimeout);
const response = await fetch(_url.href, {
headers: {
...requestHeaders,
"Content-Type": "text/plain",
},
method: requestMethod,
credentials: "omit",
mode: "cors",
body: payload,
signal: controller.signal,
});
if (timeoutId) {
clearTimeout(timeoutId);
}
const headerMap = new Headers();
response.headers.forEach((value, key) => {
headerMap.set(key, value);
});
if (response.ok) {
const result = await buildRequestOk<T>(
response,
_url,
payload,
!!options.token,
);
return result;
} else {
const error = await buildRequestFailed(
response,
_url,
payload,
!!options.token,
);
throw error;
}
}
export type HttpResponse<T> =
| HttpResponseOk<T>
| HttpResponseLoading<T>
| HttpError;
export type HttpResponsePaginated<T> =
| HttpResponseOkPaginated<T>
| HttpResponseLoading<T>
| HttpError;
export interface RequestInfo {
url: URL;
hasToken: boolean;
payload: any;
status: number;
}
interface HttpResponseLoading<T> {
ok?: false;
loading: true;
clientError?: false;
serverError?: false;
data?: T;
}
export interface HttpResponseOk<T> {
ok: true;
loading?: false;
clientError?: false;
serverError?: false;
data: T;
info?: RequestInfo;
}
export type HttpResponseOkPaginated<T> = HttpResponseOk<T> & WithPagination;
export interface WithPagination {
loadMore: () => void;
loadMorePrev: () => void;
isReachingEnd?: boolean;
isReachingStart?: boolean;
}
export type HttpError =
| HttpResponseClientError
| HttpResponseServerError
| HttpResponseUnexpectedError;
export interface SwrError {
info: unknown;
status: number;
message: string;
}
export interface HttpResponseServerError {
ok?: false;
loading?: false;
clientError?: false;
serverError: true;
error?: MerchantBackend.ErrorDetail;
status: number;
message: string;
info?: RequestInfo;
}
interface HttpResponseClientError {
ok?: false;
loading?: false;
clientError: true;
serverError?: false;
info?: RequestInfo;
isUnauthorized: boolean;
isNotfound: boolean;
status: number;
error?: MerchantBackend.ErrorDetail;
message: string;
}
interface HttpResponseUnexpectedError {
ok?: false;
loading?: false;
clientError?: false;
serverError?: false;
info?: RequestInfo;
status?: number;
error: unknown;
message: string;
}
type Methods = "GET" | "POST" | "PATCH" | "DELETE" | "PUT";
export interface RequestOptions {
method?: Methods;
token?: string;
data?: any;
params?: unknown;
}
async function buildRequestOk<T>(
response: Response,
url: URL,
payload: any,
hasToken: boolean,
): Promise<HttpResponseOk<T>> {
const dataTxt = await response.text();
const data = dataTxt ? JSON.parse(dataTxt) : undefined
return {
ok: true,
data,
info: {
payload,
url,
hasToken,
status: response.status,
},
};
}
async function buildRequestFailed(
response: Response,
url: URL,
payload: any,
hasToken: boolean,
): Promise<
| HttpResponseClientError
| HttpResponseServerError
| HttpResponseUnexpectedError
> {
const status = response?.status;
const info: RequestInfo = {
payload,
url,
hasToken,
status: status || 0,
};
try {
const dataTxt = await response.text();
const data = dataTxt ? JSON.parse(dataTxt) : undefined
if (status && status >= 400 && status < 500) {
const error: HttpResponseClientError = {
clientError: true,
isNotfound: status === 404,
isUnauthorized: status === 401,
status,
info,
message: data?.hint,
error: data,
};
return error;
}
if (status && status >= 500 && status < 600) {
const error: HttpResponseServerError = {
serverError: true,
status,
info,
message: `${data?.hint} (code ${data?.code})`,
error: data,
};
return error;
}
return {
info,
status,
error: {},
message: "NOT DEFINED",
};
} catch (ex) {
const error: HttpResponseUnexpectedError = {
info,
status,
error: ex,
message: "NOT DEFINED",
};
throw error;
}
}
// export function isAxiosError<T>(
// error: AxiosError | any,
// ): error is AxiosError<T> {
// return error && error.isAxiosError;
// }

View File

@ -1,73 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import axios, { AxiosPromise, AxiosRequestConfig } from "axios";
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
export let removeAxiosCancelToken = false;
export let axiosHandler = function doAxiosRequest(
config: AxiosRequestConfig,
): AxiosPromise<any> {
return axios(config);
};
/**
* Set this backend library to testing mode.
* Instead of calling the axios library the @handler will be called
*
* @param handler callback that will mock axios
*/
export function setAxiosRequestAsTestingEnvironment(
handler: AxiosHandler,
): void {
removeAxiosCancelToken = true;
axiosHandler = function defaultTestingHandler(config) {
const currentHanlder = listOfHandlersToUseOnce.shift();
if (!currentHanlder) {
return handler(config);
}
return currentHanlder(config);
};
}
type AxiosHandler = (config: AxiosRequestConfig) => AxiosPromise<any>;
type AxiosArguments = { args: AxiosRequestConfig | undefined };
const listOfHandlersToUseOnce = new Array<AxiosHandler>();
/**
*
* @param handler mock function
* @returns savedArgs
*/
export function mockAxiosOnce(handler: AxiosHandler): {
args: AxiosRequestConfig | undefined;
} {
const savedArgs: AxiosArguments = { args: undefined };
listOfHandlersToUseOnce.push(
(config: AxiosRequestConfig): AxiosPromise<any> => {
savedArgs.args = config;
return handler(config);
},
);
return savedArgs;
}

View File

@ -1,24 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
// This fixed an error related to the CSS and loading gif breaking my Jest test
// See https://facebook.github.io/jest/docs/en/webpack.html#handling-static-assets
export default 'test-file-stub';

View File

@ -1,31 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
// fileTransformer.js
// eslint-disable-next-line @typescript-eslint/no-var-requires
const path = require('path');
module.exports = {
process(src, filename, config, options) {
return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';';
},
};

View File

@ -1,28 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import "regenerator-runtime/runtime";
// import { configure } from 'enzyme';
// import Adapter from 'enzyme-adapter-preact-pure';
// configure({
// adapter: new Adapter()
// });

View File

@ -1,445 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import * as axios from 'axios';
import { MerchantBackend } from "../src/declaration.js";
import { mockAxiosOnce, setAxiosRequestAsTestingEnvironment } from "../src/utils/switchableAxios.js";
// import { mockAxiosOnce, setAxiosRequestAsTestingEnvironment } from "../src/hooks/backend.js";
export type Query<Req, Res> = (GetQuery | PostQuery | DeleteQuery | PatchQuery) & RequestResponse<Req, Res>
interface RequestResponse<Req, Res> {
code?: number,
}
interface GetQuery { get: string }
interface PostQuery { post: string }
interface DeleteQuery { delete: string }
interface PatchQuery { patch: string }
const JEST_DEBUG_LOG = process.env['JEST_DEBUG_LOG'] !== undefined
type ExpectationValues = { query: Query<any, any>; params?: { auth?: string, request?: any, qparam?: any, response?: any } }
type TestValues = [axios.AxiosRequestConfig | undefined, ExpectationValues | undefined]
const defaultCallback = (actualQuery?: axios.AxiosRequestConfig): axios.AxiosPromise<any> => {
if (JEST_DEBUG_LOG) {
console.log('UNEXPECTED QUERY', actualQuery)
}
throw Error('Default Axios mock callback is called, this mean that the test did a tried to use axios but there was no expectation in place, try using JEST_DEBUG_LOG env')
}
setAxiosRequestAsTestingEnvironment(
defaultCallback
);
export class AxiosMockEnvironment {
expectations: Array<{
query: Query<any, any>,
auth?: string,
params?: { request?: any, qparam?: any, response?: any },
result: { args: axios.AxiosRequestConfig | undefined }
} | undefined> = []
// axiosMock: jest.MockedFunction<axios.AxiosStatic>
addRequestExpectation<RequestType, ResponseType>(expectedQuery: Query<RequestType, ResponseType>, params: { auth?: string, request?: RequestType, qparam?: any, response?: ResponseType }): void {
const result = mockAxiosOnce(function (actualQuery?: axios.AxiosRequestConfig): axios.AxiosPromise {
if (JEST_DEBUG_LOG) {
console.log('query to the backend is made', actualQuery)
}
if (!expectedQuery) {
return Promise.reject("a query was made but it was not expected")
}
if (JEST_DEBUG_LOG) {
console.log('expected query:', params?.request)
console.log('expected qparams:', params?.qparam)
console.log('sending response:', params?.response)
}
const responseCode = expectedQuery.code || 200
//This response is what buildRequestOk is expecting in file hook/backend.ts
if (responseCode >= 200 && responseCode < 300) {
return Promise.resolve({
data: params?.response, config: {
data: params?.response,
params: actualQuery?.params || {},
}, request: { params: actualQuery?.params || {} }
} as any);
}
//This response is what buildRequestFailed is expecting in file hook/backend.ts
return Promise.reject({
response: {
status: responseCode
},
request: {
data: params?.response,
params: actualQuery?.params || {},
}
})
} as any)
this.expectations.push(expectedQuery ? { query: expectedQuery, params, result } : undefined)
}
getLastTestValues(): TestValues {
const expectedQuery = this.expectations.shift()
return [
expectedQuery?.result.args, expectedQuery
]
}
}
export function assertJustExpectedRequestWereMade(env: AxiosMockEnvironment): void {
let size = env.expectations.length
while (size-- > 0) {
assertNextRequest(env)
}
assertNoMoreRequestWereMade(env)
}
export function assertNoMoreRequestWereMade(env: AxiosMockEnvironment): void {
const [actualQuery, expectedQuery] = env.getLastTestValues()
expect(actualQuery).toBeUndefined();
expect(expectedQuery).toBeUndefined();
}
export function assertNextRequest(env: AxiosMockEnvironment): void {
const [actualQuery, expectedQuery] = env.getLastTestValues()
if (!actualQuery) {
//expected one query but the tested component didn't execute one
expect(actualQuery).toBe(expectedQuery);
return
}
if (!expectedQuery) {
const errorMessage = 'a query was made to the backend but the test explicitly expected no query';
if (JEST_DEBUG_LOG) {
console.log(errorMessage, actualQuery)
}
throw Error(errorMessage)
}
if ('get' in expectedQuery.query) {
expect(actualQuery.method).toBe('get');
expect(actualQuery.url).toBe(expectedQuery.query.get);
}
if ('post' in expectedQuery.query) {
expect(actualQuery.method).toBe('post');
expect(actualQuery.url).toBe(expectedQuery.query.post);
}
if ('delete' in expectedQuery.query) {
expect(actualQuery.method).toBe('delete');
expect(actualQuery.url).toBe(expectedQuery.query.delete);
}
if ('patch' in expectedQuery.query) {
expect(actualQuery.method).toBe('patch');
expect(actualQuery.url).toBe(expectedQuery.query.patch);
}
if (expectedQuery.params?.request) {
expect(actualQuery.data).toMatchObject(expectedQuery.params.request)
}
if (expectedQuery.params?.qparam) {
expect(actualQuery.params).toMatchObject(expectedQuery.params.qparam)
}
if (expectedQuery.params?.auth) {
expect(actualQuery.headers.Authorization).toBe(expectedQuery.params?.auth)
}
}
////////////////////
// ORDER
////////////////////
export const API_CREATE_ORDER: Query<
MerchantBackend.Orders.PostOrderRequest,
MerchantBackend.Orders.PostOrderResponse
> = {
post: "http://backend/instances/default/private/orders",
};
export const API_GET_ORDER_BY_ID = (
id: string
): Query<
unknown,
MerchantBackend.Orders.MerchantOrderStatusResponse
> => ({
get: `http://backend/instances/default/private/orders/${id}`,
});
export const API_LIST_ORDERS: Query<
unknown,
MerchantBackend.Orders.OrderHistory
> = {
get: "http://backend/instances/default/private/orders",
};
export const API_REFUND_ORDER_BY_ID = (
id: string
): Query<
MerchantBackend.Orders.RefundRequest,
MerchantBackend.Orders.MerchantRefundResponse
> => ({
post: `http://backend/instances/default/private/orders/${id}/refund`,
});
export const API_FORGET_ORDER_BY_ID = (
id: string
): Query<
MerchantBackend.Orders.ForgetRequest,
unknown
> => ({
patch: `http://backend/instances/default/private/orders/${id}/forget`,
});
export const API_DELETE_ORDER = (
id: string
): Query<
MerchantBackend.Orders.ForgetRequest,
unknown
> => ({
delete: `http://backend/instances/default/private/orders/${id}`,
});
////////////////////
// TRANSFER
////////////////////
export const API_LIST_TRANSFERS: Query<
unknown,
MerchantBackend.Transfers.TransferList
> = {
get: "http://backend/instances/default/private/transfers",
};
export const API_INFORM_TRANSFERS: Query<
MerchantBackend.Transfers.TransferInformation,
MerchantBackend.Transfers.MerchantTrackTransferResponse
> = {
post: "http://backend/instances/default/private/transfers",
};
////////////////////
// PRODUCT
////////////////////
export const API_CREATE_PRODUCT: Query<
MerchantBackend.Products.ProductAddDetail,
unknown
> = {
post: "http://backend/instances/default/private/products",
};
export const API_LIST_PRODUCTS: Query<
unknown,
MerchantBackend.Products.InventorySummaryResponse
> = {
get: "http://backend/instances/default/private/products",
};
export const API_GET_PRODUCT_BY_ID = (
id: string
): Query<unknown, MerchantBackend.Products.ProductDetail> => ({
get: `http://backend/instances/default/private/products/${id}`,
});
export const API_UPDATE_PRODUCT_BY_ID = (
id: string
): Query<
MerchantBackend.Products.ProductPatchDetail,
MerchantBackend.Products.InventorySummaryResponse
> => ({
patch: `http://backend/instances/default/private/products/${id}`,
});
export const API_DELETE_PRODUCT = (
id: string
): Query<
unknown, unknown
> => ({
delete: `http://backend/instances/default/private/products/${id}`,
});
////////////////////
// RESERVES
////////////////////
export const API_CREATE_RESERVE: Query<
MerchantBackend.Tips.ReserveCreateRequest,
MerchantBackend.Tips.ReserveCreateConfirmation
> = {
post: "http://backend/instances/default/private/reserves",
};
export const API_LIST_RESERVES: Query<
unknown,
MerchantBackend.Tips.TippingReserveStatus
> = {
get: "http://backend/instances/default/private/reserves",
};
export const API_GET_RESERVE_BY_ID = (
pub: string
): Query<unknown, MerchantBackend.Tips.ReserveDetail> => ({
get: `http://backend/instances/default/private/reserves/${pub}`,
});
export const API_GET_TIP_BY_ID = (
pub: string
): Query<
unknown,
MerchantBackend.Tips.TipDetails
> => ({
get: `http://backend/instances/default/private/tips/${pub}`,
});
export const API_AUTHORIZE_TIP_FOR_RESERVE = (
pub: string
): Query<
MerchantBackend.Tips.TipCreateRequest,
MerchantBackend.Tips.TipCreateConfirmation
> => ({
post: `http://backend/instances/default/private/reserves/${pub}/authorize-tip`,
});
export const API_AUTHORIZE_TIP: Query<
MerchantBackend.Tips.TipCreateRequest,
MerchantBackend.Tips.TipCreateConfirmation
> = ({
post: `http://backend/instances/default/private/tips`,
});
export const API_DELETE_RESERVE = (
id: string
): Query<unknown, unknown> => ({
delete: `http://backend/instances/default/private/reserves/${id}`,
});
////////////////////
// INSTANCE ADMIN
////////////////////
export const API_CREATE_INSTANCE: Query<
MerchantBackend.Instances.InstanceConfigurationMessage,
unknown
> = {
post: "http://backend/management/instances",
};
export const API_GET_INSTANCE_BY_ID = (
id: string
): Query<
unknown,
MerchantBackend.Instances.QueryInstancesResponse
> => ({
get: `http://backend/management/instances/${id}`,
});
export const API_GET_INSTANCE_KYC_BY_ID = (
id: string
): Query<
unknown,
MerchantBackend.Instances.AccountKycRedirects
> => ({
get: `http://backend/management/instances/${id}/kyc`,
});
export const API_LIST_INSTANCES: Query<
unknown,
MerchantBackend.Instances.InstancesResponse
> = {
get: "http://backend/management/instances",
};
export const API_UPDATE_INSTANCE_BY_ID = (
id: string
): Query<
MerchantBackend.Instances.InstanceReconfigurationMessage,
unknown
> => ({
patch: `http://backend/management/instances/${id}`,
});
export const API_UPDATE_INSTANCE_AUTH_BY_ID = (
id: string
): Query<
MerchantBackend.Instances.InstanceAuthConfigurationMessage,
unknown
> => ({
post: `http://backend/management/instances/${id}/auth`,
});
export const API_DELETE_INSTANCE = (
id: string
): Query<unknown, unknown> => ({
delete: `http://backend/management/instances/${id}`,
});
////////////////////
// INSTANCE
////////////////////
export const API_GET_CURRENT_INSTANCE: Query<
unknown,
MerchantBackend.Instances.QueryInstancesResponse
> = ({
get: `http://backend/instances/default/private/`,
});
export const API_GET_CURRENT_INSTANCE_KYC: Query<
unknown,
MerchantBackend.Instances.AccountKycRedirects
> =
({
get: `http://backend/instances/default/private/kyc`,
});
export const API_UPDATE_CURRENT_INSTANCE: Query<
MerchantBackend.Instances.InstanceReconfigurationMessage,
unknown
> = {
patch: `http://backend/instances/default/private/`,
};
export const API_UPDATE_CURRENT_INSTANCE_AUTH: Query<
MerchantBackend.Instances.InstanceAuthConfigurationMessage,
unknown
> = {
post: `http://backend/instances/default/private/auth`,
};
export const API_DELETE_CURRENT_INSTANCE: Query<
unknown,
unknown
> = ({
delete: `http://backend/instances/default/private`,
});

View File

@ -1,172 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { renderHook } from "@testing-library/preact-hooks";
import { ComponentChildren, h, VNode } from "preact";
import { act } from "preact/test-utils";
import { BackendContextProvider } from "../../src/context/backend.js";
import { InstanceContextProvider } from "../../src/context/instance.js";
import { MerchantBackend } from "../../src/declaration.js";
import {
useAdminAPI,
useInstanceAPI,
useManagementAPI,
} from "../../src/hooks/instance.js";
import {
API_CREATE_INSTANCE,
API_GET_CURRENT_INSTANCE,
API_UPDATE_CURRENT_INSTANCE_AUTH,
API_UPDATE_INSTANCE_AUTH_BY_ID,
assertJustExpectedRequestWereMade,
AxiosMockEnvironment,
} from "../axiosMock.js";
interface TestingContextProps {
children?: ComponentChildren;
}
function TestingContext({ children }: TestingContextProps): VNode {
return (
<BackendContextProvider defaultUrl="http://backend" initialToken="token">
{children}
</BackendContextProvider>
);
}
function AdminTestingContext({ children }: TestingContextProps): VNode {
return (
<BackendContextProvider defaultUrl="http://backend" initialToken="token">
<InstanceContextProvider
value={{
token: "token",
id: "default",
admin: true,
changeToken: () => null,
}}
>
{children}
</InstanceContextProvider>
</BackendContextProvider>
);
}
describe("backend context api ", () => {
it("should use new token after updating the instance token in the settings as user", async () => {
const env = new AxiosMockEnvironment();
const { result, waitForNextUpdate } = renderHook(
() => {
const instance = useInstanceAPI();
const management = useManagementAPI("default");
const admin = useAdminAPI();
return { instance, management, admin };
},
{ wrapper: TestingContext }
);
if (!result.current) {
expect(result.current).toBeDefined();
return;
}
env.addRequestExpectation(API_UPDATE_INSTANCE_AUTH_BY_ID("default"), {
request: {
method: "token",
token: "another_token",
},
response: {
name: "instance_name",
} as MerchantBackend.Instances.QueryInstancesResponse,
});
await act(async () => {
await result.current?.management.setNewToken("another_token");
});
// await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
env.addRequestExpectation(API_CREATE_INSTANCE, {
auth: "Bearer another_token",
request: {
id: "new_instance_id",
} as MerchantBackend.Instances.InstanceConfigurationMessage,
});
result.current.admin.createInstance({
id: "new_instance_id",
} as MerchantBackend.Instances.InstanceConfigurationMessage);
assertJustExpectedRequestWereMade(env);
});
it("should use new token after updating the instance token in the settings as admin", async () => {
const env = new AxiosMockEnvironment();
const { result, waitForNextUpdate } = renderHook(
() => {
const instance = useInstanceAPI();
const management = useManagementAPI("default");
const admin = useAdminAPI();
return { instance, management, admin };
},
{ wrapper: AdminTestingContext }
);
if (!result.current) {
expect(result.current).toBeDefined();
return;
}
env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, {
request: {
method: "token",
token: "another_token",
},
response: {
name: "instance_name",
} as MerchantBackend.Instances.QueryInstancesResponse,
});
await act(async () => {
await result.current?.instance.setNewToken("another_token");
});
// await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
env.addRequestExpectation(API_CREATE_INSTANCE, {
auth: "Bearer another_token",
request: {
id: "new_instance_id",
} as MerchantBackend.Instances.InstanceConfigurationMessage,
});
result.current.admin.createInstance({
id: "new_instance_id",
} as MerchantBackend.Instances.InstanceConfigurationMessage);
assertJustExpectedRequestWereMade(env);
});
});

View File

@ -1,28 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
declare global {
namespace jest {
interface Matchers<R> {
toBeWithinRange(a: number, b: number): R;
}
}
}

View File

@ -1,63 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { h } from "preact";
import { ProductList } from "../src/components/product/ProductList.js";
// See: https://github.com/preactjs/enzyme-adapter-preact-pure
// import { shallow } from 'enzyme';
import { render } from "@testing-library/preact";
import * as backend from "../src/context/config.js";
// import * as i18n from "../src/context/translation.js";
// import * as jedLib from "jed";
// const handler = new jedLib.Jed("en");
describe("Initial Test of the Sidebar", () => {
beforeEach(() => {
jest
.spyOn(backend, "useConfigContext")
.mockImplementation(() => ({ version: "", currency: "" }));
// jest.spyOn(i18n, "useTranslationContext").mockImplementation(() => ({
// changeLanguage: () => null,
// handler,
// lang: "en",
// }));
});
test("Product list renders a table", () => {
const context = render(
<ProductList
list={[
{
description: "description of the product",
image: "asdasda",
price: "USD:10",
quantity: 1,
taxes: [{ name: "VAT", tax: "EUR:1" }],
unit: "book",
},
]}
/>,
);
expect(context.findAllByText("description of the product")).toBeDefined();
// expect(context.find('table tr td img').map(img => img.prop('src'))).toEqual('');
});
});

View File

@ -1,158 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
import { renderHook } from "@testing-library/preact-hooks"
import { useAsync } from "../../src/hooks/async.js"
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
test("async function is called", async () => {
jest.useFakeTimers()
const timeout = 500
const asyncFunction = jest.fn(() => new Promise((res) => {
setTimeout(() => {
res({ the_answer: 'yes' })
}, timeout);
}))
const { result, waitForNextUpdate } = renderHook(() => {
return useAsync(asyncFunction)
})
expect(result.current?.isLoading).toBeFalsy()
result.current?.request()
expect(asyncFunction).toBeCalled()
await waitForNextUpdate({ timeout: 1 })
expect(result.current?.isLoading).toBeTruthy()
jest.advanceTimersByTime(timeout + 1)
await waitForNextUpdate({ timeout: 1 })
expect(result.current?.isLoading).toBeFalsy()
expect(result.current?.data).toMatchObject({ the_answer: 'yes' })
expect(result.current?.error).toBeUndefined()
expect(result.current?.isSlow).toBeFalsy()
})
test("async function return error if rejected", async () => {
jest.useFakeTimers()
const timeout = 500
const asyncFunction = jest.fn(() => new Promise((_, rej) => {
setTimeout(() => {
rej({ the_error: 'yes' })
}, timeout);
}))
const { result, waitForNextUpdate } = renderHook(() => {
return useAsync(asyncFunction)
})
expect(result.current?.isLoading).toBeFalsy()
result.current?.request()
expect(asyncFunction).toBeCalled()
await waitForNextUpdate({ timeout: 1 })
expect(result.current?.isLoading).toBeTruthy()
jest.advanceTimersByTime(timeout + 1)
await waitForNextUpdate({ timeout: 1 })
expect(result.current?.isLoading).toBeFalsy()
expect(result.current?.error).toMatchObject({ the_error: 'yes' })
expect(result.current?.data).toBeUndefined()
expect(result.current?.isSlow).toBeFalsy()
})
test("async function is slow", async () => {
jest.useFakeTimers()
const timeout = 2200
const asyncFunction = jest.fn(() => new Promise((res) => {
setTimeout(() => {
res({ the_answer: 'yes' })
}, timeout);
}))
const { result, waitForNextUpdate } = renderHook(() => {
return useAsync(asyncFunction)
})
expect(result.current?.isLoading).toBeFalsy()
result.current?.request()
expect(asyncFunction).toBeCalled()
await waitForNextUpdate({ timeout: 1 })
expect(result.current?.isLoading).toBeTruthy()
jest.advanceTimersByTime(timeout / 2)
await waitForNextUpdate({ timeout: 1 })
expect(result.current?.isLoading).toBeTruthy()
expect(result.current?.isSlow).toBeTruthy()
expect(result.current?.data).toBeUndefined()
expect(result.current?.error).toBeUndefined()
jest.advanceTimersByTime(timeout / 2)
await waitForNextUpdate({ timeout: 1 })
expect(result.current?.isLoading).toBeFalsy()
expect(result.current?.data).toMatchObject({ the_answer: 'yes' })
expect(result.current?.error).toBeUndefined()
expect(result.current?.isSlow).toBeFalsy()
})
test("async function is cancellable", async () => {
jest.useFakeTimers()
const timeout = 2200
const asyncFunction = jest.fn(() => new Promise((res) => {
setTimeout(() => {
res({ the_answer: 'yes' })
}, timeout);
}))
const { result, waitForNextUpdate } = renderHook(() => {
return useAsync(asyncFunction)
})
expect(result.current?.isLoading).toBeFalsy()
result.current?.request()
expect(asyncFunction).toBeCalled()
await waitForNextUpdate({ timeout: 1 })
expect(result.current?.isLoading).toBeTruthy()
jest.advanceTimersByTime(timeout / 2)
await waitForNextUpdate({ timeout: 1 })
expect(result.current?.isLoading).toBeTruthy()
expect(result.current?.isSlow).toBeTruthy()
expect(result.current?.data).toBeUndefined()
expect(result.current?.error).toBeUndefined()
result.current?.cancel()
await waitForNextUpdate({ timeout: 1 })
expect(result.current?.isLoading).toBeFalsy()
expect(result.current?.data).toBeUndefined()
expect(result.current?.error).toBeUndefined()
expect(result.current?.isSlow).toBeFalsy()
})

View File

@ -1,62 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { renderHook, act } from '@testing-library/preact-hooks';
import { useListener } from "../../src/hooks/listener.js";
// jest.useFakeTimers()
test('listener', async () => {
function createSomeString() {
return "hello"
}
async function addWorldToTheEnd(resultFromComponentB: string) {
return `${resultFromComponentB} world`
}
const expectedResult = "hello world"
const { result } = renderHook(() => useListener(addWorldToTheEnd))
expect(result.current).toBeDefined()
if (!result.current) {
return;
}
{
const [activator, subscriber] = result.current
expect(activator).toBeUndefined()
act(() => {
subscriber(createSomeString)
})
}
const [activator] = result.current
expect(activator).toBeDefined()
if (!activator) return;
const response = await activator()
expect(response).toBe(expectedResult)
});

View File

@ -1,51 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { renderHook, act} from '@testing-library/preact-hooks';
import { useNotifications } from "../../src/hooks/notifications.js";
jest.useFakeTimers()
test('notification should disappear after timeout', () => {
jest.spyOn(global, 'setTimeout');
const timeout = 1000
const { result, rerender } = renderHook(() => useNotifications(undefined, timeout));
expect(result.current?.notifications.length).toBe(0);
act(() => {
result.current?.pushNotification({
message: 'some_id',
type: 'INFO'
});
});
expect(result.current?.notifications.length).toBe(1);
jest.advanceTimersByTime(timeout/2);
rerender()
expect(result.current?.notifications.length).toBe(1);
jest.advanceTimersByTime(timeout);
rerender()
expect(result.current?.notifications.length).toBe(0);
});

View File

@ -1,46 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { ComponentChildren, h, VNode } from "preact";
import { SWRConfig } from "swr";
import { BackendContextProvider } from "../../../src/context/backend.js";
import { InstanceContextProvider } from "../../../src/context/instance.js";
interface TestingContextProps {
children?: ComponentChildren;
}
export function TestingContext({ children }: TestingContextProps): VNode {
const SC: any = SWRConfig
return (
<BackendContextProvider defaultUrl="http://backend" initialToken="token">
<InstanceContextProvider
value={{
token: "token",
id: "default",
admin: true,
changeToken: () => null,
}}
>
<SC value={{ provider: () => new Map() }}>{children}</SC>
</InstanceContextProvider>
</BackendContextProvider>
);
}

View File

@ -1,636 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { renderHook } from "@testing-library/preact-hooks";
import { act } from "preact/test-utils";
import { MerchantBackend } from "../../../src/declaration.js";
import { useAdminAPI, useBackendInstances, useInstanceAPI, useInstanceDetails, useManagementAPI } from "../../../src/hooks/instance.js";
import {
API_CREATE_INSTANCE,
API_DELETE_INSTANCE,
API_GET_CURRENT_INSTANCE,
API_LIST_INSTANCES,
API_UPDATE_CURRENT_INSTANCE,
API_UPDATE_CURRENT_INSTANCE_AUTH,
API_UPDATE_INSTANCE_AUTH_BY_ID,
API_UPDATE_INSTANCE_BY_ID,
assertJustExpectedRequestWereMade,
AxiosMockEnvironment
} from "../../axiosMock.js";
import { TestingContext } from "./index.js";
describe("instance api interaction with details", () => {
it("should evict cache when updating an instance", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
response: {
name: 'instance_name'
} as MerchantBackend.Instances.QueryInstancesResponse,
});
const { result, waitForNextUpdate } = renderHook(
() => {
const api = useInstanceAPI();
const query = useInstanceDetails();
return { query, api };
},
{ wrapper: TestingContext }
);
expect(result.current).toBeDefined();
if (!result.current) {
return;
}
expect(result.current.query.loading).toBeTruthy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
name: 'instance_name'
});
env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE, {
request: {
name: 'other_name'
} as MerchantBackend.Instances.InstanceReconfigurationMessage,
});
act(async () => {
await result.current?.api.updateInstance({
name: 'other_name'
} as MerchantBackend.Instances.InstanceReconfigurationMessage);
});
assertJustExpectedRequestWereMade(env);
env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
response: {
name: 'other_name'
} as MerchantBackend.Instances.QueryInstancesResponse,
});
expect(result.current.query.loading).toBeFalsy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current.query.ok).toBeTruthy();
expect(result.current.query.data).toEqual({
name: 'other_name'
});
});
it("should evict cache when setting the instance's token", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
response: {
name: 'instance_name',
auth: {
method: 'token',
token: 'not-secret',
}
} as MerchantBackend.Instances.QueryInstancesResponse,
});
const { result, waitForNextUpdate } = renderHook(
() => {
const api = useInstanceAPI();
const query = useInstanceDetails();
return { query, api };
},
{ wrapper: TestingContext }
);
expect(result.current).toBeDefined();
if (!result.current) {
return;
}
expect(result.current.query.loading).toBeTruthy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
name: 'instance_name',
auth: {
method: 'token',
token: 'not-secret',
}
});
env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, {
request: {
method: 'token',
token: 'secret'
} as MerchantBackend.Instances.InstanceAuthConfigurationMessage,
});
act(async () => {
await result.current?.api.setNewToken('secret');
});
assertJustExpectedRequestWereMade(env);
env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
response: {
name: 'instance_name',
auth: {
method: 'token',
token: 'secret',
}
} as MerchantBackend.Instances.QueryInstancesResponse,
});
expect(result.current.query.loading).toBeFalsy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current.query.ok).toBeTruthy();
expect(result.current.query.data).toEqual({
name: 'instance_name',
auth: {
method: 'token',
token: 'secret',
}
});
});
it("should evict cache when clearing the instance's token", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
response: {
name: 'instance_name',
auth: {
method: 'token',
token: 'not-secret',
}
} as MerchantBackend.Instances.QueryInstancesResponse,
});
const { result, waitForNextUpdate } = renderHook(
() => {
const api = useInstanceAPI();
const query = useInstanceDetails();
return { query, api };
},
{ wrapper: TestingContext }
);
expect(result.current).toBeDefined();
if (!result.current) {
return;
}
expect(result.current.query.loading).toBeTruthy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
name: 'instance_name',
auth: {
method: 'token',
token: 'not-secret',
}
});
env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, {
request: {
method: 'external',
} as MerchantBackend.Instances.InstanceAuthConfigurationMessage,
});
act(async () => {
await result.current?.api.clearToken();
});
assertJustExpectedRequestWereMade(env);
env.addRequestExpectation(API_GET_CURRENT_INSTANCE, {
response: {
name: 'instance_name',
auth: {
method: 'external',
}
} as MerchantBackend.Instances.QueryInstancesResponse,
});
expect(result.current.query.loading).toBeFalsy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current.query.ok).toBeTruthy();
expect(result.current.query.data).toEqual({
name: 'instance_name',
auth: {
method: 'external',
}
});
});
});
describe("instance admin api interaction with listing", () => {
it("should evict cache when creating a new instance", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_LIST_INSTANCES, {
response: {
instances: [{
name: 'instance_name'
} as MerchantBackend.Instances.Instance]
},
});
const { result, waitForNextUpdate } = renderHook(
() => {
const api = useAdminAPI();
const query = useBackendInstances();
return { query, api };
},
{ wrapper: TestingContext }
);
expect(result.current).toBeDefined();
if (!result.current) {
return;
}
expect(result.current.query.loading).toBeTruthy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
instances: [{
name: 'instance_name'
}]
});
env.addRequestExpectation(API_CREATE_INSTANCE, {
request: {
name: 'other_name'
} as MerchantBackend.Instances.InstanceConfigurationMessage,
});
act(async () => {
await result.current?.api.createInstance({
name: 'other_name'
} as MerchantBackend.Instances.InstanceConfigurationMessage);
});
assertJustExpectedRequestWereMade(env);
env.addRequestExpectation(API_LIST_INSTANCES, {
response: {
instances: [{
name: 'instance_name'
} as MerchantBackend.Instances.Instance,
{
name: 'other_name'
} as MerchantBackend.Instances.Instance]
},
});
expect(result.current.query.loading).toBeFalsy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current.query.ok).toBeTruthy();
expect(result.current.query.data).toEqual({
instances: [{
name: 'instance_name'
}, {
name: 'other_name'
}]
});
});
it("should evict cache when deleting an instance", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_LIST_INSTANCES, {
response: {
instances: [{
id: 'default',
name: 'instance_name'
} as MerchantBackend.Instances.Instance,
{
id: 'the_id',
name: 'second_instance'
} as MerchantBackend.Instances.Instance]
},
});
const { result, waitForNextUpdate } = renderHook(
() => {
const api = useAdminAPI();
const query = useBackendInstances();
return { query, api };
},
{ wrapper: TestingContext }
);
expect(result.current).toBeDefined();
if (!result.current) {
return;
}
expect(result.current.query.loading).toBeTruthy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
instances: [{
id: 'default',
name: 'instance_name'
}, {
id: 'the_id',
name: 'second_instance'
}]
});
env.addRequestExpectation(API_DELETE_INSTANCE('the_id'), {});
act(async () => {
await result.current?.api.deleteInstance('the_id');
});
assertJustExpectedRequestWereMade(env);
env.addRequestExpectation(API_LIST_INSTANCES, {
response: {
instances: [{
id: 'default',
name: 'instance_name'
} as MerchantBackend.Instances.Instance]
},
});
expect(result.current.query.loading).toBeFalsy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current.query.ok).toBeTruthy();
expect(result.current.query.data).toEqual({
instances: [{
id: 'default',
name: 'instance_name'
}]
});
});
it("should evict cache when deleting (purge) an instance", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_LIST_INSTANCES, {
response: {
instances: [{
id: 'default',
name: 'instance_name'
} as MerchantBackend.Instances.Instance,
{
id: 'the_id',
name: 'second_instance'
} as MerchantBackend.Instances.Instance]
},
});
const { result, waitForNextUpdate } = renderHook(
() => {
const api = useAdminAPI();
const query = useBackendInstances();
return { query, api };
},
{ wrapper: TestingContext }
);
expect(result.current).toBeDefined();
if (!result.current) {
return;
}
expect(result.current.query.loading).toBeTruthy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
instances: [{
id: 'default',
name: 'instance_name'
}, {
id: 'the_id',
name: 'second_instance'
}]
});
env.addRequestExpectation(API_DELETE_INSTANCE('the_id'), {
qparam: {
purge: 'YES'
}
});
act(async () => {
await result.current?.api.purgeInstance('the_id');
});
assertJustExpectedRequestWereMade(env);
env.addRequestExpectation(API_LIST_INSTANCES, {
response: {
instances: [{
id: 'default',
name: 'instance_name'
} as MerchantBackend.Instances.Instance]
},
});
expect(result.current.query.loading).toBeFalsy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current.query.ok).toBeTruthy();
expect(result.current.query.data).toEqual({
instances: [{
id: 'default',
name: 'instance_name'
}]
});
});
});
describe("instance management api interaction with listing", () => {
it("should evict cache when updating an instance", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_LIST_INSTANCES, {
response: {
instances: [{
id: 'managed',
name: 'instance_name'
} as MerchantBackend.Instances.Instance]
},
});
const { result, waitForNextUpdate } = renderHook(
() => {
const api = useManagementAPI('managed');
const query = useBackendInstances();
return { query, api };
},
{ wrapper: TestingContext }
);
expect(result.current).toBeDefined();
if (!result.current) {
return;
}
expect(result.current.query.loading).toBeTruthy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
instances: [{
id: 'managed',
name: 'instance_name'
}]
});
env.addRequestExpectation(API_UPDATE_INSTANCE_BY_ID('managed'), {
request: {
name: 'other_name'
} as MerchantBackend.Instances.InstanceReconfigurationMessage,
});
act(async () => {
await result.current?.api.updateInstance({
name: 'other_name'
} as MerchantBackend.Instances.InstanceConfigurationMessage);
});
assertJustExpectedRequestWereMade(env);
env.addRequestExpectation(API_LIST_INSTANCES, {
response: {
instances: [
{
id: 'managed',
name: 'other_name'
} as MerchantBackend.Instances.Instance]
},
});
expect(result.current.query.loading).toBeFalsy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current.query.ok).toBeTruthy();
expect(result.current.query.data).toEqual({
instances: [{
id: 'managed',
name: 'other_name'
}]
});
});
});

View File

@ -1,567 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { renderHook } from "@testing-library/preact-hooks";
import { act } from "preact/test-utils";
import { TestingContext } from ".";
import { MerchantBackend } from "../../../src/declaration.js";
import { useInstanceOrders, useOrderAPI, useOrderDetails } from "../../../src/hooks/order.js";
import {
API_CREATE_ORDER,
API_DELETE_ORDER,
API_FORGET_ORDER_BY_ID,
API_GET_ORDER_BY_ID,
API_LIST_ORDERS, API_REFUND_ORDER_BY_ID, assertJustExpectedRequestWereMade, assertNextRequest, assertNoMoreRequestWereMade, AxiosMockEnvironment
} from "../../axiosMock.js";
describe("order api interaction with listing", () => {
it("should evict cache when creating an order", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: 0, paid: "yes" },
response: {
orders: [{ order_id: "1" } as MerchantBackend.Orders.OrderHistoryEntry],
},
});
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: -20, paid: "yes" },
response: {
orders: [{ order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry],
},
});
const { result, waitForNextUpdate } = renderHook(() => {
const newDate = (d: Date) => {
console.log("new date", d);
};
const query = useInstanceOrders({ paid: "yes" }, newDate);
const api = useOrderAPI();
return { query, api };
}, { wrapper: TestingContext });
expect(result.current).toBeDefined();
if (!result.current) {
return;
}
expect(result.current.query.loading).toBeTruthy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
orders: [{ order_id: "1" }, { order_id: "2" }],
});
env.addRequestExpectation(API_CREATE_ORDER, {
request: {
order: { amount: "ARS:12", summary: "pay me" },
},
response: { order_id: "3" },
});
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: 0, paid: "yes" },
response: {
orders: [{ order_id: "1" } as any],
},
});
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: -20, paid: "yes" },
response: {
orders: [{ order_id: "2" } as any, { order_id: "3" } as any],
},
});
act(async () => {
await result.current?.api.createOrder({
order: { amount: "ARS:12", summary: "pay me" },
} as any);
});
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
orders: [{ order_id: "1" }, { order_id: "2" }, { order_id: "3" }],
});
});
it("should evict cache when doing a refund", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: 0, paid: "yes" },
response: {
orders: [{ order_id: "1", amount: 'EUR:12', refundable: true } as MerchantBackend.Orders.OrderHistoryEntry],
},
});
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: -20, paid: "yes" },
response: { orders: [], },
});
const { result, waitForNextUpdate } = renderHook(() => {
const newDate = (d: Date) => {
console.log("new date", d);
};
const query = useInstanceOrders({ paid: "yes" }, newDate);
const api = useOrderAPI();
return { query, api };
}, { wrapper: TestingContext });
expect(result.current).toBeDefined();
if (!result.current) {
return;
}
expect(result.current.query.loading).toBeTruthy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
orders: [{
order_id: "1",
amount: 'EUR:12',
refundable: true,
}],
});
env.addRequestExpectation(API_REFUND_ORDER_BY_ID('1'), {
request: {
reason: 'double pay',
refund: 'EUR:1'
},
});
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: 0, paid: "yes" },
response: {
orders: [{ order_id: "1", amount: 'EUR:12', refundable: false } as any],
},
});
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: -20, paid: "yes" },
response: { orders: [], },
});
act(async () => {
await result.current?.api.refundOrder('1', {
reason: 'double pay',
refund: 'EUR:1'
});
});
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
orders: [{
order_id: "1",
amount: 'EUR:12',
refundable: false,
}],
});
});
it("should evict cache when deleting an order", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: 0, paid: "yes" },
response: {
orders: [{ order_id: "1" } as MerchantBackend.Orders.OrderHistoryEntry],
},
});
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: -20, paid: "yes" },
response: {
orders: [{ order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry],
},
});
const { result, waitForNextUpdate } = renderHook(() => {
const newDate = (d: Date) => {
console.log("new date", d);
};
const query = useInstanceOrders({ paid: "yes" }, newDate);
const api = useOrderAPI();
return { query, api };
}, { wrapper: TestingContext });
expect(result.current).toBeDefined();
if (!result.current) {
return;
}
expect(result.current.query.loading).toBeTruthy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
orders: [{ order_id: "1" }, { order_id: "2" }],
});
env.addRequestExpectation(API_DELETE_ORDER('1'), {});
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: 0, paid: "yes" },
response: {
orders: [],
},
});
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: -20, paid: "yes" },
response: {
orders: [{ order_id: "2" } as any],
},
});
act(async () => {
await result.current?.api.deleteOrder('1');
});
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
orders: [{ order_id: "2" }],
});
});
});
describe("order api interaction with details", () => {
it("should evict cache when doing a refund", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_GET_ORDER_BY_ID('1'), {
// qparam: { delta: 0, paid: "yes" },
response: {
summary: 'description',
refund_amount: 'EUR:0',
} as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse,
});
const { result, waitForNextUpdate } = renderHook(() => {
const query = useOrderDetails('1')
const api = useOrderAPI();
return { query, api };
}, { wrapper: TestingContext });
expect(result.current).toBeDefined();
if (!result.current) {
return;
}
expect(result.current.query.loading).toBeTruthy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
summary: 'description',
refund_amount: 'EUR:0',
});
env.addRequestExpectation(API_REFUND_ORDER_BY_ID('1'), {
request: {
reason: 'double pay',
refund: 'EUR:1'
},
});
env.addRequestExpectation(API_GET_ORDER_BY_ID('1'), {
response: {
summary: 'description',
refund_amount: 'EUR:1',
} as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse,
});
act(async () => {
await result.current?.api.refundOrder('1', {
reason: 'double pay',
refund: 'EUR:1'
});
});
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
summary: 'description',
refund_amount: 'EUR:1',
});
})
it("should evict cache when doing a forget", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_GET_ORDER_BY_ID('1'), {
// qparam: { delta: 0, paid: "yes" },
response: {
summary: 'description',
refund_amount: 'EUR:0',
} as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse,
});
const { result, waitForNextUpdate } = renderHook(() => {
const query = useOrderDetails('1')
const api = useOrderAPI();
return { query, api };
}, { wrapper: TestingContext });
expect(result.current).toBeDefined();
if (!result.current) {
return;
}
expect(result.current.query.loading).toBeTruthy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
summary: 'description',
refund_amount: 'EUR:0',
});
env.addRequestExpectation(API_FORGET_ORDER_BY_ID('1'), {
request: {
fields: ['$.summary']
},
});
env.addRequestExpectation(API_GET_ORDER_BY_ID('1'), {
response: {
summary: undefined,
} as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse,
});
act(async () => {
await result.current?.api.forgetOrder('1', {
fields: ['$.summary']
});
});
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
summary: undefined,
});
})
})
describe("order listing pagination", () => {
it("should not load more if has reach the end", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: 20, wired: "yes", date_ms: 12 },
response: {
orders: [{ order_id: "1" } as any],
},
});
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: -20, wired: "yes", date_ms: 13 },
response: {
orders: [{ order_id: "2" } as any],
},
});
const { result, waitForNextUpdate } = renderHook(() => {
const newDate = (d: Date) => {
console.log("new date", d);
};
const date = new Date(12);
const query = useInstanceOrders({ wired: "yes", date }, newDate)
return { query }
}, { wrapper: TestingContext });
assertJustExpectedRequestWereMade(env);
await waitForNextUpdate();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
orders: [{ order_id: "1" }, { order_id: "2" }],
});
expect(result.current.query.isReachingEnd).toBeTruthy()
expect(result.current.query.isReachingStart).toBeTruthy()
await act(() => {
if (!result.current?.query.ok) throw Error("not ok");
result.current.query.loadMore();
});
assertNoMoreRequestWereMade(env);
await act(() => {
if (!result.current?.query.ok) throw Error("not ok");
result.current.query.loadMorePrev();
});
assertNoMoreRequestWereMade(env);
expect(result.current.query.data).toEqual({
orders: [
{ order_id: "1" },
{ order_id: "2" },
],
});
});
it("should load more if result brings more that PAGE_SIZE", async () => {
const env = new AxiosMockEnvironment();
const ordersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({ order_id: String(i) }))
const ordersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({ order_id: String(i + 20) }))
const ordersFrom20to0 = [...ordersFrom0to20].reverse()
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: 20, wired: "yes", date_ms: 12 },
response: {
orders: ordersFrom0to20,
},
});
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: -20, wired: "yes", date_ms: 13 },
response: {
orders: ordersFrom20to40,
},
});
const { result, waitForNextUpdate } = renderHook(() => {
const newDate = (d: Date) => {
console.log("new date", d);
};
const date = new Date(12);
const query = useInstanceOrders({ wired: "yes", date }, newDate)
return { query }
}, { wrapper: TestingContext });
assertJustExpectedRequestWereMade(env);
await waitForNextUpdate({ timeout: 1 });
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
orders: [...ordersFrom20to0, ...ordersFrom20to40],
});
expect(result.current.query.isReachingEnd).toBeFalsy()
expect(result.current.query.isReachingStart).toBeFalsy()
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: -40, wired: "yes", date_ms: 13 },
response: {
orders: [...ordersFrom20to40, { order_id: '41' }],
},
});
await act(() => {
if (!result.current?.query.ok) throw Error("not ok");
result.current.query.loadMore();
});
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
env.addRequestExpectation(API_LIST_ORDERS, {
qparam: { delta: 40, wired: "yes", date_ms: 12 },
response: {
orders: [...ordersFrom0to20, { order_id: '-1' }],
},
});
await act(() => {
if (!result.current?.query.ok) throw Error("not ok");
result.current.query.loadMorePrev();
});
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.data).toEqual({
orders: [{ order_id: '-1' }, ...ordersFrom20to0, ...ordersFrom20to40, { order_id: '41' }],
});
});
});

View File

@ -1,338 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { renderHook } from "@testing-library/preact-hooks";
import { act } from "preact/test-utils";
import { TestingContext } from ".";
import { MerchantBackend } from "../../../src/declaration.js";
import { useInstanceProducts, useProductAPI, useProductDetails } from "../../../src/hooks/product.js";
import {
API_CREATE_PRODUCT,
API_DELETE_PRODUCT, API_GET_PRODUCT_BY_ID,
API_LIST_PRODUCTS,
API_UPDATE_PRODUCT_BY_ID,
assertJustExpectedRequestWereMade,
assertNextRequest,
AxiosMockEnvironment
} from "../../axiosMock.js";
describe("product api interaction with listing", () => {
it("should evict cache when creating a product", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_LIST_PRODUCTS, {
response: {
products: [{ product_id: "1234" }],
},
});
env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail,
});
const { result, waitForNextUpdate } = renderHook(
() => {
const query = useInstanceProducts();
const api = useProductAPI();
return { api, query };
},
{ wrapper: TestingContext }
); // get products -> loading
expect(result.current).toBeDefined();
if (!result.current) {
return;
}
expect(result.current.query.loading).toBeTruthy();
await waitForNextUpdate({ timeout: 1 });
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual([
{ id: "1234", price: "ARS:12" },
]);
env.addRequestExpectation(API_CREATE_PRODUCT, {
request: { price: "ARS:23" } as MerchantBackend.Products.ProductAddDetail,
});
env.addRequestExpectation(API_LIST_PRODUCTS, {
response: {
products: [{ product_id: "1234" }, { product_id: "2345" }],
},
});
env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail,
});
env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail,
});
env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), {
response: { price: "ARS:23" } as MerchantBackend.Products.ProductDetail,
});
act(async () => {
await result.current?.api.createProduct({
price: "ARS:23",
} as any);
});
assertNextRequest(env);
await waitForNextUpdate({ timeout: 1 });
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual([
{
id: "1234",
price: "ARS:12",
},
{
id: "2345",
price: "ARS:23",
},
]);
});
it("should evict cache when updating a product", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_LIST_PRODUCTS, {
response: {
products: [{ product_id: "1234" }],
},
});
env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail,
});
const { result, waitForNextUpdate } = renderHook(
() => {
const query = useInstanceProducts();
const api = useProductAPI();
return { api, query };
},
{ wrapper: TestingContext }
); // get products -> loading
expect(result.current).toBeDefined();
if (!result.current) {
return;
}
expect(result.current.query.loading).toBeTruthy();
await waitForNextUpdate({ timeout: 1 });
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual([
{ id: "1234", price: "ARS:12" },
]);
env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("1234"), {
request: { price: "ARS:13" } as MerchantBackend.Products.ProductPatchDetail,
});
env.addRequestExpectation(API_LIST_PRODUCTS, {
response: {
products: [{ product_id: "1234" }],
},
});
env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
response: { price: "ARS:13" } as MerchantBackend.Products.ProductDetail,
});
act(async () => {
await result.current?.api.updateProduct("1234", {
price: "ARS:13",
} as any);
});
assertNextRequest(env);
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual([
{
id: "1234",
price: "ARS:13",
},
]);
});
it("should evict cache when deleting a product", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_LIST_PRODUCTS, {
response: {
products: [{ product_id: "1234" }, { product_id: "2345" }],
},
});
env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail,
});
env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), {
response: { price: "ARS:23" } as MerchantBackend.Products.ProductDetail,
});
const { result, waitForNextUpdate } = renderHook(
() => {
const query = useInstanceProducts();
const api = useProductAPI();
return { api, query };
},
{ wrapper: TestingContext }
); // get products -> loading
expect(result.current).toBeDefined();
if (!result.current) {
return;
}
expect(result.current.query.loading).toBeTruthy();
await waitForNextUpdate({ timeout: 1 });
await waitForNextUpdate({ timeout: 1 });
// await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual([
{ id: "1234", price: "ARS:12" },
{ id: "2345", price: "ARS:23" },
]);
env.addRequestExpectation(API_DELETE_PRODUCT("2345"), {});
env.addRequestExpectation(API_LIST_PRODUCTS, {
response: {
products: [{ product_id: "1234" }],
},
});
env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), {
response: { price: "ARS:13" } as MerchantBackend.Products.ProductDetail,
});
act(async () => {
await result.current?.api.deleteProduct("2345");
});
assertNextRequest(env);
await waitForNextUpdate({ timeout: 1 });
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual([
{
id: "1234",
price: "ARS:13",
},
]);
});
});
describe("product api interaction with details", () => {
it("should evict cache when updating a product", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), {
response: {
description: "this is a description",
} as MerchantBackend.Products.ProductDetail,
});
const { result, waitForNextUpdate } = renderHook(() => {
const query = useProductDetails("12");
const api = useProductAPI();
return { query, api };
}, { wrapper: TestingContext });
expect(result.current).toBeDefined();
if (!result.current) {
return;
}
expect(result.current.query.loading).toBeTruthy();
await waitForNextUpdate();
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
description: "this is a description",
});
env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("12"), {
request: { description: "other description" } as MerchantBackend.Products.ProductPatchDetail,
});
env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), {
response: {
description: "other description",
} as MerchantBackend.Products.ProductDetail,
});
act(async () => {
return await result.current?.api.updateProduct("12", {
description: "other description",
} as any);
});
assertNextRequest(env);
await waitForNextUpdate();
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
description: "other description",
});
})
})

View File

@ -1,470 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { renderHook } from "@testing-library/preact-hooks";
import { act } from "preact/test-utils";
import { MerchantBackend } from "../../../src/declaration.js";
import {
useInstanceReserves,
useReserveDetails,
useReservesAPI,
useTipDetails,
} from "../../../src/hooks/reserves.js";
import {
API_AUTHORIZE_TIP,
API_AUTHORIZE_TIP_FOR_RESERVE,
API_CREATE_RESERVE,
API_DELETE_RESERVE,
API_GET_RESERVE_BY_ID,
API_GET_TIP_BY_ID,
API_LIST_RESERVES,
assertJustExpectedRequestWereMade,
AxiosMockEnvironment,
} from "../../axiosMock.js";
import { TestingContext } from "./index.js";
describe("reserve api interaction with listing", () => {
it("should evict cache when creating a reserve", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_LIST_RESERVES, {
response: {
reserves: [
{
reserve_pub: "11",
} as MerchantBackend.Tips.ReserveStatusEntry,
],
},
});
const { result, waitForNextUpdate } = renderHook(
() => {
const api = useReservesAPI();
const query = useInstanceReserves();
return { query, api };
},
{ wrapper: TestingContext }
);
expect(result.current).toBeDefined();
if (!result.current) {
return;
}
expect(result.current.query.loading).toBeTruthy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
reserves: [{ reserve_pub: "11" }],
});
env.addRequestExpectation(API_CREATE_RESERVE, {
request: {
initial_balance: "ARS:3333",
exchange_url: "http://url",
wire_method: "iban",
},
response: {
reserve_pub: "22",
payto_uri: "payto",
},
});
act(async () => {
await result.current?.api.createReserve({
initial_balance: "ARS:3333",
exchange_url: "http://url",
wire_method: "iban",
});
return;
});
assertJustExpectedRequestWereMade(env);
env.addRequestExpectation(API_LIST_RESERVES, {
response: {
reserves: [
{
reserve_pub: "11",
} as MerchantBackend.Tips.ReserveStatusEntry,
{
reserve_pub: "22",
} as MerchantBackend.Tips.ReserveStatusEntry,
],
},
});
expect(result.current.query.loading).toBeFalsy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current.query.ok).toBeTruthy();
expect(result.current.query.data).toEqual({
reserves: [
{
reserve_pub: "11",
} as MerchantBackend.Tips.ReserveStatusEntry,
{
reserve_pub: "22",
} as MerchantBackend.Tips.ReserveStatusEntry,
],
});
});
it("should evict cache when deleting a reserve", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_LIST_RESERVES, {
response: {
reserves: [
{
reserve_pub: "11",
} as MerchantBackend.Tips.ReserveStatusEntry,
{
reserve_pub: "22",
} as MerchantBackend.Tips.ReserveStatusEntry,
{
reserve_pub: "33",
} as MerchantBackend.Tips.ReserveStatusEntry,
],
},
});
const { result, waitForNextUpdate } = renderHook(
() => {
const api = useReservesAPI();
const query = useInstanceReserves();
return { query, api };
},
{
wrapper: TestingContext,
}
);
expect(result.current).toBeDefined();
if (!result.current) {
return;
}
expect(result.current.query.loading).toBeTruthy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
reserves: [
{ reserve_pub: "11" },
{ reserve_pub: "22" },
{ reserve_pub: "33" },
],
});
env.addRequestExpectation(API_DELETE_RESERVE("11"), {});
act(async () => {
await result.current?.api.deleteReserve("11");
return;
});
assertJustExpectedRequestWereMade(env);
env.addRequestExpectation(API_LIST_RESERVES, {
response: {
reserves: [
{
reserve_pub: "22",
} as MerchantBackend.Tips.ReserveStatusEntry,
{
reserve_pub: "33",
} as MerchantBackend.Tips.ReserveStatusEntry,
],
},
});
expect(result.current.query.loading).toBeFalsy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current.query.ok).toBeTruthy();
expect(result.current.query.data).toEqual({
reserves: [
{
reserve_pub: "22",
} as MerchantBackend.Tips.ReserveStatusEntry,
{
reserve_pub: "33",
} as MerchantBackend.Tips.ReserveStatusEntry,
],
});
});
});
describe("reserve api interaction with details", () => {
it("should evict cache when adding a tip for a specific reserve", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
response: {
payto_uri: "payto://here",
tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }],
} as MerchantBackend.Tips.ReserveDetail,
});
const { result, waitForNextUpdate } = renderHook(
() => {
const api = useReservesAPI();
const query = useReserveDetails("11");
return { query, api };
},
{
wrapper: TestingContext,
}
);
expect(result.current).toBeDefined();
if (!result.current) {
return;
}
expect(result.current.query.loading).toBeTruthy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
payto_uri: "payto://here",
tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }],
});
env.addRequestExpectation(API_AUTHORIZE_TIP_FOR_RESERVE("11"), {
request: {
amount: "USD:12",
justification: "not",
next_url: "http://taler.net",
},
response: {
tip_id: "id2",
taler_tip_uri: "uri",
tip_expiration: { t_s: 1 },
tip_status_url: "url",
},
});
act(async () => {
await result.current?.api.authorizeTipReserve("11", {
amount: "USD:12",
justification: "not",
next_url: "http://taler.net",
});
});
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
response: {
payto_uri: "payto://here",
tips: [
{ reason: "why?", tip_id: "id1", total_amount: "USD:10" },
{ reason: "not", tip_id: "id2", total_amount: "USD:12" },
],
} as MerchantBackend.Tips.ReserveDetail,
});
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current.query.ok).toBeTruthy();
expect(result.current.query.data).toEqual({
payto_uri: "payto://here",
tips: [
{ reason: "why?", tip_id: "id1", total_amount: "USD:10" },
{ reason: "not", tip_id: "id2", total_amount: "USD:12" },
],
});
});
it("should evict cache when adding a tip for a random reserve", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
response: {
payto_uri: "payto://here",
tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }],
} as MerchantBackend.Tips.ReserveDetail,
});
const { result, waitForNextUpdate } = renderHook(
() => {
const api = useReservesAPI();
const query = useReserveDetails("11");
return { query, api };
},
{
wrapper: TestingContext,
}
);
expect(result.current).toBeDefined();
if (!result.current) {
return;
}
expect(result.current.query.loading).toBeTruthy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
payto_uri: "payto://here",
tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }],
});
env.addRequestExpectation(API_AUTHORIZE_TIP, {
request: {
amount: "USD:12",
justification: "not",
next_url: "http://taler.net",
},
response: {
tip_id: "id2",
taler_tip_uri: "uri",
tip_expiration: { t_s: 1 },
tip_status_url: "url",
},
});
act(async () => {
await result.current?.api.authorizeTip({
amount: "USD:12",
justification: "not",
next_url: "http://taler.net",
});
});
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), {
response: {
payto_uri: "payto://here",
tips: [
{ reason: "why?", tip_id: "id1", total_amount: "USD:10" },
{ reason: "not", tip_id: "id2", total_amount: "USD:12" },
],
} as MerchantBackend.Tips.ReserveDetail,
});
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current.query.ok).toBeTruthy();
expect(result.current.query.data).toEqual({
payto_uri: "payto://here",
tips: [
{ reason: "why?", tip_id: "id1", total_amount: "USD:10" },
{ reason: "not", tip_id: "id2", total_amount: "USD:12" },
],
});
});
});
describe("reserve api interaction with tip details", () => {
it("should list tips", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_GET_TIP_BY_ID("11"), {
response: {
total_picked_up: "USD:12",
reason: "not",
} as MerchantBackend.Tips.TipDetails,
});
const { result, waitForNextUpdate } = renderHook(
() => {
// const api = useReservesAPI();
const query = useTipDetails("11");
return { query };
},
{
wrapper: TestingContext,
}
);
expect(result.current).toBeDefined();
if (!result.current) {
return;
}
expect(result.current.query.loading).toBeTruthy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
total_picked_up: "USD:12",
reason: "not",
});
});
});

View File

@ -1,268 +0,0 @@
/*
This file is part of GNU Taler
(C) 2021-2023 Taler Systems S.A.
GNU Taler is free software; you can redistribute it and/or modify it under the
terms of the GNU General Public License as published by the Free Software
Foundation; either version 3, or (at your option) any later version.
GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
/**
*
* @author Sebastian Javier Marchano (sebasjm)
*/
import { act, renderHook } from "@testing-library/preact-hooks";
import { TestingContext } from "./index.js";
import { useInstanceTransfers, useTransferAPI } from "../../../src/hooks/transfer.js";
import {
API_INFORM_TRANSFERS,
API_LIST_TRANSFERS,
assertJustExpectedRequestWereMade,
assertNoMoreRequestWereMade,
AxiosMockEnvironment,
} from "../../axiosMock.js";
import { MerchantBackend } from "../../../src/declaration.js";
describe("transfer api interaction with listing", () => {
it("should evict cache when informing a transfer", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_LIST_TRANSFERS, {
qparam: { limit: 0 },
response: {
transfers: [{ wtid: "2" } as MerchantBackend.Transfers.TransferDetails],
},
});
// FIXME: is this query really needed? if the hook is rendered without
// position argument then then backend is returning the newest and no need
// to this second query
env.addRequestExpectation(API_LIST_TRANSFERS, {
qparam: { limit: -20 },
response: {
transfers: [],
},
});
const { result, waitForNextUpdate } = renderHook(() => {
const moveCursor = (d: string) => {
console.log("new position", d);
};
const query = useInstanceTransfers({}, moveCursor);
const api = useTransferAPI();
return { query, api };
}, { wrapper: TestingContext });
expect(result.current).toBeDefined();
if (!result.current) {
return;
}
expect(result.current.query.loading).toBeTruthy();
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current.query.ok).toBeTruthy();
if (!result.current.query.ok) return;
expect(result.current.query.data).toEqual({
transfers: [{ wtid: "2" }],
});
env.addRequestExpectation(API_INFORM_TRANSFERS, {
request: {
wtid: '3',
credit_amount: 'EUR:1',
exchange_url: 'exchange.url',
payto_uri: 'payto://'
},
response: { total: '' } as any,
});
env.addRequestExpectation(API_LIST_TRANSFERS, {
qparam: { limit: 0 },
response: {
transfers: [{ wtid: "2" } as any, { wtid: "3" } as any],
},
});
env.addRequestExpectation(API_LIST_TRANSFERS, {
qparam: { limit: -20 },
response: {
transfers: [],
},
});
act(async () => {
await result.current?.api.informTransfer({
wtid: '3',
credit_amount: 'EUR:1',
exchange_url: 'exchange.url',
payto_uri: 'payto://'
});
});
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.loading).toBeFalsy();
expect(result.current.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
transfers: [{ wtid: "3" }, { wtid: "2" }],
});
});
});
describe("transfer listing pagination", () => {
it("should not load more if has reach the end", async () => {
const env = new AxiosMockEnvironment();
env.addRequestExpectation(API_LIST_TRANSFERS, {
qparam: { limit: 0, payto_uri: 'payto://' },
response: {
transfers: [{ wtid: "2" } as any],
},
});
env.addRequestExpectation(API_LIST_TRANSFERS, {
qparam: { limit: -20, payto_uri: 'payto://' },
response: {
transfers: [{ wtid: "1" } as any],
},
});
const { result, waitForNextUpdate } = renderHook(() => {
const moveCursor = (d: string) => {
console.log("new position", d);
};
const query = useInstanceTransfers({ payto_uri: 'payto://' }, moveCursor)
return { query }
}, { wrapper: TestingContext });
assertJustExpectedRequestWereMade(env);
await waitForNextUpdate();
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
transfers: [{ wtid: "2" }, { wtid: "1" }],
});
expect(result.current.query.isReachingEnd).toBeTruthy()
expect(result.current.query.isReachingStart).toBeTruthy()
await act(() => {
if (!result.current?.query.ok) throw Error("not ok");
result.current.query.loadMore();
});
assertNoMoreRequestWereMade(env);
await act(() => {
if (!result.current?.query.ok) throw Error("not ok");
result.current.query.loadMorePrev();
});
assertNoMoreRequestWereMade(env);
expect(result.current.query.data).toEqual({
transfers: [
{ wtid: "2" },
{ wtid: "1" },
],
});
});
it("should load more if result brings more that PAGE_SIZE", async () => {
const env = new AxiosMockEnvironment();
const transfersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({ wtid: String(i) }))
const transfersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({ wtid: String(i + 20) }))
const transfersFrom20to0 = [...transfersFrom0to20].reverse()
env.addRequestExpectation(API_LIST_TRANSFERS, {
qparam: { limit: 20, payto_uri: 'payto://' },
response: {
transfers: transfersFrom0to20,
},
});
env.addRequestExpectation(API_LIST_TRANSFERS, {
qparam: { limit: -20, payto_uri: 'payto://' },
response: {
transfers: transfersFrom20to40,
},
});
const { result, waitForNextUpdate } = renderHook(() => {
const moveCursor = (d: string) => {
console.log("new position", d);
};
const query = useInstanceTransfers({ payto_uri: 'payto://', position: '1' }, moveCursor)
return { query }
}, { wrapper: TestingContext });
assertJustExpectedRequestWereMade(env);
await waitForNextUpdate({ timeout: 1 });
expect(result.current?.query.ok).toBeTruthy();
if (!result.current?.query.ok) return;
expect(result.current.query.data).toEqual({
transfers: [...transfersFrom20to0, ...transfersFrom20to40],
});
expect(result.current.query.isReachingEnd).toBeFalsy()
expect(result.current.query.isReachingStart).toBeFalsy()
env.addRequestExpectation(API_LIST_TRANSFERS, {
qparam: { limit: -40, payto_uri: 'payto://', offset: "1" },
response: {
transfers: [...transfersFrom20to40, { wtid: '41' }],
},
});
await act(() => {
if (!result.current?.query.ok) throw Error("not ok");
result.current.query.loadMore();
});
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
env.addRequestExpectation(API_LIST_TRANSFERS, {
qparam: { limit: 40, payto_uri: 'payto://', offset: "1" },
response: {
transfers: [...transfersFrom0to20, { wtid: '-1' }],
},
});
await act(() => {
if (!result.current?.query.ok) throw Error("not ok");
result.current.query.loadMorePrev();
});
await waitForNextUpdate({ timeout: 1 });
assertJustExpectedRequestWereMade(env);
expect(result.current.query.data).toEqual({
transfers: [{ wtid: '-1' }, ...transfersFrom20to0, ...transfersFrom20to40, { wtid: '41' }],
});
});
});

View File

@ -343,15 +343,12 @@ importers:
'@gnu-taler/pogen': ^0.0.5
'@gnu-taler/taler-util': workspace:*
'@gnu-taler/web-util': workspace:*
'@testing-library/preact': ^2.0.1
'@testing-library/preact-hooks': ^1.1.0
'@types/chai': ^4.3.0
'@types/history': ^4.7.8
'@types/jest': ^26.0.23
'@types/mocha': ^8.2.3
'@types/node': ^18.11.17
'@typescript-eslint/eslint-plugin': ^4.22.0
'@typescript-eslint/parser': ^4.22.0
axios: ^0.21.1
base64-inline-loader: ^1.1.1
bulma: ^0.9.2
bulma-checkbox: ^1.1.1
@ -360,6 +357,7 @@ importers:
bulma-switch-control: ^1.1.1
bulma-timeline: ^3.0.4
bulma-upload-control: ^1.2.0
chai: ^4.3.6
date-fns: 2.29.3
dotenv: ^8.2.0
eslint: ^7.25.0
@ -371,8 +369,6 @@ importers:
html-webpack-skip-assets-plugin: ^1.0.1
inline-chunk-html-plugin: ^1.1.1
jed: 1.1.1
jest: ^26.6.3
jest-preset-preact: ^4.0.2
mocha: ^9.2.0
preact: 10.11.3
preact-render-to-string: ^5.2.6
@ -380,6 +376,7 @@ importers:
qrcode-generator: 1.4.4
rimraf: ^3.0.2
sass: 1.56.1
source-map-support: ^0.5.21
swr: 1.3.0
typedoc: ^0.20.36
typescript: 4.8.4
@ -387,7 +384,6 @@ importers:
dependencies:
'@gnu-taler/taler-util': link:../taler-util
'@gnu-taler/web-util': link:../web-util
axios: 0.21.4
date-fns: 2.29.3
history: 4.10.1
jed: 1.1.1
@ -399,10 +395,8 @@ importers:
devDependencies:
'@creativebulma/bulma-tooltip': 1.2.0
'@gnu-taler/pogen': link:../pogen
'@testing-library/preact': 2.0.1_preact@10.11.3
'@testing-library/preact-hooks': 1.1.0_eng4adldpgibddgycwaukopxga
'@types/chai': 4.3.3
'@types/history': 4.7.11
'@types/jest': 26.0.24
'@types/mocha': 8.2.3
'@types/node': 18.11.17
'@typescript-eslint/eslint-plugin': 4.33.0_k4l66av2tbo6kxzw52jzgbfzii
@ -415,20 +409,20 @@ importers:
bulma-switch-control: 1.2.2
bulma-timeline: 3.0.5
bulma-upload-control: 1.2.0
chai: 4.3.6
dotenv: 8.6.0
eslint: 7.32.0
eslint-config-preact: 1.3.0_nxlzr75jbqkso2fds5zjovs2ii
eslint-config-preact: 1.3.0_pycg7frr72nxxf2dj537ozbyqq
eslint-plugin-header: 3.1.1_eslint@7.32.0
html-webpack-inline-chunk-plugin: 1.1.1
html-webpack-inline-source-plugin: 0.0.10
html-webpack-skip-assets-plugin: 1.0.3
inline-chunk-html-plugin: 1.1.1
jest: 26.6.3
jest-preset-preact: 4.0.5_w5bq6jgm3cbfmbu2zwqko4iate
mocha: 9.2.2
preact-render-to-string: 5.2.6_preact@10.11.3
rimraf: 3.0.2
sass: 1.56.1
source-map-support: 0.5.21
typedoc: 0.20.37_typescript@4.8.4
typescript: 4.8.4
@ -5814,16 +5808,6 @@ packages:
preact: 10.11.2
dev: true
/@testing-library/preact-hooks/1.1.0_eng4adldpgibddgycwaukopxga:
resolution: {integrity: sha512-+JIor+NsOHkK3oIrwMDGKGHXTN0JJi462dBJlj4FNbGaDPTlctE6eu2ranWQirh7/FJMkWfzQCP+tk7jmY8ZrQ==}
peerDependencies:
'@testing-library/preact': ^2.0.0
preact: ^10.4.8
dependencies:
'@testing-library/preact': 2.0.1_preact@10.11.3
preact: 10.11.3
dev: true
/@testing-library/preact/2.0.1_preact@10.11.2:
resolution: {integrity: sha512-79kwVOY+3caoLgaPbiPzikjgY0Aya7Fc7TvGtR1upCnz2wrtmPDnN2t9vO7I7vDP2zoA+feSwOH5Q0BFErhaaQ==}
engines: {node: '>= 10'}
@ -5834,16 +5818,6 @@ packages:
preact: 10.11.2
dev: true
/@testing-library/preact/2.0.1_preact@10.11.3:
resolution: {integrity: sha512-79kwVOY+3caoLgaPbiPzikjgY0Aya7Fc7TvGtR1upCnz2wrtmPDnN2t9vO7I7vDP2zoA+feSwOH5Q0BFErhaaQ==}
engines: {node: '>= 10'}
peerDependencies:
preact: '>=10 || ^10.0.0-alpha.0 || ^10.0.0-beta.0'
dependencies:
'@testing-library/dom': 7.31.2
preact: 10.11.3
dev: true
/@tootallnate/once/1.1.2:
resolution: {integrity: sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==}
engines: {node: '>= 6'}
@ -11268,6 +11242,28 @@ packages:
- typescript
dev: true
/eslint-config-preact/1.3.0_pycg7frr72nxxf2dj537ozbyqq:
resolution: {integrity: sha512-yHYXg5qNzEJd3D/30AmsIW0W8MuY858KpApXp7xxBF08IYUljSKCOqMx+dVucXHQnAm7+11wOnMkgVHIBAechw==}
peerDependencies:
eslint: 6.x || 7.x || 8.x
dependencies:
'@babel/core': 7.18.9
'@babel/eslint-parser': 7.19.1_o5peei4wpze5egwf42u76kwdva
'@babel/plugin-syntax-class-properties': 7.12.13_@babel+core@7.18.9
'@babel/plugin-syntax-decorators': 7.19.0_@babel+core@7.18.9
'@babel/plugin-syntax-jsx': 7.18.6_@babel+core@7.18.9
eslint: 7.32.0
eslint-plugin-compat: 4.0.2_eslint@7.32.0
eslint-plugin-jest: 25.7.0_pycg7frr72nxxf2dj537ozbyqq
eslint-plugin-react: 7.31.10_eslint@7.32.0
eslint-plugin-react-hooks: 4.6.0_eslint@7.32.0
transitivePeerDependencies:
- '@typescript-eslint/eslint-plugin'
- jest
- supports-color
- typescript
dev: true
/eslint-config-preact/1.3.0_qqbgcrpnpybc6dh47gt272vyy4:
resolution: {integrity: sha512-yHYXg5qNzEJd3D/30AmsIW0W8MuY858KpApXp7xxBF08IYUljSKCOqMx+dVucXHQnAm7+11wOnMkgVHIBAechw==}
peerDependencies:
@ -11430,6 +11426,27 @@ packages:
- typescript
dev: true
/eslint-plugin-jest/25.7.0_pycg7frr72nxxf2dj537ozbyqq:
resolution: {integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
peerDependencies:
'@typescript-eslint/eslint-plugin': ^4.0.0 || ^5.0.0
eslint: ^6.0.0 || ^7.0.0 || ^8.0.0
jest: '*'
peerDependenciesMeta:
'@typescript-eslint/eslint-plugin':
optional: true
jest:
optional: true
dependencies:
'@typescript-eslint/eslint-plugin': 4.33.0_k4l66av2tbo6kxzw52jzgbfzii
'@typescript-eslint/experimental-utils': 5.41.0_3rubbgt5ekhqrcgx4uwls3neim
eslint: 7.32.0
transitivePeerDependencies:
- supports-color
- typescript
dev: true
/eslint-plugin-jest/25.7.0_qqbgcrpnpybc6dh47gt272vyy4:
resolution: {integrity: sha512-PWLUEXeeF7C9QGKqvdSbzLOiLTx+bno7/HC9eefePfEb257QFHg7ye3dh80AZVkaa/RQsBB1Q/ORQvg2X7F0NQ==}
engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0}
@ -14582,30 +14599,6 @@ packages:
- supports-color
dev: true
/jest-preset-preact/4.0.5_w5bq6jgm3cbfmbu2zwqko4iate:
resolution: {integrity: sha512-MnU7mfpnwopJkdx0WoEyRmrNDIvRN+w6sOur0zEhaRYYMo0gJM7UdZHWTV8k6uo0+ypY+m0kQW6kMukUx4v8JQ==}
peerDependencies:
jest: 26.x || 27.x
preact: 10.x
preact-render-to-string: 5.x
dependencies:
'@babel/core': 7.18.9
'@babel/plugin-proposal-class-properties': 7.18.6_@babel+core@7.18.9
'@babel/plugin-transform-react-jsx': 7.19.0_@babel+core@7.18.9
'@babel/preset-env': 7.18.9_@babel+core@7.18.9
'@babel/preset-typescript': 7.18.6_@babel+core@7.18.9
babel-jest: 27.5.1_@babel+core@7.18.9
identity-obj-proxy: 3.0.0
isomorphic-unfetch: 3.1.0
jest: 26.6.3
jest-watch-typeahead: 0.6.5_jest@26.6.3
preact: 10.11.3
preact-render-to-string: 5.2.6_preact@10.11.3
transitivePeerDependencies:
- encoding
- supports-color
dev: true
/jest-regex-util/26.0.0:
resolution: {integrity: sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==}
engines: {node: '>= 10.14.2'}