diff options
Diffstat (limited to 'packages/merchant-backoffice-ui/tests')
19 files changed, 3604 insertions, 0 deletions
diff --git a/packages/merchant-backoffice-ui/tests/__mocks__/browserMocks.ts b/packages/merchant-backoffice-ui/tests/__mocks__/browserMocks.ts new file mode 100644 index 000000000..ee6bba505 --- /dev/null +++ b/packages/merchant-backoffice-ui/tests/__mocks__/browserMocks.ts @@ -0,0 +1,42 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE.  See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/> + */ + + /** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +// Mock Browser API's which are not supported by JSDOM, e.g. ServiceWorker, LocalStorage +/** + * An example how to mock localStorage is given below 👇 + */ + +/*  +// Mocks localStorage +const localStorageMock = (function() { +	let store = {}; + +	return { +		getItem: (key) => store[key] || null, +		setItem: (key, value) => store[key] = value.toString(), +		clear: () => store = {} +	}; + +})(); + +Object.defineProperty(window, 'localStorage', { +	value: localStorageMock +}); */ diff --git a/packages/merchant-backoffice-ui/tests/__mocks__/fileMocks.ts b/packages/merchant-backoffice-ui/tests/__mocks__/fileMocks.ts new file mode 100644 index 000000000..0c045e9d1 --- /dev/null +++ b/packages/merchant-backoffice-ui/tests/__mocks__/fileMocks.ts @@ -0,0 +1,24 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE.  See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/> + */ + + /** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +// This fixed an error related to the CSS and loading gif breaking my Jest test +// See https://facebook.github.io/jest/docs/en/webpack.html#handling-static-assets +export default 'test-file-stub'; diff --git a/packages/merchant-backoffice-ui/tests/__mocks__/fileTransformer.js b/packages/merchant-backoffice-ui/tests/__mocks__/fileTransformer.js new file mode 100644 index 000000000..e6193f8fd --- /dev/null +++ b/packages/merchant-backoffice-ui/tests/__mocks__/fileTransformer.js @@ -0,0 +1,31 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE.  See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/> + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ +// fileTransformer.js + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const path = require('path'); + +module.exports = { +    process(src, filename, config, options) { +        return 'module.exports = ' + JSON.stringify(path.basename(filename)) + ';'; +    }, +}; + diff --git a/packages/merchant-backoffice-ui/tests/__mocks__/setupTests.ts b/packages/merchant-backoffice-ui/tests/__mocks__/setupTests.ts new file mode 100644 index 000000000..b08eb7fe6 --- /dev/null +++ b/packages/merchant-backoffice-ui/tests/__mocks__/setupTests.ts @@ -0,0 +1,28 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE.  See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import "regenerator-runtime/runtime"; +// import { configure } from 'enzyme'; +// import Adapter from 'enzyme-adapter-preact-pure'; + +// configure({ +//     adapter: new Adapter() +// }); diff --git a/packages/merchant-backoffice-ui/tests/axiosMock.ts b/packages/merchant-backoffice-ui/tests/axiosMock.ts new file mode 100644 index 000000000..13ddab598 --- /dev/null +++ b/packages/merchant-backoffice-ui/tests/axiosMock.ts @@ -0,0 +1,445 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE.  See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/> + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ +import * as axios from 'axios'; +import { MerchantBackend } from '../src/declaration'; +import { mockAxiosOnce, setAxiosRequestAsTestingEnvironment } from '../src/utils/switchableAxios'; +// import { mockAxiosOnce, setAxiosRequestAsTestingEnvironment } from "../src/hooks/backend"; + +export type Query<Req, Res> = (GetQuery | PostQuery | DeleteQuery | PatchQuery) & RequestResponse<Req, Res> + +interface RequestResponse<Req, Res> { +  code?: number, +} +interface GetQuery { get: string } +interface PostQuery { post: string } +interface DeleteQuery { delete: string } +interface PatchQuery { patch: string } + + +const JEST_DEBUG_LOG = process.env['JEST_DEBUG_LOG'] !== undefined + +type ExpectationValues = { query: Query<any, any>; params?: { auth?: string, request?: any, qparam?: any, response?: any } } + +type TestValues = [axios.AxiosRequestConfig | undefined, ExpectationValues | undefined] + +const defaultCallback = (actualQuery?: axios.AxiosRequestConfig): axios.AxiosPromise<any> => { +  if (JEST_DEBUG_LOG) { +    console.log('UNEXPECTED QUERY', actualQuery) +  } +  throw Error('Default Axios mock callback is called, this mean that the test did a tried to use axios but there was no expectation in place, try using JEST_DEBUG_LOG env') +} + +setAxiosRequestAsTestingEnvironment( +  defaultCallback +); + +export class AxiosMockEnvironment { +  expectations: Array<{ +    query: Query<any, any>, +    auth?: string, +    params?: { request?: any, qparam?: any, response?: any }, +    result: { args: axios.AxiosRequestConfig | undefined } +  } | undefined> = [] +  // axiosMock: jest.MockedFunction<axios.AxiosStatic> + +  addRequestExpectation<RequestType, ResponseType>(expectedQuery: Query<RequestType, ResponseType>, params: { auth?: string, request?: RequestType, qparam?: any, response?: ResponseType }): void { +    const result = mockAxiosOnce(function (actualQuery?: axios.AxiosRequestConfig): axios.AxiosPromise { + +      if (JEST_DEBUG_LOG) { +        console.log('query to the backend is made', actualQuery) +      } +      if (!expectedQuery) { +        return Promise.reject("a query was made but it was not expected") +      } +      if (JEST_DEBUG_LOG) { +        console.log('expected query:', params?.request) +        console.log('expected qparams:', params?.qparam) +        console.log('sending response:', params?.response) +      } + +      const responseCode = expectedQuery.code || 200 + +      //This response is what buildRequestOk is expecting in file hook/backend.ts +      if (responseCode >= 200 && responseCode < 300) { +        return Promise.resolve({ +          data: params?.response, config: { +            data: params?.response, +            params: actualQuery?.params || {}, +          }, request: { params: actualQuery?.params || {} } +        } as any); +      } +      //This response is what buildRequestFailed is expecting in file hook/backend.ts +      return Promise.reject({ +        response: { +          status: responseCode +        }, +        request: { +          data: params?.response, +          params: actualQuery?.params || {}, +        } +      }) + +    } as any) + +    this.expectations.push(expectedQuery ? { query: expectedQuery, params, result } : undefined) +  } + +  getLastTestValues(): TestValues { +    const expectedQuery = this.expectations.shift() + +    return [ +      expectedQuery?.result.args, expectedQuery +    ] +  } + +} + +export function assertJustExpectedRequestWereMade(env: AxiosMockEnvironment): void { +  let size = env.expectations.length +  while (size-- > 0) { +    assertNextRequest(env) +  } +  assertNoMoreRequestWereMade(env) +} + +export function assertNoMoreRequestWereMade(env: AxiosMockEnvironment): void { +  const [actualQuery, expectedQuery] = env.getLastTestValues() + +  expect(actualQuery).toBeUndefined(); +  expect(expectedQuery).toBeUndefined(); +} + +export function assertNextRequest(env: AxiosMockEnvironment): void { +  const [actualQuery, expectedQuery] = env.getLastTestValues() + +  if (!actualQuery) { +    //expected one query but the tested component didn't execute one +    expect(actualQuery).toBe(expectedQuery); +    return +  } + +  if (!expectedQuery) { +    const errorMessage = 'a query was made to the backend but the test explicitly expected no query'; +    if (JEST_DEBUG_LOG) { +      console.log(errorMessage, actualQuery) +    } +    throw Error(errorMessage) +  } +  if ('get' in expectedQuery.query) { +    expect(actualQuery.method).toBe('get'); +    expect(actualQuery.url).toBe(expectedQuery.query.get); +  } +  if ('post' in expectedQuery.query) { +    expect(actualQuery.method).toBe('post'); +    expect(actualQuery.url).toBe(expectedQuery.query.post); +  } +  if ('delete' in expectedQuery.query) { +    expect(actualQuery.method).toBe('delete'); +    expect(actualQuery.url).toBe(expectedQuery.query.delete); +  } +  if ('patch' in expectedQuery.query) { +    expect(actualQuery.method).toBe('patch'); +    expect(actualQuery.url).toBe(expectedQuery.query.patch); +  } + +  if (expectedQuery.params?.request) { +    expect(actualQuery.data).toMatchObject(expectedQuery.params.request) +  } +  if (expectedQuery.params?.qparam) { +    expect(actualQuery.params).toMatchObject(expectedQuery.params.qparam) +  } + +  if (expectedQuery.params?.auth) { +    expect(actualQuery.headers.Authorization).toBe(expectedQuery.params?.auth) +  } + +} + +//////////////////// +// ORDER +//////////////////// + +export const API_CREATE_ORDER: Query< +  MerchantBackend.Orders.PostOrderRequest, +  MerchantBackend.Orders.PostOrderResponse +> = { +  post: "http://backend/instances/default/private/orders", +}; + +export const API_GET_ORDER_BY_ID = ( +  id: string +): Query< +  unknown, +  MerchantBackend.Orders.MerchantOrderStatusResponse +> => ({ +  get: `http://backend/instances/default/private/orders/${id}`, +}); + +export const API_LIST_ORDERS: Query< +  unknown, +  MerchantBackend.Orders.OrderHistory +> = { +  get: "http://backend/instances/default/private/orders", +}; + +export const API_REFUND_ORDER_BY_ID = ( +  id: string +): Query< +  MerchantBackend.Orders.RefundRequest, +  MerchantBackend.Orders.MerchantRefundResponse +> => ({ +  post: `http://backend/instances/default/private/orders/${id}/refund`, +}); + +export const API_FORGET_ORDER_BY_ID = ( +  id: string +): Query< +  MerchantBackend.Orders.ForgetRequest, +  unknown +> => ({ +  patch: `http://backend/instances/default/private/orders/${id}/forget`, +}); + +export const API_DELETE_ORDER = ( +  id: string +): Query< +  MerchantBackend.Orders.ForgetRequest, +  unknown +> => ({ +  delete: `http://backend/instances/default/private/orders/${id}`, +}); + +//////////////////// +// TRANSFER +//////////////////// + +export const API_LIST_TRANSFERS: Query< +  unknown, +  MerchantBackend.Transfers.TransferList +> = { +  get: "http://backend/instances/default/private/transfers", +}; + +export const API_INFORM_TRANSFERS: Query< +  MerchantBackend.Transfers.TransferInformation, +  MerchantBackend.Transfers.MerchantTrackTransferResponse +> = { +  post: "http://backend/instances/default/private/transfers", +}; + +//////////////////// +// PRODUCT +//////////////////// + +export const API_CREATE_PRODUCT: Query< +  MerchantBackend.Products.ProductAddDetail, +  unknown +> = { +  post: "http://backend/instances/default/private/products", +}; + +export const API_LIST_PRODUCTS: Query< +  unknown, +  MerchantBackend.Products.InventorySummaryResponse +> = { +  get: "http://backend/instances/default/private/products", +}; + +export const API_GET_PRODUCT_BY_ID = ( +  id: string +): Query<unknown, MerchantBackend.Products.ProductDetail> => ({ +  get: `http://backend/instances/default/private/products/${id}`, +}); + +export const API_UPDATE_PRODUCT_BY_ID = ( +  id: string +): Query< +  MerchantBackend.Products.ProductPatchDetail, +  MerchantBackend.Products.InventorySummaryResponse +> => ({ +  patch: `http://backend/instances/default/private/products/${id}`, +}); + +export const API_DELETE_PRODUCT = ( +  id: string +): Query< +  unknown, unknown +> => ({ +  delete: `http://backend/instances/default/private/products/${id}`, +}); + +//////////////////// +// RESERVES +//////////////////// + +export const API_CREATE_RESERVE: Query< +  MerchantBackend.Tips.ReserveCreateRequest, +  MerchantBackend.Tips.ReserveCreateConfirmation +> = { +  post: "http://backend/instances/default/private/reserves", +}; +export const API_LIST_RESERVES: Query< +  unknown, +  MerchantBackend.Tips.TippingReserveStatus +> = { +  get: "http://backend/instances/default/private/reserves", +}; + +export const API_GET_RESERVE_BY_ID = ( +  pub: string +): Query<unknown, MerchantBackend.Tips.ReserveDetail> => ({ +  get: `http://backend/instances/default/private/reserves/${pub}`, +}); + +export const API_GET_TIP_BY_ID = ( +  pub: string +): Query< +  unknown, +  MerchantBackend.Tips.TipDetails +> => ({ +  get: `http://backend/instances/default/private/tips/${pub}`, +}); + +export const API_AUTHORIZE_TIP_FOR_RESERVE = ( +  pub: string +): Query< +  MerchantBackend.Tips.TipCreateRequest, +  MerchantBackend.Tips.TipCreateConfirmation +> => ({ +  post: `http://backend/instances/default/private/reserves/${pub}/authorize-tip`, +}); + +export const API_AUTHORIZE_TIP: Query< +  MerchantBackend.Tips.TipCreateRequest, +  MerchantBackend.Tips.TipCreateConfirmation +> = ({ +  post: `http://backend/instances/default/private/tips`, +}); + + +export const API_DELETE_RESERVE = ( +  id: string +): Query<unknown, unknown> => ({ +  delete: `http://backend/instances/default/private/reserves/${id}`, +}); + + +//////////////////// +// INSTANCE ADMIN +//////////////////// + +export const API_CREATE_INSTANCE: Query< +  MerchantBackend.Instances.InstanceConfigurationMessage, +  unknown +> = { +  post: "http://backend/management/instances", +}; + +export const API_GET_INSTANCE_BY_ID = ( +  id: string +): Query< +  unknown, +  MerchantBackend.Instances.QueryInstancesResponse +> => ({ +  get: `http://backend/management/instances/${id}`, +}); + +export const API_GET_INSTANCE_KYC_BY_ID = ( +  id: string +): Query< +  unknown, +  MerchantBackend.Instances.AccountKycRedirects +> => ({ +  get: `http://backend/management/instances/${id}/kyc`, +}); + +export const API_LIST_INSTANCES: Query< +  unknown, +  MerchantBackend.Instances.InstancesResponse +> = { +  get: "http://backend/management/instances", +}; + +export const API_UPDATE_INSTANCE_BY_ID = ( +  id: string +): Query< +  MerchantBackend.Instances.InstanceReconfigurationMessage, +  unknown +> => ({ +  patch: `http://backend/management/instances/${id}`, +}); + +export const API_UPDATE_INSTANCE_AUTH_BY_ID = ( +  id: string +): Query< +  MerchantBackend.Instances.InstanceAuthConfigurationMessage, +  unknown +> => ({ +  post: `http://backend/management/instances/${id}/auth`, +}); + +export const API_DELETE_INSTANCE = ( +  id: string +): Query<unknown, unknown> => ({ +  delete: `http://backend/management/instances/${id}`, +}); + +//////////////////// +// INSTANCE  +//////////////////// + +export const API_GET_CURRENT_INSTANCE: Query< +  unknown, +  MerchantBackend.Instances.QueryInstancesResponse +> = ({ +  get: `http://backend/instances/default/private/`, +}); + +export const API_GET_CURRENT_INSTANCE_KYC: Query< +  unknown, +  MerchantBackend.Instances.AccountKycRedirects +> = +  ({ +    get: `http://backend/instances/default/private/kyc`, +  }); + +export const API_UPDATE_CURRENT_INSTANCE: Query< +  MerchantBackend.Instances.InstanceReconfigurationMessage, +  unknown +> = { +  patch: `http://backend/instances/default/private/`, +}; + +export const API_UPDATE_CURRENT_INSTANCE_AUTH: Query< +  MerchantBackend.Instances.InstanceAuthConfigurationMessage, +  unknown +> = { +  post: `http://backend/instances/default/private/auth`, +}; + +export const API_DELETE_CURRENT_INSTANCE: Query< +  unknown, +  unknown +> = ({ +  delete: `http://backend/instances/default/private`, +}); + + diff --git a/packages/merchant-backoffice-ui/tests/context/backend.test.tsx b/packages/merchant-backoffice-ui/tests/context/backend.test.tsx new file mode 100644 index 000000000..b7b50fd47 --- /dev/null +++ b/packages/merchant-backoffice-ui/tests/context/backend.test.tsx @@ -0,0 +1,172 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE.  See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { renderHook } from "@testing-library/preact-hooks"; +import { ComponentChildren, h, VNode } from "preact"; +import { act } from "preact/test-utils"; +import { BackendContextProvider } from "../../src/context/backend"; +import { InstanceContextProvider } from "../../src/context/instance"; +import { MerchantBackend } from "../../src/declaration"; +import { +  useAdminAPI, +  useInstanceAPI, +  useManagementAPI, +} from "../../src/hooks/instance"; +import { +  API_CREATE_INSTANCE, +  API_GET_CURRENT_INSTANCE, +  API_UPDATE_CURRENT_INSTANCE_AUTH, +  API_UPDATE_INSTANCE_AUTH_BY_ID, +  assertJustExpectedRequestWereMade, +  AxiosMockEnvironment, +} from "../axiosMock"; + +interface TestingContextProps { +  children?: ComponentChildren; +} + +function TestingContext({ children }: TestingContextProps): VNode { +  return ( +    <BackendContextProvider defaultUrl="http://backend" initialToken="token"> +      {children} +    </BackendContextProvider> +  ); +} +function AdminTestingContext({ children }: TestingContextProps): VNode { +  return ( +    <BackendContextProvider defaultUrl="http://backend" initialToken="token"> +      <InstanceContextProvider +        value={{ +          token: "token", +          id: "default", +          admin: true, +          changeToken: () => null, +        }} +      > +        {children} +      </InstanceContextProvider> +    </BackendContextProvider> +  ); +} + +describe("backend context api ", () => { +  it("should use new token after updating the instance token in the settings as user", async () => { +    const env = new AxiosMockEnvironment(); + +    const { result, waitForNextUpdate } = renderHook( +      () => { +        const instance = useInstanceAPI(); +        const management = useManagementAPI("default"); +        const admin = useAdminAPI(); + +        return { instance, management, admin }; +      }, +      { wrapper: TestingContext } +    ); + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } + +    env.addRequestExpectation(API_UPDATE_INSTANCE_AUTH_BY_ID("default"), { +      request: { +        method: "token", +        token: "another_token", +      }, +      response: { +        name: "instance_name", +      } as MerchantBackend.Instances.QueryInstancesResponse, +    }); + +    await act(async () => { +      await result.current?.management.setNewToken("another_token"); +    }); + +    // await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    env.addRequestExpectation(API_CREATE_INSTANCE, { +      auth: "Bearer another_token", +      request: { +        id: "new_instance_id", +      } as MerchantBackend.Instances.InstanceConfigurationMessage, +    }); + +    result.current.admin.createInstance({ +      id: "new_instance_id", +    } as MerchantBackend.Instances.InstanceConfigurationMessage); + +    assertJustExpectedRequestWereMade(env); +  }); + +  it("should use new token after updating the instance token in the settings as admin", async () => { +    const env = new AxiosMockEnvironment(); + +    const { result, waitForNextUpdate } = renderHook( +      () => { +        const instance = useInstanceAPI(); +        const management = useManagementAPI("default"); +        const admin = useAdminAPI(); + +        return { instance, management, admin }; +      }, +      { wrapper: AdminTestingContext } +    ); + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } + +    env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { +      request: { +        method: "token", +        token: "another_token", +      }, +      response: { +        name: "instance_name", +      } as MerchantBackend.Instances.QueryInstancesResponse, +    }); + +    await act(async () => { +      await result.current?.instance.setNewToken("another_token"); +    }); + +    // await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    env.addRequestExpectation(API_CREATE_INSTANCE, { +      auth: "Bearer another_token", +      request: { +        id: "new_instance_id", +      } as MerchantBackend.Instances.InstanceConfigurationMessage, +    }); + +    result.current.admin.createInstance({ +      id: "new_instance_id", +    } as MerchantBackend.Instances.InstanceConfigurationMessage); + +    assertJustExpectedRequestWereMade(env); +  }); +}); diff --git a/packages/merchant-backoffice-ui/tests/declarations.d.ts b/packages/merchant-backoffice-ui/tests/declarations.d.ts new file mode 100644 index 000000000..61a53dc69 --- /dev/null +++ b/packages/merchant-backoffice-ui/tests/declarations.d.ts @@ -0,0 +1,28 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE.  See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +declare global { +  namespace jest { +    interface Matchers<R> { +      toBeWithinRange(a: number, b: number): R; +    } +  } +} diff --git a/packages/merchant-backoffice-ui/tests/functions/regex.test.ts b/packages/merchant-backoffice-ui/tests/functions/regex.test.ts new file mode 100644 index 000000000..fc8a6a42f --- /dev/null +++ b/packages/merchant-backoffice-ui/tests/functions/regex.test.ts @@ -0,0 +1,87 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE.  See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/> + */ + + /** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { AMOUNT_REGEX, PAYTO_REGEX } from "../../src/utils/constants"; + +describe('payto uri format', () => { +  const valids = [ +    'payto://iban/DE75512108001245126199?amount=EUR:200.0&message=hello', +    'payto://ach/122000661/1234', +    'payto://upi/alice@example.com?receiver-name=Alice&amount=INR:200', +    'payto://void/?amount=EUR:10.5', +    'payto://ilp/g.acme.bob' +  ] +   +  test('should be valid', () => { +    valids.forEach(v => expect(v).toMatch(PAYTO_REGEX)) +  }); +   +  const invalids = [ +    // has two question marks +    'payto://iban/DE75?512108001245126199?amount=EUR:200.0&message=hello', +    // has a space +    'payto://ach /122000661/1234', +    // has a space +    'payto://upi/alice@ example.com?receiver-name=Alice&amount=INR:200', +    // invalid field name (mount instead of amount) +    'payto://void/?mount=EUR:10.5', +    // payto:// is incomplete +    'payto: //ilp/g.acme.bob' +  ] +   +  test('should not be valid', () => { +    invalids.forEach(v => expect(v).not.toMatch(PAYTO_REGEX)) +  });   +}) + +describe('amount format', () => { +  const valids = [ +    'ARS:10', +    'COL:10.2', +    'UY:1,000.2', +    'ARS:10.123,123', +    'ARS:1,000,000', +    'ARSCOL:10', +    'THISISTHEMOTHERCOIN:1,000,000.123,123', +  ] +   +  test('should be valid', () => { +    valids.forEach(v => expect(v).toMatch(AMOUNT_REGEX)) +  }); +   +  const invalids = [ +    //no currency name +    ':10', +    //use . instead of , +    'ARS:1.000.000', +    //currency name with numbers +    '1ARS:10', +    //currency name with numbers +    'AR5:10', +    //missing value +    'USD:', +  ] +   +  test('should not be valid', () => { +    invalids.forEach(v => expect(v).not.toMatch(AMOUNT_REGEX)) +  });   + +})
\ No newline at end of file diff --git a/packages/merchant-backoffice-ui/tests/header.test.tsx b/packages/merchant-backoffice-ui/tests/header.test.tsx new file mode 100644 index 000000000..f098b70d5 --- /dev/null +++ b/packages/merchant-backoffice-ui/tests/header.test.tsx @@ -0,0 +1,63 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE.  See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { h } from "preact"; +import { ProductList } from "../src/components/product/ProductList"; +// See: https://github.com/preactjs/enzyme-adapter-preact-pure +// import { shallow } from 'enzyme'; +import * as backend from "../src/context/config"; +import { render, findAllByText } from "@testing-library/preact"; +import * as i18n from "../src/context/translation"; + +import * as jedLib from "jed"; +const handler = new jedLib.Jed("en"); + +describe("Initial Test of the Sidebar", () => { +  beforeEach(() => { +    jest +      .spyOn(backend, "useConfigContext") +      .mockImplementation(() => ({ version: "", currency: "" })); +    jest.spyOn(i18n, "useTranslationContext").mockImplementation(() => ({ +      changeLanguage: () => null, +      handler, +      lang: "en", +    })); +  }); +  test("Product list renders a table", () => { +    const context = render( +      <ProductList +        list={[ +          { +            description: "description of the product", +            image: "asdasda", +            price: "USD:10", +            quantity: 1, +            taxes: [{ name: "VAT", tax: "EUR:1" }], +            unit: "book", +          }, +        ]} +      /> +    ); + +    expect(context.findAllByText("description of the product")).toBeDefined(); +    // expect(context.find('table tr td img').map(img => img.prop('src'))).toEqual(''); +  }); +}); diff --git a/packages/merchant-backoffice-ui/tests/hooks/async.test.ts b/packages/merchant-backoffice-ui/tests/hooks/async.test.ts new file mode 100644 index 000000000..a6d0cddfa --- /dev/null +++ b/packages/merchant-backoffice-ui/tests/hooks/async.test.ts @@ -0,0 +1,158 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE.  See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/> + */ + +import { renderHook } from "@testing-library/preact-hooks" +import { useAsync } from "../../src/hooks/async" + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ +test("async function is called", async () => { +  jest.useFakeTimers() + +  const timeout = 500 + +  const asyncFunction = jest.fn(() => new Promise((res) => { +    setTimeout(() => { +      res({ the_answer: 'yes' }) +    }, timeout); +  })) + +  const { result, waitForNextUpdate } = renderHook(() => { +    return useAsync(asyncFunction) +  }) + +  expect(result.current?.isLoading).toBeFalsy() + +  result.current?.request() +  expect(asyncFunction).toBeCalled() +  await waitForNextUpdate({ timeout: 1 }) +  expect(result.current?.isLoading).toBeTruthy() + +  jest.advanceTimersByTime(timeout + 1) +  await waitForNextUpdate({ timeout: 1 }) +  expect(result.current?.isLoading).toBeFalsy() +  expect(result.current?.data).toMatchObject({ the_answer: 'yes' }) +  expect(result.current?.error).toBeUndefined() +  expect(result.current?.isSlow).toBeFalsy() +}) + +test("async function return error if rejected", async () => { +  jest.useFakeTimers() + +  const timeout = 500 + +  const asyncFunction = jest.fn(() => new Promise((_, rej) => { +    setTimeout(() => { +      rej({ the_error: 'yes' }) +    }, timeout); +  })) + +  const { result, waitForNextUpdate } = renderHook(() => { +    return useAsync(asyncFunction) +  }) + +  expect(result.current?.isLoading).toBeFalsy() + +  result.current?.request() +  expect(asyncFunction).toBeCalled() +  await waitForNextUpdate({ timeout: 1 }) +  expect(result.current?.isLoading).toBeTruthy() + +  jest.advanceTimersByTime(timeout + 1) +  await waitForNextUpdate({ timeout: 1 }) +  expect(result.current?.isLoading).toBeFalsy() +  expect(result.current?.error).toMatchObject({ the_error: 'yes' }) +  expect(result.current?.data).toBeUndefined() +  expect(result.current?.isSlow).toBeFalsy() +}) + +test("async function is slow", async () => { +  jest.useFakeTimers() + +  const timeout = 2200 + +  const asyncFunction = jest.fn(() => new Promise((res) => { +    setTimeout(() => { +      res({ the_answer: 'yes' }) +    }, timeout); +  })) + +  const { result, waitForNextUpdate } = renderHook(() => { +    return useAsync(asyncFunction) +  }) + +  expect(result.current?.isLoading).toBeFalsy() + +  result.current?.request() +  expect(asyncFunction).toBeCalled() +  await waitForNextUpdate({ timeout: 1 }) +  expect(result.current?.isLoading).toBeTruthy() + +  jest.advanceTimersByTime(timeout / 2) +  await waitForNextUpdate({ timeout: 1 }) +  expect(result.current?.isLoading).toBeTruthy() +  expect(result.current?.isSlow).toBeTruthy() +  expect(result.current?.data).toBeUndefined() +  expect(result.current?.error).toBeUndefined() + +  jest.advanceTimersByTime(timeout / 2) +  await waitForNextUpdate({ timeout: 1 }) +  expect(result.current?.isLoading).toBeFalsy() +  expect(result.current?.data).toMatchObject({ the_answer: 'yes' }) +  expect(result.current?.error).toBeUndefined() +  expect(result.current?.isSlow).toBeFalsy() + +}) + +test("async function is cancellable", async () => { +  jest.useFakeTimers() + +  const timeout = 2200 + +  const asyncFunction = jest.fn(() => new Promise((res) => { +    setTimeout(() => { +      res({ the_answer: 'yes' }) +    }, timeout); +  })) + +  const { result, waitForNextUpdate } = renderHook(() => { +    return useAsync(asyncFunction) +  }) + +  expect(result.current?.isLoading).toBeFalsy() + +  result.current?.request() +  expect(asyncFunction).toBeCalled() +  await waitForNextUpdate({ timeout: 1 }) +  expect(result.current?.isLoading).toBeTruthy() + +  jest.advanceTimersByTime(timeout / 2) +  await waitForNextUpdate({ timeout: 1 }) +  expect(result.current?.isLoading).toBeTruthy() +  expect(result.current?.isSlow).toBeTruthy() +  expect(result.current?.data).toBeUndefined() +  expect(result.current?.error).toBeUndefined() + +  result.current?.cancel() +  await waitForNextUpdate({ timeout: 1 }) +  expect(result.current?.isLoading).toBeFalsy() +  expect(result.current?.data).toBeUndefined() +  expect(result.current?.error).toBeUndefined() +  expect(result.current?.isSlow).toBeFalsy() + +}) diff --git a/packages/merchant-backoffice-ui/tests/hooks/listener.test.ts b/packages/merchant-backoffice-ui/tests/hooks/listener.test.ts new file mode 100644 index 000000000..ae34c1339 --- /dev/null +++ b/packages/merchant-backoffice-ui/tests/hooks/listener.test.ts @@ -0,0 +1,62 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE.  See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/> + */ + +/** +* +* @author Sebastian Javier Marchano (sebasjm) +*/ + +import { renderHook, act } from '@testing-library/preact-hooks'; +import { useListener } from '../../src/hooks/listener'; + +// 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)) + +  if (!result.current) { +    expect(result.current).toBeDefined() +    return; +  } + +  { +    const [activator, subscriber] = result.current +    expect(activator).toBeUndefined() + +    act(() => { +      subscriber(createSomeString) +    }) + +  } + +  const [activator] = result.current +  expect(activator).toBeDefined() +  if (!activator) return; + +  const response = await activator() +  expect(response).toBe(expectedResult) + +}); diff --git a/packages/merchant-backoffice-ui/tests/hooks/notification.test.ts b/packages/merchant-backoffice-ui/tests/hooks/notification.test.ts new file mode 100644 index 000000000..6825a825a --- /dev/null +++ b/packages/merchant-backoffice-ui/tests/hooks/notification.test.ts @@ -0,0 +1,51 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE.  See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/> + */ + + /** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { renderHook, act} from '@testing-library/preact-hooks'; +import { useNotifications } from '../../src/hooks/notifications'; + +jest.useFakeTimers() + +test('notification should disapear after timeout', () => { +  jest.spyOn(global, 'setTimeout'); + +  const timeout = 1000 +  const { result, rerender } = renderHook(() => useNotifications(undefined, timeout)); + +  expect(result.current?.notifications.length).toBe(0); + +  act(() => { +    result.current?.pushNotification({ +      message: 'some_id', +      type: 'INFO' +    }); +  }); +  expect(result.current?.notifications.length).toBe(1); + +  jest.advanceTimersByTime(timeout/2); +  rerender() +  expect(result.current?.notifications.length).toBe(1); + +  jest.advanceTimersByTime(timeout); +  rerender() +  expect(result.current?.notifications.length).toBe(0); + +}); diff --git a/packages/merchant-backoffice-ui/tests/hooks/swr/index.tsx b/packages/merchant-backoffice-ui/tests/hooks/swr/index.tsx new file mode 100644 index 000000000..44514855d --- /dev/null +++ b/packages/merchant-backoffice-ui/tests/hooks/swr/index.tsx @@ -0,0 +1,45 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE.  See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { ComponentChildren, h, VNode } from "preact"; +import { SWRConfig } from "swr"; +import { BackendContextProvider } from "../../../src/context/backend"; +import { InstanceContextProvider } from "../../../src/context/instance"; + +interface TestingContextProps { +  children?: ComponentChildren; +} +export function TestingContext({ children }: TestingContextProps): VNode { +  return ( +    <BackendContextProvider defaultUrl="http://backend" initialToken="token"> +      <InstanceContextProvider +        value={{ +          token: "token", +          id: "default", +          admin: true, +          changeToken: () => null, +        }} +      > +        <SWRConfig value={{ provider: () => new Map() }}>{children}</SWRConfig> +      </InstanceContextProvider> +    </BackendContextProvider> +  ); +} diff --git a/packages/merchant-backoffice-ui/tests/hooks/swr/instance.test.ts b/packages/merchant-backoffice-ui/tests/hooks/swr/instance.test.ts new file mode 100644 index 000000000..55d9fa6ee --- /dev/null +++ b/packages/merchant-backoffice-ui/tests/hooks/swr/instance.test.ts @@ -0,0 +1,636 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE.  See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { renderHook } from "@testing-library/preact-hooks"; +import { act } from "preact/test-utils"; +import { MerchantBackend } from "../../../src/declaration"; +import { useAdminAPI, useBackendInstances, useInstanceAPI, useInstanceDetails, useManagementAPI } from "../../../src/hooks/instance"; +import { +  API_CREATE_INSTANCE, +  API_DELETE_INSTANCE, +  API_GET_CURRENT_INSTANCE, +  API_LIST_INSTANCES, +  API_UPDATE_CURRENT_INSTANCE, +  API_UPDATE_CURRENT_INSTANCE_AUTH, +  API_UPDATE_INSTANCE_AUTH_BY_ID, +  API_UPDATE_INSTANCE_BY_ID, +  assertJustExpectedRequestWereMade, +  AxiosMockEnvironment +} from "../../axiosMock"; +import { TestingContext } from "./index"; + +describe("instance api interaction with details ", () => { + +  it("should evict cache when updating an instance", async () => { + +    const env = new AxiosMockEnvironment(); + +    env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { +      response: { +        name: 'instance_name' +      } as MerchantBackend.Instances.QueryInstancesResponse, +    }); + +    const { result, waitForNextUpdate } = renderHook( +      () => { +        const api = useInstanceAPI(); +        const query = useInstanceDetails(); + +        return { query, api }; +      }, +      { wrapper: TestingContext } +    ); + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } +    expect(result.current.query.loading).toBeTruthy(); + +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); + +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      name: 'instance_name' +    }); + +    env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE, { +      request: { +        name: 'other_name' +      } as MerchantBackend.Instances.InstanceReconfigurationMessage, +    }); + +    act(async () => { +      await result.current?.api.updateInstance({ +        name: 'other_name' +      } as MerchantBackend.Instances.InstanceReconfigurationMessage); +    }); + +    assertJustExpectedRequestWereMade(env); + +    env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { +      response: { +        name: 'other_name' +      } as MerchantBackend.Instances.QueryInstancesResponse, +    }); + +    expect(result.current.query.loading).toBeFalsy(); + +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current.query.ok).toBeTruthy(); + +    expect(result.current.query.data).toEqual({ +      name: 'other_name' +    }); +  }); + +  it("should evict cache when setting the instance's token", async () => { +    const env = new AxiosMockEnvironment(); + +    env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { +      response: { +        name: 'instance_name', +        auth: { +          method: 'token', +          token: 'not-secret', +        } +      } as MerchantBackend.Instances.QueryInstancesResponse, +    }); + +    const { result, waitForNextUpdate } = renderHook( +      () => { +        const api = useInstanceAPI(); +        const query = useInstanceDetails(); + +        return { query, api }; +      }, +      { wrapper: TestingContext } +    ); + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } +    expect(result.current.query.loading).toBeTruthy(); + +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); + +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      name: 'instance_name', +      auth: { +        method: 'token', +        token: 'not-secret', +      } +    }); + +    env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { +      request: { +        method: 'token', +        token: 'secret' +      } as MerchantBackend.Instances.InstanceAuthConfigurationMessage, +    }); + +    act(async () => { +      await result.current?.api.setNewToken('secret'); +    }); + +    assertJustExpectedRequestWereMade(env); + +    env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { +      response: { +        name: 'instance_name', +        auth: { +          method: 'token', +          token: 'secret', +        } +      } as MerchantBackend.Instances.QueryInstancesResponse, +    }); + +    expect(result.current.query.loading).toBeFalsy(); + +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current.query.ok).toBeTruthy(); + +    expect(result.current.query.data).toEqual({ +      name: 'instance_name', +      auth: { +        method: 'token', +        token: 'secret', +      } +    }); +  }); + +  it("should evict cache when clearing the instance's token", async () => { +    const env = new AxiosMockEnvironment(); + +    env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { +      response: { +        name: 'instance_name', +        auth: { +          method: 'token', +          token: 'not-secret', +        } +      } as MerchantBackend.Instances.QueryInstancesResponse, +    }); + +    const { result, waitForNextUpdate } = renderHook( +      () => { +        const api = useInstanceAPI(); +        const query = useInstanceDetails(); + +        return { query, api }; +      }, +      { wrapper: TestingContext } +    ); + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } +    expect(result.current.query.loading).toBeTruthy(); + +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); + +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      name: 'instance_name', +      auth: { +        method: 'token', +        token: 'not-secret', +      } +    }); + +    env.addRequestExpectation(API_UPDATE_CURRENT_INSTANCE_AUTH, { +      request: { +        method: 'external', +      } as MerchantBackend.Instances.InstanceAuthConfigurationMessage, +    }); + +    act(async () => { +      await result.current?.api.clearToken(); +    }); + +    assertJustExpectedRequestWereMade(env); + +    env.addRequestExpectation(API_GET_CURRENT_INSTANCE, { +      response: { +        name: 'instance_name', +        auth: { +          method: 'external', +        } +      } as MerchantBackend.Instances.QueryInstancesResponse, +    }); + +    expect(result.current.query.loading).toBeFalsy(); + +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current.query.ok).toBeTruthy(); + +    expect(result.current.query.data).toEqual({ +      name: 'instance_name', +      auth: { +        method: 'external', +      } +    }); +  }); +}); + +describe("instance admin api interaction with listing ", () => { + +  it("should evict cache when creating a new instance", async () => { +    const env = new AxiosMockEnvironment(); + +    env.addRequestExpectation(API_LIST_INSTANCES, { +      response: { +        instances: [{ +          name: 'instance_name' +        } as MerchantBackend.Instances.Instance] +      }, +    }); + +    const { result, waitForNextUpdate } = renderHook( +      () => { +        const api = useAdminAPI(); +        const query = useBackendInstances(); + +        return { query, api }; +      }, +      { wrapper: TestingContext } +    ); + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } +    expect(result.current.query.loading).toBeTruthy(); + +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); + +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      instances: [{ +        name: 'instance_name' +      }] +    }); + +    env.addRequestExpectation(API_CREATE_INSTANCE, { +      request: { +        name: 'other_name' +      } as MerchantBackend.Instances.InstanceConfigurationMessage, +    }); + +    act(async () => { +      await result.current?.api.createInstance({ +        name: 'other_name' +      } as MerchantBackend.Instances.InstanceConfigurationMessage); +    }); + +    assertJustExpectedRequestWereMade(env); + +    env.addRequestExpectation(API_LIST_INSTANCES, { +      response: { +        instances: [{ +          name: 'instance_name' +        } as MerchantBackend.Instances.Instance, +        { +          name: 'other_name' +        } as MerchantBackend.Instances.Instance] +      }, +    }); + +    expect(result.current.query.loading).toBeFalsy(); + +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current.query.ok).toBeTruthy(); + +    expect(result.current.query.data).toEqual({ +      instances: [{ +        name: 'instance_name' +      }, { +        name: 'other_name' +      }] +    }); +  }); + +  it("should evict cache when deleting an instance", async () => { +    const env = new AxiosMockEnvironment(); + +    env.addRequestExpectation(API_LIST_INSTANCES, { +      response: { +        instances: [{ +          id: 'default', +          name: 'instance_name' +        } as MerchantBackend.Instances.Instance, +        { +          id: 'the_id', +          name: 'second_instance' +        } as MerchantBackend.Instances.Instance] +      }, +    }); + +    const { result, waitForNextUpdate } = renderHook( +      () => { +        const api = useAdminAPI(); +        const query = useBackendInstances(); + +        return { query, api }; +      }, +      { wrapper: TestingContext } +    ); + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } +    expect(result.current.query.loading).toBeTruthy(); + +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); + +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      instances: [{ +        id: 'default', +        name: 'instance_name' +      }, { +        id: 'the_id', +        name: 'second_instance' +      }] +    }); + +    env.addRequestExpectation(API_DELETE_INSTANCE('the_id'), {}); + +    act(async () => { +      await result.current?.api.deleteInstance('the_id'); +    }); + +    assertJustExpectedRequestWereMade(env); + +    env.addRequestExpectation(API_LIST_INSTANCES, { +      response: { +        instances: [{ +          id: 'default', +          name: 'instance_name' +        } as MerchantBackend.Instances.Instance] +      }, +    }); + +    expect(result.current.query.loading).toBeFalsy(); + +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current.query.ok).toBeTruthy(); + +    expect(result.current.query.data).toEqual({ +      instances: [{ +        id: 'default', +        name: 'instance_name' +      }] +    }); +  }); +  it("should evict cache when deleting (purge) an instance", async () => { +    const env = new AxiosMockEnvironment(); + +    env.addRequestExpectation(API_LIST_INSTANCES, { +      response: { +        instances: [{ +          id: 'default', +          name: 'instance_name' +        } as MerchantBackend.Instances.Instance, +        { +          id: 'the_id', +          name: 'second_instance' +        } as MerchantBackend.Instances.Instance] +      }, +    }); + +    const { result, waitForNextUpdate } = renderHook( +      () => { +        const api = useAdminAPI(); +        const query = useBackendInstances(); + +        return { query, api }; +      }, +      { wrapper: TestingContext } +    ); + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } +    expect(result.current.query.loading).toBeTruthy(); + +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); + +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      instances: [{ +        id: 'default', +        name: 'instance_name' +      }, { +        id: 'the_id', +        name: 'second_instance' +      }] +    }); + +    env.addRequestExpectation(API_DELETE_INSTANCE('the_id'), { +      qparam: { +        purge: 'YES' +      } +    }); + +    act(async () => { +      await result.current?.api.purgeInstance('the_id'); +    }); + +    assertJustExpectedRequestWereMade(env); + +    env.addRequestExpectation(API_LIST_INSTANCES, { +      response: { +        instances: [{ +          id: 'default', +          name: 'instance_name' +        } as MerchantBackend.Instances.Instance] +      }, +    }); + +    expect(result.current.query.loading).toBeFalsy(); + +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current.query.ok).toBeTruthy(); + +    expect(result.current.query.data).toEqual({ +      instances: [{ +        id: 'default', +        name: 'instance_name' +      }] +    }); +  }); +}); + +describe("instance management api interaction with listing ", () => { + +  it("should evict cache when updating an instance", async () => { +    const env = new AxiosMockEnvironment(); + +    env.addRequestExpectation(API_LIST_INSTANCES, { +      response: { +        instances: [{ +          id: 'managed', +          name: 'instance_name' +        } as MerchantBackend.Instances.Instance] +      }, +    }); + +    const { result, waitForNextUpdate } = renderHook( +      () => { +        const api = useManagementAPI('managed'); +        const query = useBackendInstances(); + +        return { query, api }; +      }, +      { wrapper: TestingContext } +    ); + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } +    expect(result.current.query.loading).toBeTruthy(); + +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); + +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      instances: [{ +        id: 'managed', +        name: 'instance_name' +      }] +    }); + +    env.addRequestExpectation(API_UPDATE_INSTANCE_BY_ID('managed'), { +      request: { +        name: 'other_name' +      } as MerchantBackend.Instances.InstanceReconfigurationMessage, +    }); + +    act(async () => { +      await result.current?.api.updateInstance({ +        name: 'other_name' +      } as MerchantBackend.Instances.InstanceConfigurationMessage); +    }); + +    assertJustExpectedRequestWereMade(env); + +    env.addRequestExpectation(API_LIST_INSTANCES, { +      response: { +        instances: [ +          { +            id: 'managed', +            name: 'other_name' +          } as MerchantBackend.Instances.Instance] +      }, +    }); + +    expect(result.current.query.loading).toBeFalsy(); + +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current.query.ok).toBeTruthy(); + +    expect(result.current.query.data).toEqual({ +      instances: [{ +        id: 'managed', +        name: 'other_name' +      }] +    }); +  }); + +}); + diff --git a/packages/merchant-backoffice-ui/tests/hooks/swr/order.test.ts b/packages/merchant-backoffice-ui/tests/hooks/swr/order.test.ts new file mode 100644 index 000000000..e7f6c9334 --- /dev/null +++ b/packages/merchant-backoffice-ui/tests/hooks/swr/order.test.ts @@ -0,0 +1,567 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE.  See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { renderHook } from "@testing-library/preact-hooks"; +import { act } from "preact/test-utils"; +import { TestingContext } from "."; +import { MerchantBackend } from "../../../src/declaration"; +import { useInstanceOrders, useOrderAPI, useOrderDetails } from "../../../src/hooks/order"; +import { +  API_CREATE_ORDER, +  API_DELETE_ORDER, +  API_FORGET_ORDER_BY_ID, +  API_GET_ORDER_BY_ID, +  API_LIST_ORDERS, API_REFUND_ORDER_BY_ID, assertJustExpectedRequestWereMade, assertNextRequest, assertNoMoreRequestWereMade, AxiosMockEnvironment +} from "../../axiosMock"; + +describe("order api interaction with listing", () => { + +  it("should evict cache when creating an order", async () => { +    const env = new AxiosMockEnvironment(); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: 0, paid: "yes" }, +      response: { +        orders: [{ order_id: "1" } as MerchantBackend.Orders.OrderHistoryEntry], +      }, +    }); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: -20, paid: "yes" }, +      response: { +        orders: [{ order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry], +      }, +    }); + + +    const { result, waitForNextUpdate } = renderHook(() => { +      const newDate = (d: Date) => { +        console.log("new date", d); +      }; +      const query = useInstanceOrders({ paid: "yes" }, newDate); +      const api = useOrderAPI(); + +      return { query, api }; +    }, { wrapper: TestingContext }); + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } + +    expect(result.current.query.loading).toBeTruthy(); +    await waitForNextUpdate({ timeout: 1 }); +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      orders: [{ order_id: "1" }, { order_id: "2" }], +    }); + +    env.addRequestExpectation(API_CREATE_ORDER, { +      request: { +        order: { amount: "ARS:12", summary: "pay me" }, +      }, +      response: { order_id: "3" }, +    }); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: 0, paid: "yes" }, +      response: { +        orders: [{ order_id: "1" } as any], +      }, +    }); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: -20, paid: "yes" }, +      response: { +        orders: [{ order_id: "2" } as any, { order_id: "3" } as any], +      }, +    }); + +    act(async () => { +      await result.current?.api.createOrder({ +        order: { amount: "ARS:12", summary: "pay me" }, +      } as any); +    }); + +    await waitForNextUpdate({ timeout: 1 }); +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      orders: [{ order_id: "1" }, { order_id: "2" }, { order_id: "3" }], +    }); +  }); +  it("should evict cache when doing a refund", async () => { +    const env = new AxiosMockEnvironment(); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: 0, paid: "yes" }, +      response: { +        orders: [{ order_id: "1", amount: 'EUR:12', refundable: true } as MerchantBackend.Orders.OrderHistoryEntry], +      }, +    }); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: -20, paid: "yes" }, +      response: { orders: [], }, +    }); + + +    const { result, waitForNextUpdate } = renderHook(() => { +      const newDate = (d: Date) => { +        console.log("new date", d); +      }; +      const query = useInstanceOrders({ paid: "yes" }, newDate); +      const api = useOrderAPI(); + +      return { query, api }; +    }, { wrapper: TestingContext }); + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } + +    expect(result.current.query.loading).toBeTruthy(); +    await waitForNextUpdate({ timeout: 1 }); +    assertJustExpectedRequestWereMade(env); + + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      orders: [{ +        order_id: "1", +        amount: 'EUR:12', +        refundable: true, +      }], +    }); + +    env.addRequestExpectation(API_REFUND_ORDER_BY_ID('1'), { +      request: { +        reason: 'double pay', +        refund: 'EUR:1' +      }, +    }); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: 0, paid: "yes" }, +      response: { +        orders: [{ order_id: "1", amount: 'EUR:12', refundable: false } as any], +      }, +    }); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: -20, paid: "yes" }, +      response: { orders: [], }, +    }); + +    act(async () => { +      await result.current?.api.refundOrder('1', { +        reason: 'double pay', +        refund: 'EUR:1' +      }); +    }); + +    await waitForNextUpdate({ timeout: 1 }); +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      orders: [{ +        order_id: "1", +        amount: 'EUR:12', +        refundable: false, +      }], +    }); +  }); +  it("should evict cache when deleting an order", async () => { +    const env = new AxiosMockEnvironment(); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: 0, paid: "yes" }, +      response: { +        orders: [{ order_id: "1" } as MerchantBackend.Orders.OrderHistoryEntry], +      }, +    }); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: -20, paid: "yes" }, +      response: { +        orders: [{ order_id: "2" } as MerchantBackend.Orders.OrderHistoryEntry], +      }, +    }); + + +    const { result, waitForNextUpdate } = renderHook(() => { +      const newDate = (d: Date) => { +        console.log("new date", d); +      }; +      const query = useInstanceOrders({ paid: "yes" }, newDate); +      const api = useOrderAPI(); + +      return { query, api }; +    }, { wrapper: TestingContext }); + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } + +    expect(result.current.query.loading).toBeTruthy(); +    await waitForNextUpdate({ timeout: 1 }); +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      orders: [{ order_id: "1" }, { order_id: "2" }], +    }); + +    env.addRequestExpectation(API_DELETE_ORDER('1'), {}); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: 0, paid: "yes" }, +      response: { +        orders: [], +      }, +    }); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: -20, paid: "yes" }, +      response: { +        orders: [{ order_id: "2" } as any], +      }, +    }); + +    act(async () => { +      await result.current?.api.deleteOrder('1'); +    }); + +    await waitForNextUpdate({ timeout: 1 }); +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      orders: [{ order_id: "2" }], +    }); +  }); + +}); + +describe("order api interaction with details", () => { + +  it("should evict cache when doing a refund", async () => { +    const env = new AxiosMockEnvironment(); + +    env.addRequestExpectation(API_GET_ORDER_BY_ID('1'), { +      // qparam: { delta: 0, paid: "yes" }, +      response: { +        summary: 'description', +        refund_amount: 'EUR:0', +      } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, +    }); + +    const { result, waitForNextUpdate } = renderHook(() => { +      const query = useOrderDetails('1') +      const api = useOrderAPI(); + +      return { query, api }; +    }, { wrapper: TestingContext }); + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } + +    expect(result.current.query.loading).toBeTruthy(); +    await waitForNextUpdate({ timeout: 1 }); +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      summary: 'description', +      refund_amount: 'EUR:0', +    }); + +    env.addRequestExpectation(API_REFUND_ORDER_BY_ID('1'), { +      request: { +        reason: 'double pay', +        refund: 'EUR:1' +      }, +    }); + +    env.addRequestExpectation(API_GET_ORDER_BY_ID('1'), { +      response: { +        summary: 'description', +        refund_amount: 'EUR:1', +      } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, +    }); + +    act(async () => { +      await result.current?.api.refundOrder('1', { +        reason: 'double pay', +        refund: 'EUR:1' +      }); +    }); + +    await waitForNextUpdate({ timeout: 1 }); +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      summary: 'description', +      refund_amount: 'EUR:1', +    }); +  }) +  it("should evict cache when doing a forget", async () => { +    const env = new AxiosMockEnvironment(); + +    env.addRequestExpectation(API_GET_ORDER_BY_ID('1'), { +      // qparam: { delta: 0, paid: "yes" }, +      response: { +        summary: 'description', +        refund_amount: 'EUR:0', +      } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, +    }); + +    const { result, waitForNextUpdate } = renderHook(() => { +      const query = useOrderDetails('1') +      const api = useOrderAPI(); + +      return { query, api }; +    }, { wrapper: TestingContext }); + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } + +    expect(result.current.query.loading).toBeTruthy(); +    await waitForNextUpdate({ timeout: 1 }); +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      summary: 'description', +      refund_amount: 'EUR:0', +    }); + +    env.addRequestExpectation(API_FORGET_ORDER_BY_ID('1'), { +      request: { +        fields: ['$.summary'] +      }, +    }); + +    env.addRequestExpectation(API_GET_ORDER_BY_ID('1'), { +      response: { +        summary: undefined, +      } as unknown as MerchantBackend.Orders.CheckPaymentPaidResponse, +    }); + +    act(async () => { +      await result.current?.api.forgetOrder('1', { +        fields: ['$.summary'] +      }); +    }); + +    await waitForNextUpdate({ timeout: 1 }); +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      summary: undefined, +    }); +  }) +}) + +describe("order listing pagination", () => { + +  it("should not load more if has reach the end", async () => { +    const env = new AxiosMockEnvironment(); +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: 20, wired: "yes", date_ms: 12 }, +      response: { +        orders: [{ order_id: "1" } as any], +      }, +    }); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: -20, wired: "yes", date_ms: 13 }, +      response: { +        orders: [{ order_id: "2" } as any], +      }, +    }); + + +    const { result, waitForNextUpdate } = renderHook(() => { +      const newDate = (d: Date) => { +        console.log("new date", d); +      }; +      const date = new Date(12); +      const query = useInstanceOrders({ wired: "yes", date }, newDate) +      return { query } +    }, { wrapper: TestingContext }); + +    assertJustExpectedRequestWereMade(env); + +    await waitForNextUpdate(); + +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      orders: [{ order_id: "1" }, { order_id: "2" }], +    }); + +    expect(result.current.query.isReachingEnd).toBeTruthy() +    expect(result.current.query.isReachingStart).toBeTruthy() + +    await act(() => { +      if (!result.current?.query.ok) throw Error("not ok"); +      result.current.query.loadMore(); +    }); +    assertNoMoreRequestWereMade(env); + +    await act(() => { +      if (!result.current?.query.ok) throw Error("not ok"); +      result.current.query.loadMorePrev(); +    }); +    assertNoMoreRequestWereMade(env); + +    expect(result.current.query.data).toEqual({ +      orders: [ +        { order_id: "1" }, +        { order_id: "2" }, +      ], +    }); +  }); + +  it("should load more if result brings more that PAGE_SIZE", async () => { +    const env = new AxiosMockEnvironment(); + +    const ordersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({ order_id: String(i) })) +    const ordersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({ order_id: String(i + 20) })) +    const ordersFrom20to0 = [...ordersFrom0to20].reverse() + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: 20, wired: "yes", date_ms: 12 }, +      response: { +        orders: ordersFrom0to20, +      }, +    }); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: -20, wired: "yes", date_ms: 13 }, +      response: { +        orders: ordersFrom20to40, +      }, +    }); + +    const { result, waitForNextUpdate } = renderHook(() => { +      const newDate = (d: Date) => { +        console.log("new date", d); +      }; +      const date = new Date(12); +      const query = useInstanceOrders({ wired: "yes", date }, newDate) +      return { query } +    }, { wrapper: TestingContext }); + +    assertJustExpectedRequestWereMade(env); + +    await waitForNextUpdate({ timeout: 1 }); + +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      orders: [...ordersFrom20to0, ...ordersFrom20to40], +    }); + +    expect(result.current.query.isReachingEnd).toBeFalsy() +    expect(result.current.query.isReachingStart).toBeFalsy() + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: -40, wired: "yes", date_ms: 13 }, +      response: { +        orders: [...ordersFrom20to40, { order_id: '41' }], +      }, +    }); + +    await act(() => { +      if (!result.current?.query.ok) throw Error("not ok"); +      result.current.query.loadMore(); +    }); +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    env.addRequestExpectation(API_LIST_ORDERS, { +      qparam: { delta: 40, wired: "yes", date_ms: 12 }, +      response: { +        orders: [...ordersFrom0to20, { order_id: '-1' }], +      }, +    }); + +    await act(() => { +      if (!result.current?.query.ok) throw Error("not ok"); +      result.current.query.loadMorePrev(); +    }); +    await waitForNextUpdate({ timeout: 1 }); +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.data).toEqual({ +      orders: [{ order_id: '-1' }, ...ordersFrom20to0, ...ordersFrom20to40, { order_id: '41' }], +    }); +  }); + + +}); diff --git a/packages/merchant-backoffice-ui/tests/hooks/swr/product.test.ts b/packages/merchant-backoffice-ui/tests/hooks/swr/product.test.ts new file mode 100644 index 000000000..5d39a7c47 --- /dev/null +++ b/packages/merchant-backoffice-ui/tests/hooks/swr/product.test.ts @@ -0,0 +1,338 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE.  See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { renderHook } from "@testing-library/preact-hooks"; +import { act } from "preact/test-utils"; +import { TestingContext } from "."; +import { MerchantBackend } from "../../../src/declaration"; +import { useInstanceProducts, useProductAPI, useProductDetails } from "../../../src/hooks/product"; +import { +  API_CREATE_PRODUCT, +  API_DELETE_PRODUCT, API_GET_PRODUCT_BY_ID, +  API_LIST_PRODUCTS, +  API_UPDATE_PRODUCT_BY_ID, +  assertJustExpectedRequestWereMade, +  assertNextRequest, +  AxiosMockEnvironment +} from "../../axiosMock"; + +describe("product api interaction with listing ", () => { +  it("should evict cache when creating a product", async () => { +    const env = new AxiosMockEnvironment(); + +    env.addRequestExpectation(API_LIST_PRODUCTS, { +      response: { +        products: [{ product_id: "1234" }], +      }, +    }); +    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { +      response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, +    }); + +    const { result, waitForNextUpdate } = renderHook( +      () => { +        const query = useInstanceProducts(); +        const api = useProductAPI(); +        return { api, query }; +      }, +      { wrapper: TestingContext } +    ); // get products -> loading + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } +    expect(result.current.query.loading).toBeTruthy(); +    await waitForNextUpdate({ timeout: 1 }); + +    await waitForNextUpdate({ timeout: 1 }); +    assertJustExpectedRequestWereMade(env); +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual([ +      { id: "1234", price: "ARS:12" }, +    ]); + +    env.addRequestExpectation(API_CREATE_PRODUCT, { +      request: { price: "ARS:23" } as MerchantBackend.Products.ProductAddDetail, +    }); + +    env.addRequestExpectation(API_LIST_PRODUCTS, { +      response: { +        products: [{ product_id: "1234" }, { product_id: "2345" }], +      }, +    }); +    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { +      response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, +    }); +    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { +      response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, +    }); +    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), { +      response: { price: "ARS:23" } as MerchantBackend.Products.ProductDetail, +    }); + +    act(async () => { +      await result.current?.api.createProduct({ +        price: "ARS:23", +      } as any); +    }); + +    assertNextRequest(env); +    await waitForNextUpdate({ timeout: 1 }); +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual([ +      { +        id: "1234", +        price: "ARS:12", +      }, +      { +        id: "2345", +        price: "ARS:23", +      }, +    ]); +  }); + +  it("should evict cache when updating a product", async () => { +    const env = new AxiosMockEnvironment(); + +    env.addRequestExpectation(API_LIST_PRODUCTS, { +      response: { +        products: [{ product_id: "1234" }], +      }, +    }); +    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { +      response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, +    }); + +    const { result, waitForNextUpdate } = renderHook( +      () => { +        const query = useInstanceProducts(); +        const api = useProductAPI(); +        return { api, query }; +      }, +      { wrapper: TestingContext } +    ); // get products -> loading + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } +    expect(result.current.query.loading).toBeTruthy(); +    await waitForNextUpdate({ timeout: 1 }); + +    await waitForNextUpdate({ timeout: 1 }); +    assertJustExpectedRequestWereMade(env); +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual([ +      { id: "1234", price: "ARS:12" }, +    ]); + +    env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("1234"), { +      request: { price: "ARS:13" } as MerchantBackend.Products.ProductPatchDetail, +    }); + +    env.addRequestExpectation(API_LIST_PRODUCTS, { +      response: { +        products: [{ product_id: "1234" }], +      }, +    }); +    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { +      response: { price: "ARS:13" } as MerchantBackend.Products.ProductDetail, +    }); + +    act(async () => { +      await result.current?.api.updateProduct("1234", { +        price: "ARS:13", +      } as any); +    }); + +    assertNextRequest(env); +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual([ +      { +        id: "1234", +        price: "ARS:13", +      }, +    ]); +  }); + +  it("should evict cache when deleting a product", async () => { +    const env = new AxiosMockEnvironment(); + +    env.addRequestExpectation(API_LIST_PRODUCTS, { +      response: { +        products: [{ product_id: "1234" }, { product_id: "2345" }], +      }, +    }); +    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { +      response: { price: "ARS:12" } as MerchantBackend.Products.ProductDetail, +    }); +    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("2345"), { +      response: { price: "ARS:23" } as MerchantBackend.Products.ProductDetail, +    }); + +    const { result, waitForNextUpdate } = renderHook( +      () => { +        const query = useInstanceProducts(); +        const api = useProductAPI(); +        return { api, query }; +      }, +      { wrapper: TestingContext } +    ); // get products -> loading + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } +    expect(result.current.query.loading).toBeTruthy(); +    await waitForNextUpdate({ timeout: 1 }); + +    await waitForNextUpdate({ timeout: 1 }); +    // await waitForNextUpdate({ timeout: 1 }); +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual([ +      { id: "1234", price: "ARS:12" }, +      { id: "2345", price: "ARS:23" }, +    ]); + +    env.addRequestExpectation(API_DELETE_PRODUCT("2345"), {}); + +    env.addRequestExpectation(API_LIST_PRODUCTS, { +      response: { +        products: [{ product_id: "1234" }], +      }, +    }); +    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("1234"), { +      response: { price: "ARS:13" } as MerchantBackend.Products.ProductDetail, +    }); + +    act(async () => { +      await result.current?.api.deleteProduct("2345"); +    }); + +    assertNextRequest(env); +    await waitForNextUpdate({ timeout: 1 }); +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual([ +      { +        id: "1234", +        price: "ARS:13", +      }, +    ]); +  }); + +}); + +describe("product api interaction with details", () => { +  it("should evict cache when updating a product", async () => { +    const env = new AxiosMockEnvironment(); + +    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), { +      response: { +        description: "this is a description", +      } as MerchantBackend.Products.ProductDetail, +    }); + +    const { result, waitForNextUpdate } = renderHook(() => { +      const query = useProductDetails("12"); +      const api = useProductAPI(); +      return { query, api }; +    }, { wrapper: TestingContext }); + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } +    expect(result.current.query.loading).toBeTruthy(); +    await waitForNextUpdate(); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      description: "this is a description", +    }); + +    env.addRequestExpectation(API_UPDATE_PRODUCT_BY_ID("12"), { +      request: { description: "other description" } as MerchantBackend.Products.ProductPatchDetail, +    }); + +    env.addRequestExpectation(API_GET_PRODUCT_BY_ID("12"), { +      response: { +        description: "other description", +      } as MerchantBackend.Products.ProductDetail, +    }); + +    act(async () => { +      return await result.current?.api.updateProduct("12", { +        description: "other description", +      } as any); +    }); + +    assertNextRequest(env); +    await waitForNextUpdate(); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      description: "other description", +    }); +  }) +})
\ No newline at end of file diff --git a/packages/merchant-backoffice-ui/tests/hooks/swr/reserve.test.ts b/packages/merchant-backoffice-ui/tests/hooks/swr/reserve.test.ts new file mode 100644 index 000000000..0361c54e8 --- /dev/null +++ b/packages/merchant-backoffice-ui/tests/hooks/swr/reserve.test.ts @@ -0,0 +1,470 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE.  See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { renderHook } from "@testing-library/preact-hooks"; +import { act } from "preact/test-utils"; +import { MerchantBackend } from "../../../src/declaration"; +import { +  useInstanceReserves, +  useReserveDetails, +  useReservesAPI, +  useTipDetails, +} from "../../../src/hooks/reserves"; +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"; +import { TestingContext } from "./index"; + +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 } +    ); + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } +    expect(result.current.query.loading).toBeTruthy(); + +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      reserves: [{ reserve_pub: "11" }], +    }); + +    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, +      } +    ); + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } +    expect(result.current.query.loading).toBeTruthy(); + +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      reserves: [ +        { reserve_pub: "11" }, +        { reserve_pub: "22" }, +        { reserve_pub: "33" }, +      ], +    }); + +    env.addRequestExpectation(API_DELETE_RESERVE("11"), {}); + +    act(async () => { +      await result.current?.api.deleteReserve("11"); +      return; +    }); + +    assertJustExpectedRequestWereMade(env); + +    env.addRequestExpectation(API_LIST_RESERVES, { +      response: { +        reserves: [ +          { +            reserve_pub: "22", +          } as MerchantBackend.Tips.ReserveStatusEntry, +          { +            reserve_pub: "33", +          } as MerchantBackend.Tips.ReserveStatusEntry, +        ], +      }, +    }); + +    expect(result.current.query.loading).toBeFalsy(); + +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current.query.ok).toBeTruthy(); + +    expect(result.current.query.data).toEqual({ +      reserves: [ +        { +          reserve_pub: "22", +        } as MerchantBackend.Tips.ReserveStatusEntry, +        { +          reserve_pub: "33", +        } as MerchantBackend.Tips.ReserveStatusEntry, +      ], +    }); +  }); +}); + +describe("reserve api interaction with details", () => { +  it("should evict cache when adding a tip for a specific reserve", async () => { +    const env = new AxiosMockEnvironment(); + +    env.addRequestExpectation(API_GET_RESERVE_BY_ID("11"), { +      response: { +        payto_uri: "payto://here", +        tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }], +      } as MerchantBackend.Tips.ReserveDetail, +    }); + +    const { result, waitForNextUpdate } = renderHook( +      () => { +        const api = useReservesAPI(); +        const query = useReserveDetails("11"); + +        return { query, api }; +      }, +      { +        wrapper: TestingContext, +      } +    ); + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } +    expect(result.current.query.loading).toBeTruthy(); + +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      payto_uri: "payto://here", +      tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }], +    }); + +    env.addRequestExpectation(API_AUTHORIZE_TIP_FOR_RESERVE("11"), { +      request: { +        amount: "USD:12", +        justification: "not", +        next_url: "http://taler.net", +      }, +      response: { +        tip_id: "id2", +        taler_tip_uri: "uri", +        tip_expiration: { t_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, +      } +    ); + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } +    expect(result.current.query.loading).toBeTruthy(); + +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      payto_uri: "payto://here", +      tips: [{ reason: "why?", tip_id: "id1", total_amount: "USD:10" }], +    }); + +    env.addRequestExpectation(API_AUTHORIZE_TIP, { +      request: { +        amount: "USD:12", +        justification: "not", +        next_url: "http://taler.net", +      }, +      response: { +        tip_id: "id2", +        taler_tip_uri: "uri", +        tip_expiration: { t_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, +      } +    ); + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } +    expect(result.current.query.loading).toBeTruthy(); + +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      total_picked_up: "USD:12", +      reason: "not", +    }); +  }); +}); diff --git a/packages/merchant-backoffice-ui/tests/hooks/swr/transfer.test.ts b/packages/merchant-backoffice-ui/tests/hooks/swr/transfer.test.ts new file mode 100644 index 000000000..612cf8842 --- /dev/null +++ b/packages/merchant-backoffice-ui/tests/hooks/swr/transfer.test.ts @@ -0,0 +1,268 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE.  See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { act, renderHook } from "@testing-library/preact-hooks"; +import { TestingContext } from "./index"; +import { useInstanceTransfers, useTransferAPI } from "../../../src/hooks/transfer"; +import { +  API_INFORM_TRANSFERS, +  API_LIST_TRANSFERS, +  assertJustExpectedRequestWereMade, +  assertNoMoreRequestWereMade, +  AxiosMockEnvironment, +} from "../../axiosMock"; +import { MerchantBackend } from "../../../src/declaration"; + +describe("transfer api interaction with listing", () => { + +  it("should evict cache when informing a transfer", async () => { +    const env = new AxiosMockEnvironment(); + +    env.addRequestExpectation(API_LIST_TRANSFERS, { +      qparam: { limit: 0 }, +      response: { +        transfers: [{ wtid: "2" } as MerchantBackend.Transfers.TransferDetails], +      }, +    }); +    // FIXME: is this query really needed? if the hook is rendered without +    // position argument then then backend is returning the newest and no need +    // to this second query  +    env.addRequestExpectation(API_LIST_TRANSFERS, { +      qparam: { limit: -20 }, +      response: { +        transfers: [], +      }, +    }); + +    const { result, waitForNextUpdate } = renderHook(() => { +      const moveCursor = (d: string) => { +        console.log("new position", d); +      }; +      const query = useInstanceTransfers({}, moveCursor); +      const api = useTransferAPI(); + +      return { query, api }; +    }, { wrapper: TestingContext }); + +    if (!result.current) { +      expect(result.current).toBeDefined(); +      return; +    } + +    expect(result.current.query.loading).toBeTruthy(); +    await waitForNextUpdate({ timeout: 1 }); +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current.query.ok).toBeTruthy(); +    if (!result.current.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      transfers: [{ wtid: "2" }], +    }); + +    env.addRequestExpectation(API_INFORM_TRANSFERS, { +      request: { +        wtid: '3', +        credit_amount: 'EUR:1', +        exchange_url: 'exchange.url', +        payto_uri: 'payto://' +      }, +      response: { total: '' } as any, +    }); + +    env.addRequestExpectation(API_LIST_TRANSFERS, { +      qparam: { limit: 0 }, +      response: { +        transfers: [{ wtid: "2" } as any, { wtid: "3" } as any], +      }, +    }); + +    env.addRequestExpectation(API_LIST_TRANSFERS, { +      qparam: { limit: -20 }, +      response: { +        transfers: [], +      }, +    }); + +    act(async () => { +      await result.current?.api.informTransfer({ +        wtid: '3', +        credit_amount: 'EUR:1', +        exchange_url: 'exchange.url', +        payto_uri: 'payto://' +      }); +    }); + +    await waitForNextUpdate({ timeout: 1 }); +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.loading).toBeFalsy(); +    expect(result.current.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      transfers: [{ wtid: "3" }, { wtid: "2" }], +    }); +  }); + +}); + +describe("transfer listing pagination", () => { + +  it("should not load more if has reach the end", async () => { +    const env = new AxiosMockEnvironment(); +    env.addRequestExpectation(API_LIST_TRANSFERS, { +      qparam: { limit: 0, payto_uri: 'payto://' }, +      response: { +        transfers: [{ wtid: "2" } as any], +      }, +    }); + +    env.addRequestExpectation(API_LIST_TRANSFERS, { +      qparam: { limit: -20, payto_uri: 'payto://' }, +      response: { +        transfers: [{ wtid: "1" } as any], +      }, +    }); + + +    const { result, waitForNextUpdate } = renderHook(() => { +      const moveCursor = (d: string) => { +        console.log("new position", d); +      }; +      const query = useInstanceTransfers({ payto_uri: 'payto://' }, moveCursor) +      return { query } +    }, { wrapper: TestingContext }); + +    assertJustExpectedRequestWereMade(env); + +    await waitForNextUpdate(); + +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      transfers: [{ wtid: "2" }, { wtid: "1" }], +    }); + +    expect(result.current.query.isReachingEnd).toBeTruthy() +    expect(result.current.query.isReachingStart).toBeTruthy() + +    await act(() => { +      if (!result.current?.query.ok) throw Error("not ok"); +      result.current.query.loadMore(); +    }); +    assertNoMoreRequestWereMade(env); + +    await act(() => { +      if (!result.current?.query.ok) throw Error("not ok"); +      result.current.query.loadMorePrev(); +    }); +    assertNoMoreRequestWereMade(env); + +    expect(result.current.query.data).toEqual({ +      transfers: [ +        { wtid: "2" }, +        { wtid: "1" }, +      ], +    }); +  }); + +  it("should load more if result brings more that PAGE_SIZE", async () => { +    const env = new AxiosMockEnvironment(); + +    const transfersFrom0to20 = Array.from({ length: 20 }).map((e, i) => ({ wtid: String(i) })) +    const transfersFrom20to40 = Array.from({ length: 20 }).map((e, i) => ({ wtid: String(i + 20) })) +    const transfersFrom20to0 = [...transfersFrom0to20].reverse() + +    env.addRequestExpectation(API_LIST_TRANSFERS, { +      qparam: { limit: 20, payto_uri: 'payto://' }, +      response: { +        transfers: transfersFrom0to20, +      }, +    }); + +    env.addRequestExpectation(API_LIST_TRANSFERS, { +      qparam: { limit: -20, payto_uri: 'payto://' }, +      response: { +        transfers: transfersFrom20to40, +      }, +    }); + +    const { result, waitForNextUpdate } = renderHook(() => { +      const moveCursor = (d: string) => { +        console.log("new position", d); +      }; +      const query = useInstanceTransfers({ payto_uri: 'payto://', position: '1' }, moveCursor) +      return { query } +    }, { wrapper: TestingContext }); + +    assertJustExpectedRequestWereMade(env); + +    await waitForNextUpdate({ timeout: 1 }); + +    expect(result.current?.query.ok).toBeTruthy(); +    if (!result.current?.query.ok) return; + +    expect(result.current.query.data).toEqual({ +      transfers: [...transfersFrom20to0, ...transfersFrom20to40], +    }); + +    expect(result.current.query.isReachingEnd).toBeFalsy() +    expect(result.current.query.isReachingStart).toBeFalsy() + +    env.addRequestExpectation(API_LIST_TRANSFERS, { +      qparam: { limit: -40, payto_uri: 'payto://', offset: "1" }, +      response: { +        transfers: [...transfersFrom20to40, { wtid: '41' }], +      }, +    }); + +    await act(() => { +      if (!result.current?.query.ok) throw Error("not ok"); +      result.current.query.loadMore(); +    }); +    await waitForNextUpdate({ timeout: 1 }); + +    assertJustExpectedRequestWereMade(env); + +    env.addRequestExpectation(API_LIST_TRANSFERS, { +      qparam: { limit: 40, payto_uri: 'payto://', offset: "1" }, +      response: { +        transfers: [...transfersFrom0to20, { wtid: '-1' }], +      }, +    }); + +    await act(() => { +      if (!result.current?.query.ok) throw Error("not ok"); +      result.current.query.loadMorePrev(); +    }); +    await waitForNextUpdate({ timeout: 1 }); +    assertJustExpectedRequestWereMade(env); + +    expect(result.current.query.data).toEqual({ +      transfers: [{ wtid: '-1' }, ...transfersFrom20to0, ...transfersFrom20to40, { wtid: '41' }], +    }); +  }); + + +}); diff --git a/packages/merchant-backoffice-ui/tests/stories.test.tsx b/packages/merchant-backoffice-ui/tests/stories.test.tsx new file mode 100644 index 000000000..5fb3483d2 --- /dev/null +++ b/packages/merchant-backoffice-ui/tests/stories.test.tsx @@ -0,0 +1,89 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE.  See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/> + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ +import { h, VNode } from "preact"; +import * as config from "../src/context/config"; +import * as i18n from "../src/context/translation"; +import { cleanup, render as originalRender } from "@testing-library/preact"; +import { SWRConfig } from "swr"; + +import fs from "fs"; + +function getFiles(dir: string, files_: string[] = []) { +  const files = fs.readdirSync(dir); +  for (const i in files) { +    const name = dir + "/" + files[i]; +    if (fs.statSync(name).isDirectory()) { +      getFiles(name, files_); +    } else { +      files_.push(name); +    } +  } +  return files_; +} + +const STORIES_NAME_REGEX = RegExp(".*.stories.tsx"); + +function render(vnode: VNode) { +  return originalRender( +    <SWRConfig +      value={{ +        provider: () => new Map(), +      }} +    > +      {vnode} +    </SWRConfig> +  ); +} + +import * as jedLib from "jed"; +const handler = new jedLib.Jed("en"); + +describe("storybook testing", () => { +  it("render every story", () => { +    jest +      .spyOn(config, "useConfigContext") +      .mockImplementation(() => ({ version: "1.0.0", currency: "EUR" })); +    jest.spyOn(i18n, "useTranslationContext").mockImplementation(() => ({ +      changeLanguage: () => null, +      handler, +      lang: "en", +    })); + +    getFiles("./src") +      .filter((f) => STORIES_NAME_REGEX.test(f)) +      .map((f) => { +        // const f = "./src/paths/instance/transfers/list/List.stories.tsx"; +        // eslint-disable-next-line @typescript-eslint/no-var-requires +        const s = require(`../${f}`); + +        delete s.default; +        Object.keys(s).forEach((k) => { +          const Component = s[k]; +          const vdom = <Component {...Component.args} />; +          expect(() => { +            const { unmount } = render(vdom); +            unmount(); +          }).not.toThrow(); //`problem rendering ${f} example ${k}` +          cleanup(); +        }); +      }); +  }); +});  | 
