diff --git a/packages/backend/src/accounting/psql/balance.test.ts b/packages/backend/src/accounting/psql/balance.test.ts index 027c6f1eff..46a5c9df89 100644 --- a/packages/backend/src/accounting/psql/balance.test.ts +++ b/packages/backend/src/accounting/psql/balance.test.ts @@ -39,7 +39,7 @@ describe('Balances', (): void => { afterEach(async (): Promise => { jest.useRealTimers() - await truncateTables(knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/accounting/psql/ledger-account/index.test.ts b/packages/backend/src/accounting/psql/ledger-account/index.test.ts index fdb9a898c2..483203ed44 100644 --- a/packages/backend/src/accounting/psql/ledger-account/index.test.ts +++ b/packages/backend/src/accounting/psql/ledger-account/index.test.ts @@ -13,15 +13,18 @@ import { ForeignKeyViolationError } from 'objection' import { createLedgerAccount } from '../../../tests/ledgerAccount' import { ServiceDependencies } from '../service' import { createAccount, getLiquidityAccount } from '.' +import { AppServices } from '../../../app' +import { IocContract } from '@adonisjs/fold' describe('Ledger Account', (): void => { + let deps: IocContract let serviceDeps: ServiceDependencies let appContainer: TestContainer let knex: Knex let asset: Asset beforeAll(async (): Promise => { - const deps = initIocContainer({ ...Config, useTigerBeetle: false }) + deps = initIocContainer({ ...Config, useTigerBeetle: false }) appContainer = await createTestApp(deps) serviceDeps = { logger: await deps.use('logger'), @@ -40,7 +43,7 @@ describe('Ledger Account', (): void => { afterEach(async (): Promise => { jest.useRealTimers() - await truncateTables(knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/accounting/psql/ledger-transfer/index.test.ts b/packages/backend/src/accounting/psql/ledger-transfer/index.test.ts index 3efb12f752..0a6e9ea452 100644 --- a/packages/backend/src/accounting/psql/ledger-transfer/index.test.ts +++ b/packages/backend/src/accounting/psql/ledger-transfer/index.test.ts @@ -22,15 +22,18 @@ import { } from '.' import { ServiceDependencies } from '../service' import { TransferError } from '../../errors' +import { AppServices } from '../../../app' +import { IocContract } from '@adonisjs/fold' describe('Ledger Transfer', (): void => { + let deps: IocContract let serviceDeps: ServiceDependencies let appContainer: TestContainer let knex: Knex let asset: Asset beforeAll(async (): Promise => { - const deps = initIocContainer({ ...Config, useTigerBeetle: false }) + deps = initIocContainer({ ...Config, useTigerBeetle: false }) appContainer = await createTestApp(deps) serviceDeps = { logger: await deps.use('logger'), @@ -65,7 +68,7 @@ describe('Ledger Transfer', (): void => { afterEach(async (): Promise => { jest.useRealTimers() - await truncateTables(knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/accounting/psql/ledger-transfer/model.test.ts b/packages/backend/src/accounting/psql/ledger-transfer/model.test.ts index aea03c66f5..9b1853b26e 100644 --- a/packages/backend/src/accounting/psql/ledger-transfer/model.test.ts +++ b/packages/backend/src/accounting/psql/ledger-transfer/model.test.ts @@ -47,7 +47,7 @@ describe('Ledger Transfer Model', (): void => { afterEach(async (): Promise => { jest.useRealTimers() - await truncateTables(knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/accounting/psql/service.test.ts b/packages/backend/src/accounting/psql/service.test.ts index e799068137..c0f544abe9 100644 --- a/packages/backend/src/accounting/psql/service.test.ts +++ b/packages/backend/src/accounting/psql/service.test.ts @@ -62,7 +62,7 @@ describe('Psql Accounting Service', (): void => { afterEach(async (): Promise => { jest.useRealTimers() - await truncateTables(knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/accounting/tigerbeetle/service.test.ts b/packages/backend/src/accounting/tigerbeetle/service.test.ts index 686f77ea21..de79ce21d3 100644 --- a/packages/backend/src/accounting/tigerbeetle/service.test.ts +++ b/packages/backend/src/accounting/tigerbeetle/service.test.ts @@ -57,7 +57,7 @@ describe('TigerBeetle Accounting Service', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/asset/model.test.ts b/packages/backend/src/asset/model.test.ts index d9464d7844..44d1ac98b7 100644 --- a/packages/backend/src/asset/model.test.ts +++ b/packages/backend/src/asset/model.test.ts @@ -25,7 +25,7 @@ describe('Models', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/asset/service.test.ts b/packages/backend/src/asset/service.test.ts index 5fd3042012..3d8e328f41 100644 --- a/packages/backend/src/asset/service.test.ts +++ b/packages/backend/src/asset/service.test.ts @@ -38,7 +38,7 @@ describe('Asset Service', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { @@ -414,7 +414,7 @@ describe('Asset Service using Cache', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/fee/model.test.ts b/packages/backend/src/fee/model.test.ts index 35c39d4d1b..b4b044354a 100644 --- a/packages/backend/src/fee/model.test.ts +++ b/packages/backend/src/fee/model.test.ts @@ -23,7 +23,7 @@ describe('Fee Model', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/fee/service.test.ts b/packages/backend/src/fee/service.test.ts index e57068f7e3..5e80acde78 100644 --- a/packages/backend/src/fee/service.test.ts +++ b/packages/backend/src/fee/service.test.ts @@ -4,7 +4,6 @@ import { TestContainer, createTestApp } from '../tests/app' import { initIocContainer } from '..' import { Config } from '../config/app' import { FeeService } from './service' -import { Knex } from 'knex' import { truncateTables } from '../tests/tableManager' import { createAsset } from '../tests/asset' import { Asset } from '../asset/model' @@ -18,14 +17,12 @@ import { Pagination, SortOrder } from '../shared/baseModel' describe('Fee Service', (): void => { let deps: IocContract let appContainer: TestContainer - let knex: Knex let feeService: FeeService let asset: Asset beforeAll(async (): Promise => { deps = await initIocContainer(Config) appContainer = await createTestApp(deps) - knex = appContainer.knex feeService = await deps.use('feeService') }) @@ -34,7 +31,7 @@ describe('Fee Service', (): void => { }) afterEach(async (): Promise => { - await truncateTables(knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/graphql/middleware/index.test.ts b/packages/backend/src/graphql/middleware/index.test.ts index 6456b418df..5728a0170c 100644 --- a/packages/backend/src/graphql/middleware/index.test.ts +++ b/packages/backend/src/graphql/middleware/index.test.ts @@ -33,7 +33,7 @@ describe('GraphQL Middleware', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/graphql/resolvers/accounting_transfer.psql.test.ts b/packages/backend/src/graphql/resolvers/accounting_transfer.psql.test.ts index 1ec2f06a61..fea75ff56c 100644 --- a/packages/backend/src/graphql/resolvers/accounting_transfer.psql.test.ts +++ b/packages/backend/src/graphql/resolvers/accounting_transfer.psql.test.ts @@ -37,7 +37,7 @@ describe('Accounting Transfer', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/graphql/resolvers/accounting_transfer.tigerbeetle.test.ts b/packages/backend/src/graphql/resolvers/accounting_transfer.tigerbeetle.test.ts index b6fe08cee7..e35a9d41ac 100644 --- a/packages/backend/src/graphql/resolvers/accounting_transfer.tigerbeetle.test.ts +++ b/packages/backend/src/graphql/resolvers/accounting_transfer.tigerbeetle.test.ts @@ -39,7 +39,7 @@ describe('TigerBeetle: Accounting Transfer', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/graphql/resolvers/asset.test.ts b/packages/backend/src/graphql/resolvers/asset.test.ts index 8ee9e279bd..12b6fdb2af 100644 --- a/packages/backend/src/graphql/resolvers/asset.test.ts +++ b/packages/backend/src/graphql/resolvers/asset.test.ts @@ -54,7 +54,7 @@ describe('Asset Resolvers', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/graphql/resolvers/auto-peering.test.ts b/packages/backend/src/graphql/resolvers/auto-peering.test.ts index 49715fdfe0..0d7862c7db 100644 --- a/packages/backend/src/graphql/resolvers/auto-peering.test.ts +++ b/packages/backend/src/graphql/resolvers/auto-peering.test.ts @@ -93,7 +93,7 @@ describe('Auto Peering Resolvers', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/graphql/resolvers/combined_payments.test.ts b/packages/backend/src/graphql/resolvers/combined_payments.test.ts index 2f3ac34706..8cea2d69c0 100644 --- a/packages/backend/src/graphql/resolvers/combined_payments.test.ts +++ b/packages/backend/src/graphql/resolvers/combined_payments.test.ts @@ -34,7 +34,7 @@ describe('Payment', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/graphql/resolvers/fee.test.ts b/packages/backend/src/graphql/resolvers/fee.test.ts index ee7306f2ef..8eade969c3 100644 --- a/packages/backend/src/graphql/resolvers/fee.test.ts +++ b/packages/backend/src/graphql/resolvers/fee.test.ts @@ -31,7 +31,7 @@ describe('Fee Resolvers', () => { }) afterEach(async () => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async () => { diff --git a/packages/backend/src/graphql/resolvers/incoming_payment.test.ts b/packages/backend/src/graphql/resolvers/incoming_payment.test.ts index 77dfc66d96..30cbfa893b 100644 --- a/packages/backend/src/graphql/resolvers/incoming_payment.test.ts +++ b/packages/backend/src/graphql/resolvers/incoming_payment.test.ts @@ -50,7 +50,7 @@ describe('Incoming Payment Resolver', (): void => { }) afterAll(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) await appContainer.apolloClient.stop() await appContainer.shutdown() }) diff --git a/packages/backend/src/graphql/resolvers/liquidity.test.ts b/packages/backend/src/graphql/resolvers/liquidity.test.ts index 457099cb36..5c417e32c1 100644 --- a/packages/backend/src/graphql/resolvers/liquidity.test.ts +++ b/packages/backend/src/graphql/resolvers/liquidity.test.ts @@ -60,7 +60,7 @@ describe('Liquidity Resolvers', (): void => { }) afterAll(async (): Promise => { - await truncateTables(knex) + await truncateTables(deps) await appContainer.apolloClient.stop() await appContainer.shutdown() }) diff --git a/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts b/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts index a4698992dc..9bf4c88788 100644 --- a/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts +++ b/packages/backend/src/graphql/resolvers/outgoing_payment.test.ts @@ -58,7 +58,7 @@ describe('OutgoingPayment Resolvers', (): void => { afterEach(async (): Promise => { jest.restoreAllMocks() - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/graphql/resolvers/peer.test.ts b/packages/backend/src/graphql/resolvers/peer.test.ts index 4aec23a2e8..215169895e 100644 --- a/packages/backend/src/graphql/resolvers/peer.test.ts +++ b/packages/backend/src/graphql/resolvers/peer.test.ts @@ -68,7 +68,7 @@ describe('Peer Resolvers', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/graphql/resolvers/quote.test.ts b/packages/backend/src/graphql/resolvers/quote.test.ts index 246f7ac486..96157ee50d 100644 --- a/packages/backend/src/graphql/resolvers/quote.test.ts +++ b/packages/backend/src/graphql/resolvers/quote.test.ts @@ -46,7 +46,7 @@ describe('Quote Resolvers', (): void => { afterEach(async (): Promise => { jest.restoreAllMocks() - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/graphql/resolvers/tenant.test.ts b/packages/backend/src/graphql/resolvers/tenant.test.ts index 177ed85015..99cda31197 100644 --- a/packages/backend/src/graphql/resolvers/tenant.test.ts +++ b/packages/backend/src/graphql/resolvers/tenant.test.ts @@ -64,14 +64,15 @@ describe('Tenant Resolvers', (): void => { let deps: IocContract let appContainer: TestContainer let config: IAppConfig + const dbSchema = 'tenant_resolver_test_schema' beforeAll(async (): Promise => { deps = await initIocContainer({ ...Config, - dbSchema: 'tenant_service_test_schema' + dbSchema }) - appContainer = await createTestApp(deps) config = await deps.use('config') + appContainer = await createTestApp(deps) const authServiceClient = await deps.use('authServiceClient') jest .spyOn(authServiceClient.tenant, 'create') @@ -85,7 +86,7 @@ describe('Tenant Resolvers', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex, true) + await truncateTables(deps, { truncateTenants: true }) }) afterAll(async (): Promise => { await appContainer.apolloClient.stop() @@ -346,25 +347,91 @@ describe('Tenant Resolvers', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) - test.each` - isOperator | description - ${true} | ${'operator'} - ${false} | ${'tenant'} - `( - 'can update a tenant as $description', - async ({ isOperator }): Promise => { - const client = isOperator - ? appContainer.apolloClient - : tenantedApolloClient - const updateInput = { - ...generateTenantInput(), - id: tenant.id - } + test('can update a tenant as operator', async (): Promise => { + // operator cant update apiSecret + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { apiSecret, ...input } = generateTenantInput() + const updateInput = { + ...input, + id: tenant.id + } + + const mutation = await appContainer.apolloClient + .mutate({ + mutation: gql` + mutation UpdateTenant($input: UpdateTenantInput!) { + updateTenant(input: $input) { + tenant { + id + email + apiSecret + idpConsentUrl + idpSecret + publicName + } + } + } + `, + variables: { + input: updateInput + } + }) + .then((query): TenantMutationResponse => query.data?.updateTenant) + + expect(mutation.tenant).toEqual({ + ...updateInput, + __typename: 'Tenant', + apiSecret: expect.any(String) + }) + }) + + test('can update a tenant as tenant', async (): Promise => { + const updateInput = { + ...generateTenantInput(), + id: tenant.id + } + + const mutation = await tenantedApolloClient + .mutate({ + mutation: gql` + mutation UpdateTenant($input: UpdateTenantInput!) { + updateTenant(input: $input) { + tenant { + id + email + apiSecret + idpConsentUrl + idpSecret + publicName + } + } + } + `, + variables: { + input: updateInput + } + }) + .then((query): TenantMutationResponse => query.data?.updateTenant) - const mutation = await client + expect(mutation.tenant).toEqual({ + ...updateInput, + __typename: 'Tenant' + }) + }) + + test('Cannot update API secret as operator', async (): Promise => { + const updateInput = { + ...generateTenantInput(), + id: tenant.id, + apiSecret: 'newApiSecretValue' + } + + try { + expect.assertions(2) + await appContainer.apolloClient .mutate({ mutation: gql` mutation UpdateTenant($input: UpdateTenantInput!) { @@ -385,13 +452,19 @@ describe('Tenant Resolvers', (): void => { } }) .then((query): TenantMutationResponse => query.data?.updateTenant) - - expect(mutation.tenant).toEqual({ - ...updateInput, - __typename: 'Tenant' - }) + } catch (error) { + expect(error).toBeInstanceOf(ApolloError) + expect((error as ApolloError).graphQLErrors).toContainEqual( + expect.objectContaining({ + message: 'Operator cannot update apiSecret over admin api', + extensions: expect.objectContaining({ + code: GraphQLErrorCode.BadUserInput + }) + }) + ) } - ) + }) + test('Cannot update other tenant as non-operator', async (): Promise => { const firstTenant = await createTenant(deps) const secondTenant = await createTenant(deps) diff --git a/packages/backend/src/graphql/resolvers/tenant.ts b/packages/backend/src/graphql/resolvers/tenant.ts index 6fdddf16da..eae36ebc3a 100644 --- a/packages/backend/src/graphql/resolvers/tenant.ts +++ b/packages/backend/src/graphql/resolvers/tenant.ts @@ -122,6 +122,17 @@ export const updateTenant: MutationResolvers['updateTenan }) } + if (isOperator && 'apiSecret' in args.input) { + throw new GraphQLError( + 'Operator cannot update apiSecret over admin api', + { + extensions: { + code: GraphQLErrorCode.BadUserInput + } + } + ) + } + const tenantService = await ctx.container.use('tenantService') try { const updatedTenant = await tenantService.update(args.input) diff --git a/packages/backend/src/graphql/resolvers/tenant_settings.test.ts b/packages/backend/src/graphql/resolvers/tenant_settings.test.ts index c4afbd6732..473f1e0795 100644 --- a/packages/backend/src/graphql/resolvers/tenant_settings.test.ts +++ b/packages/backend/src/graphql/resolvers/tenant_settings.test.ts @@ -59,11 +59,12 @@ function createTenantedApolloClient( describe('Tenant Settings Resolvers', (): void => { let deps: IocContract let appContainer: TestContainer + const dbSchema = 'tenant_settings_resolver_test_schema' beforeAll(async (): Promise => { deps = initIocContainer({ ...Config, - dbSchema: 'tenant_settings_service_test_schema' + dbSchema }) appContainer = await createTestApp(deps) @@ -80,7 +81,7 @@ describe('Tenant Settings Resolvers', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex, true) + await truncateTables(deps, { truncateTenants: true }) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts b/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts index fc4400f9af..9cd736f973 100644 --- a/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts +++ b/packages/backend/src/graphql/resolvers/walletAddressKey.test.ts @@ -41,7 +41,7 @@ describe('Wallet Address Key Resolvers', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/graphql/resolvers/wallet_address.test.ts b/packages/backend/src/graphql/resolvers/wallet_address.test.ts index 88ae1d78d8..045956da46 100644 --- a/packages/backend/src/graphql/resolvers/wallet_address.test.ts +++ b/packages/backend/src/graphql/resolvers/wallet_address.test.ts @@ -3,7 +3,11 @@ import { gql, ApolloError } from '@apollo/client' import { Knex } from 'knex' import { v4 as uuid } from 'uuid' -import { createTestApp, TestContainer } from '../../tests/app' +import { + createApolloClient, + createTestApp, + TestContainer +} from '../../tests/app' import { IocContract } from '@adonisjs/fold' import { AppServices } from '../../app' import { Asset } from '../../asset/model' @@ -38,6 +42,7 @@ import { GraphQLErrorCode } from '../errors' import { AssetService } from '../../asset/service' import { faker } from '@faker-js/faker' import { Tenant } from '../../tenants/model' +import { createTenant } from '../../tests/tenant' describe('Wallet Address Resolvers', (): void => { let deps: IocContract @@ -49,8 +54,7 @@ describe('Wallet Address Resolvers', (): void => { beforeAll(async (): Promise => { deps = initIocContainer({ ...Config, - localCacheDuration: 0, - adminApiSecret: '123' //to force not being an operator. + localCacheDuration: 0 }) appContainer = await createTestApp(deps) knex = appContainer.knex @@ -59,7 +63,7 @@ describe('Wallet Address Resolvers', (): void => { }) afterEach(async (): Promise => { - await truncateTables(knex) + await truncateTables(deps) }) afterAll(async (): Promise => { @@ -315,14 +319,22 @@ describe('Wallet Address Resolvers', (): void => { }) test('bad input data when not allowed to perform cross tenant create', async (): Promise => { + // Make request as non-operator. + const nonOperatorTenant = await createTenant(deps) + const tenantedApolloClient = await createApolloClient( + appContainer.container, + appContainer.app, + nonOperatorTenant.id + ) + const badInputData = { - tenantId: 'ae4950b6-3e1b-4e50-ad24-25c065bdd3a9', + tenantId: uuid(), // some tenant other than requestor assetId: input.assetId, url: input.url } try { expect.assertions(2) - await appContainer.apolloClient + await tenantedApolloClient .mutate({ mutation: gql` mutation CreateWalletAddress( @@ -362,6 +374,69 @@ describe('Wallet Address Resolvers', (): void => { ) } }) + + test('Operator can perform cross tenant create', async (): Promise => { + // Setup non-tenant operator and form request for it from operator + const nonOperatorTenant = await createTenant(deps) + const asset = await createAsset( + deps, + { + code: 'xyz', + scale: 2 + }, + nonOperatorTenant.id + ) + const input = { + tenantId: nonOperatorTenant.id, + assetId: asset.id, + url: 'https://bob.me/.well-known/pay' + } + const response = await appContainer.apolloClient // operator client + .mutate({ + mutation: gql` + mutation CreateWalletAddress($input: CreateWalletAddressInput!) { + createWalletAddress(input: $input) { + walletAddress { + id + asset { + code + scale + } + url + } + } + } + `, + variables: { + input + } + }) + .then((query): CreateWalletAddressMutationResponse => { + if (query.data) { + return query.data.createWalletAddress + } else { + throw new Error('Data was empty') + } + }) + + assert.ok(response.walletAddress) + expect(response.walletAddress).toEqual({ + __typename: 'WalletAddress', + id: response.walletAddress.id, + url: input.url, + asset: { + __typename: 'Asset', + code: asset.code, + scale: asset.scale + } + }) + await expect( + walletAddressService.get(response.walletAddress.id) + ).resolves.toMatchObject({ + id: response.walletAddress.id, + asset + }) + }) }) describe('Update Wallet Address', (): void => { diff --git a/packages/backend/src/graphql/resolvers/webhooks.test.ts b/packages/backend/src/graphql/resolvers/webhooks.test.ts index b5ecda5ccb..9d6afddfca 100644 --- a/packages/backend/src/graphql/resolvers/webhooks.test.ts +++ b/packages/backend/src/graphql/resolvers/webhooks.test.ts @@ -20,7 +20,7 @@ describe('Webhook Events Query', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 0dcde43128..922f375c52 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -236,7 +236,8 @@ export function initIocContainer( knex: await deps.use('knex'), tenantCache: await deps.use('tenantCache'), authServiceClient: deps.use('authServiceClient'), - tenantSettingService: await deps.use('tenantSettingService') + tenantSettingService: await deps.use('tenantSettingService'), + config: await deps.use('config') }) }) @@ -768,6 +769,11 @@ export const start = async ( Model.knex(knex) + // Update Operator Tenant from config + const tenantService = await container.use('tenantService') + const error = await tenantService.updateOperatorApiSecretFromConfig() + if (error) throw error + await app.boot() await app.startAdminServer(config.adminPort) logger.info(`Admin listening on ${app.getAdminPort()}`) diff --git a/packages/backend/src/open_payments/authServer/service.test.ts b/packages/backend/src/open_payments/authServer/service.test.ts index 1fd758a204..9b4a4af00a 100644 --- a/packages/backend/src/open_payments/authServer/service.test.ts +++ b/packages/backend/src/open_payments/authServer/service.test.ts @@ -24,7 +24,7 @@ describe('Auth Server Service', (): void => { }) afterEach(async (): Promise => { - await truncateTables(knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/open_payments/grant/service.test.ts b/packages/backend/src/open_payments/grant/service.test.ts index eb10c65d71..fe7a0ba84e 100644 --- a/packages/backend/src/open_payments/grant/service.test.ts +++ b/packages/backend/src/open_payments/grant/service.test.ts @@ -46,7 +46,7 @@ describe('Grant Service', (): void => { afterEach(async (): Promise => { jest.useRealTimers() - await truncateTables(knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/open_payments/payment/combined/service.test.ts b/packages/backend/src/open_payments/payment/combined/service.test.ts index 60251049ec..1f2bf75580 100644 --- a/packages/backend/src/open_payments/payment/combined/service.test.ts +++ b/packages/backend/src/open_payments/payment/combined/service.test.ts @@ -4,7 +4,6 @@ import { TestContainer, createTestApp } from '../../../tests/app' import { initIocContainer } from '../../..' import { Config } from '../../../config/app' import { CombinedPaymentService } from './service' -import { Knex } from 'knex' import { truncateTables } from '../../../tests/tableManager' import { getPageTests } from '../../../shared/baseModel.test' import { createOutgoingPayment } from '../../../tests/outgoingPayment' @@ -25,7 +24,6 @@ import { describe('Combined Payment Service', (): void => { let deps: IocContract let appContainer: TestContainer - let knex: Knex let combinedPaymentService: CombinedPaymentService let tenantId: string let sendAsset: Asset @@ -36,7 +34,6 @@ describe('Combined Payment Service', (): void => { beforeAll(async (): Promise => { deps = await initIocContainer(Config) appContainer = await createTestApp(deps) - knex = appContainer.knex combinedPaymentService = await deps.use('combinedPaymentService') tenantId = Config.operatorTenantId }) @@ -57,7 +54,7 @@ describe('Combined Payment Service', (): void => { }) afterEach(async (): Promise => { - await truncateTables(knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/open_payments/payment/incoming/model.test.ts b/packages/backend/src/open_payments/payment/incoming/model.test.ts index a71cf23d4b..3f1f91c76e 100644 --- a/packages/backend/src/open_payments/payment/incoming/model.test.ts +++ b/packages/backend/src/open_payments/payment/incoming/model.test.ts @@ -29,7 +29,7 @@ describe('Models', (): void => { afterEach(async (): Promise => { jest.useRealTimers() - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/open_payments/payment/incoming/routes.test.ts b/packages/backend/src/open_payments/payment/incoming/routes.test.ts index 06b0c69193..506aadf3ea 100644 --- a/packages/backend/src/open_payments/payment/incoming/routes.test.ts +++ b/packages/backend/src/open_payments/payment/incoming/routes.test.ts @@ -72,7 +72,7 @@ describe('Incoming Payment Routes', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/open_payments/payment/incoming/service.test.ts b/packages/backend/src/open_payments/payment/incoming/service.test.ts index afacfd0452..39b436341b 100644 --- a/packages/backend/src/open_payments/payment/incoming/service.test.ts +++ b/packages/backend/src/open_payments/payment/incoming/service.test.ts @@ -65,7 +65,7 @@ describe('Incoming Payment Service', (): void => { afterEach(async (): Promise => { jest.useRealTimers() - await truncateTables(knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/open_payments/payment/incoming_remote/service.test.ts b/packages/backend/src/open_payments/payment/incoming_remote/service.test.ts index a971c3523a..89d06494eb 100644 --- a/packages/backend/src/open_payments/payment/incoming_remote/service.test.ts +++ b/packages/backend/src/open_payments/payment/incoming_remote/service.test.ts @@ -1,4 +1,3 @@ -import { Knex } from 'knex' import { v4 as uuid } from 'uuid' import { RemoteIncomingPaymentService } from './service' import { createTestApp, TestContainer } from '../../../tests/app' @@ -26,7 +25,6 @@ describe('Remote Incoming Payment Service', (): void => { let deps: IocContract let appContainer: TestContainer let remoteIncomingPaymentService: RemoteIncomingPaymentService - let knex: Knex let openPaymentsClient: OpenPaymentsClient let grantService: GrantService @@ -35,7 +33,6 @@ describe('Remote Incoming Payment Service', (): void => { appContainer = await createTestApp(deps) openPaymentsClient = await deps.use('openPaymentsClient') grantService = await deps.use('grantService') - knex = appContainer.knex remoteIncomingPaymentService = await deps.use( 'remoteIncomingPaymentService' ) @@ -43,7 +40,7 @@ describe('Remote Incoming Payment Service', (): void => { afterEach(async (): Promise => { jest.useRealTimers() - await truncateTables(knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/open_payments/payment/outgoing/model.test.ts b/packages/backend/src/open_payments/payment/outgoing/model.test.ts index f724192d6c..5c3538c13d 100644 --- a/packages/backend/src/open_payments/payment/outgoing/model.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/model.test.ts @@ -23,7 +23,7 @@ describe('Outgoing Payment Event Model', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/open_payments/payment/outgoing/routes.test.ts b/packages/backend/src/open_payments/payment/outgoing/routes.test.ts index 98a744f1e3..4f04a3642c 100644 --- a/packages/backend/src/open_payments/payment/outgoing/routes.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/routes.test.ts @@ -88,7 +88,7 @@ describe('Outgoing Payment Routes', (): void => { }) afterEach(async (): Promise => { - await truncateTables(knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/open_payments/payment/outgoing/service.test.ts b/packages/backend/src/open_payments/payment/outgoing/service.test.ts index 05007973d3..4329933f3e 100644 --- a/packages/backend/src/open_payments/payment/outgoing/service.test.ts +++ b/packages/backend/src/open_payments/payment/outgoing/service.test.ts @@ -324,7 +324,7 @@ describe('OutgoingPaymentService', (): void => { afterEach(async (): Promise => { jest.restoreAllMocks() receiverWalletAddress.scope?.persist(false) - await truncateTables(knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/open_payments/quote/routes.test.ts b/packages/backend/src/open_payments/quote/routes.test.ts index 3c161305fb..2c480bd45d 100644 --- a/packages/backend/src/open_payments/quote/routes.test.ts +++ b/packages/backend/src/open_payments/quote/routes.test.ts @@ -89,7 +89,7 @@ describe('Quote Routes', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/open_payments/quote/service.test.ts b/packages/backend/src/open_payments/quote/service.test.ts index ef91ab9568..9ecc0601ba 100644 --- a/packages/backend/src/open_payments/quote/service.test.ts +++ b/packages/backend/src/open_payments/quote/service.test.ts @@ -131,7 +131,7 @@ describe('QuoteService', (): void => { afterEach(async (): Promise => { jest.restoreAllMocks() - await truncateTables(knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/open_payments/receiver/model.test.ts b/packages/backend/src/open_payments/receiver/model.test.ts index 5ed02b0144..295de1dec9 100644 --- a/packages/backend/src/open_payments/receiver/model.test.ts +++ b/packages/backend/src/open_payments/receiver/model.test.ts @@ -29,7 +29,7 @@ describe('Receiver Model', (): void => { afterEach(async (): Promise => { jest.useRealTimers() - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/open_payments/receiver/service.test.ts b/packages/backend/src/open_payments/receiver/service.test.ts index 2077703d1c..4881e0617f 100644 --- a/packages/backend/src/open_payments/receiver/service.test.ts +++ b/packages/backend/src/open_payments/receiver/service.test.ts @@ -72,7 +72,7 @@ describe('Receiver Service', (): void => { afterEach(async (): Promise => { jest.restoreAllMocks() - await truncateTables(knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/open_payments/wallet_address/key/routes.test.ts b/packages/backend/src/open_payments/wallet_address/key/routes.test.ts index 0ea2017292..1608b3580f 100644 --- a/packages/backend/src/open_payments/wallet_address/key/routes.test.ts +++ b/packages/backend/src/open_payments/wallet_address/key/routes.test.ts @@ -37,7 +37,7 @@ describe('Wallet Address Keys Routes', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/open_payments/wallet_address/key/service.test.ts b/packages/backend/src/open_payments/wallet_address/key/service.test.ts index 57363cb649..c9a0970aa6 100644 --- a/packages/backend/src/open_payments/wallet_address/key/service.test.ts +++ b/packages/backend/src/open_payments/wallet_address/key/service.test.ts @@ -37,7 +37,7 @@ describe('Wallet Address Key Service', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/open_payments/wallet_address/middleware.test.ts b/packages/backend/src/open_payments/wallet_address/middleware.test.ts index 3ae2438ec1..af62847107 100644 --- a/packages/backend/src/open_payments/wallet_address/middleware.test.ts +++ b/packages/backend/src/open_payments/wallet_address/middleware.test.ts @@ -53,7 +53,7 @@ describe('Wallet Address Middleware', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/open_payments/wallet_address/model.test.ts b/packages/backend/src/open_payments/wallet_address/model.test.ts index 3dd586873d..c46499557b 100644 --- a/packages/backend/src/open_payments/wallet_address/model.test.ts +++ b/packages/backend/src/open_payments/wallet_address/model.test.ts @@ -377,7 +377,7 @@ describe('Models', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/open_payments/wallet_address/routes.test.ts b/packages/backend/src/open_payments/wallet_address/routes.test.ts index e30d4056fb..87da55b2ec 100644 --- a/packages/backend/src/open_payments/wallet_address/routes.test.ts +++ b/packages/backend/src/open_payments/wallet_address/routes.test.ts @@ -34,7 +34,7 @@ describe('Wallet Address Routes', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/open_payments/wallet_address/service.test.ts b/packages/backend/src/open_payments/wallet_address/service.test.ts index 5d6f139996..1fb01fc48d 100644 --- a/packages/backend/src/open_payments/wallet_address/service.test.ts +++ b/packages/backend/src/open_payments/wallet_address/service.test.ts @@ -50,7 +50,7 @@ describe('Open Payments Wallet Address Service', (): void => { afterEach(async (): Promise => { jest.useRealTimers() - await truncateTables(knex) + await truncateTables(deps) }) afterAll(async (): Promise => { @@ -847,7 +847,7 @@ describe('Open Payments Wallet Address Service using Cache', (): void => { afterEach(async (): Promise => { jest.useRealTimers() - await truncateTables(knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/payment-method/handler/service.test.ts b/packages/backend/src/payment-method/handler/service.test.ts index 5bcfaf36ea..519904d0d4 100644 --- a/packages/backend/src/payment-method/handler/service.test.ts +++ b/packages/backend/src/payment-method/handler/service.test.ts @@ -36,7 +36,7 @@ describe('PaymentMethodHandlerService', (): void => { afterEach(async (): Promise => { jest.restoreAllMocks() - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/payment-method/ilp/auto-peering/routes.test.ts b/packages/backend/src/payment-method/ilp/auto-peering/routes.test.ts index 2a5f16b2a7..8c6a876ca5 100644 --- a/packages/backend/src/payment-method/ilp/auto-peering/routes.test.ts +++ b/packages/backend/src/payment-method/ilp/auto-peering/routes.test.ts @@ -23,7 +23,7 @@ describe('Auto Peering Routes', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/payment-method/ilp/auto-peering/service.test.ts b/packages/backend/src/payment-method/ilp/auto-peering/service.test.ts index af2c2e2282..6e5d14be75 100644 --- a/packages/backend/src/payment-method/ilp/auto-peering/service.test.ts +++ b/packages/backend/src/payment-method/ilp/auto-peering/service.test.ts @@ -38,7 +38,7 @@ describe('Auto Peering Service', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/payment-method/ilp/peer-http-token/service.test.ts b/packages/backend/src/payment-method/ilp/peer-http-token/service.test.ts index d9624fbb0b..1fc6c5675c 100644 --- a/packages/backend/src/payment-method/ilp/peer-http-token/service.test.ts +++ b/packages/backend/src/payment-method/ilp/peer-http-token/service.test.ts @@ -28,7 +28,7 @@ describe('HTTP Token Service', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/payment-method/ilp/peer/model.test.ts b/packages/backend/src/payment-method/ilp/peer/model.test.ts index a664509eb1..355a579fe5 100644 --- a/packages/backend/src/payment-method/ilp/peer/model.test.ts +++ b/packages/backend/src/payment-method/ilp/peer/model.test.ts @@ -33,7 +33,7 @@ describe('Models', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/payment-method/ilp/peer/service.test.ts b/packages/backend/src/payment-method/ilp/peer/service.test.ts index 9d0a7ae430..084cd7de63 100644 --- a/packages/backend/src/payment-method/ilp/peer/service.test.ts +++ b/packages/backend/src/payment-method/ilp/peer/service.test.ts @@ -55,7 +55,7 @@ describe('Peer Service', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/payment-method/ilp/service.test.ts b/packages/backend/src/payment-method/ilp/service.test.ts index 48e59a88c4..29f380ba61 100644 --- a/packages/backend/src/payment-method/ilp/service.test.ts +++ b/packages/backend/src/payment-method/ilp/service.test.ts @@ -80,7 +80,7 @@ describe('IlpPaymentService', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) jest.restoreAllMocks() nock.cleanAll() diff --git a/packages/backend/src/payment-method/ilp/spsp/middleware.test.ts b/packages/backend/src/payment-method/ilp/spsp/middleware.test.ts index 896c30dac6..8d24d79e25 100644 --- a/packages/backend/src/payment-method/ilp/spsp/middleware.test.ts +++ b/packages/backend/src/payment-method/ilp/spsp/middleware.test.ts @@ -49,7 +49,7 @@ describe('SPSP Middleware', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/payment-method/ilp/spsp/routes.test.ts b/packages/backend/src/payment-method/ilp/spsp/routes.test.ts index b02c18274c..fdef34c42e 100644 --- a/packages/backend/src/payment-method/ilp/spsp/routes.test.ts +++ b/packages/backend/src/payment-method/ilp/spsp/routes.test.ts @@ -41,7 +41,7 @@ describe('SPSP Routes', (): void => { }) afterAll(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) await appContainer.shutdown() }) diff --git a/packages/backend/src/payment-method/ilp/stream-credentials/service.test.ts b/packages/backend/src/payment-method/ilp/stream-credentials/service.test.ts index 3092f76491..1587af57c1 100644 --- a/packages/backend/src/payment-method/ilp/stream-credentials/service.test.ts +++ b/packages/backend/src/payment-method/ilp/stream-credentials/service.test.ts @@ -37,7 +37,7 @@ describe('Stream Credentials Service', (): void => { }) afterEach(async (): Promise => { - await truncateTables(knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/payment-method/local/service.test.ts b/packages/backend/src/payment-method/local/service.test.ts index ee2afc5d2b..4729d2d973 100644 --- a/packages/backend/src/payment-method/local/service.test.ts +++ b/packages/backend/src/payment-method/local/service.test.ts @@ -87,7 +87,7 @@ describe('LocalPaymentService', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) jest.restoreAllMocks() nock.cleanAll() diff --git a/packages/backend/src/shared/pagination.test.ts b/packages/backend/src/shared/pagination.test.ts index 85d3df3e01..1136e50a7a 100644 --- a/packages/backend/src/shared/pagination.test.ts +++ b/packages/backend/src/shared/pagination.test.ts @@ -35,7 +35,7 @@ describe('Pagination', (): void => { }) afterEach(async (): Promise => { - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/shared/utils.test.ts b/packages/backend/src/shared/utils.test.ts index 63b00099ca..6adebe8a04 100644 --- a/packages/backend/src/shared/utils.test.ts +++ b/packages/backend/src/shared/utils.test.ts @@ -317,7 +317,7 @@ describe('utils', (): void => { afterEach(async (): Promise => { await redis.flushall() - await truncateTables(appContainer.knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/tenants/errors.ts b/packages/backend/src/tenants/errors.ts new file mode 100644 index 0000000000..d3ed0ac0b6 --- /dev/null +++ b/packages/backend/src/tenants/errors.ts @@ -0,0 +1,13 @@ +export enum TenantError { + TenantNotFound = 'TenantNotFound' +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types +export const isTenantError = (o: any): o is TenantError => + Object.values(TenantError).includes(o) + +export const errorToMessage: { + [key in TenantError]: string +} = { + [TenantError.TenantNotFound]: 'Tenant not found' +} diff --git a/packages/backend/src/tenants/service.test.ts b/packages/backend/src/tenants/service.test.ts index b5c3f01af7..6dcb645737 100644 --- a/packages/backend/src/tenants/service.test.ts +++ b/packages/backend/src/tenants/service.test.ts @@ -1,7 +1,6 @@ import assert from 'assert' import { faker } from '@faker-js/faker' import { IocContract } from '@adonisjs/fold' -import nock from 'nock' import { Knex } from 'knex' import { AppServices } from '../app' import { initIocContainer } from '..' @@ -18,6 +17,7 @@ import { AuthServiceClient } from '../auth-service-client/client' import { withConfigOverride } from '../tests/helpers' import { TenantSetting } from './settings/model' import { TenantSettingService } from './settings/service' +import { isTenantError, TenantError } from './errors' describe('Tenant Service', (): void => { let deps: IocContract @@ -34,20 +34,19 @@ describe('Tenant Service', (): void => { ...Config, dbSchema }) - appContainer = await createTestApp(deps) - tenantService = await deps.use('tenantService') knex = await deps.use('knex') config = await deps.use('config') + appContainer = await createTestApp(deps) + tenantService = await deps.use('tenantService') authServiceClient = await deps.use('authServiceClient') tenantSettingsService = await deps.use('tenantSettingService') }) afterEach(async (): Promise => { - await truncateTables(knex, true, dbSchema) + await truncateTables(deps, { truncateTenants: true }) }) afterAll(async (): Promise => { - nock.cleanAll() await appContainer.shutdown() }) @@ -447,3 +446,95 @@ describe('Tenant Service', (): void => { }) }) }) + +describe('Tenant Service (no tenant truncate)', (): void => { + let deps: IocContract + let config: IAppConfig + let appContainer: TestContainer + let tenantService: TenantService + let knex: Knex + let tenantCache: CacheDataStore + let updateSpyWasCalled: boolean + const dbSchema = 'tenant_service_test_schema2' + + beforeAll(async (): Promise => { + deps = initIocContainer({ + ...Config, + dbSchema + }) + knex = await deps.use('knex') + config = await deps.use('config') + tenantService = await deps.use('tenantService') + tenantCache = await deps.use('tenantCache') + + const updateOperatorSecretSpy = jest.spyOn( + tenantService, + 'updateOperatorApiSecretFromConfig' + ) + appContainer = await createTestApp(deps) + updateSpyWasCalled = updateOperatorSecretSpy.mock.calls.length > 0 + }) + + afterEach(async (): Promise => { + await truncateTables(deps) + }) + + afterAll(async (): Promise => { + await appContainer.shutdown() + }) + describe('updateOperatorApiSecretFromConfig', () => { + test('called on application start', async (): Promise => { + expect(updateSpyWasCalled).toBe(true) + }) + + test('updates secret if changed', async (): Promise => { + // Setup operator with different secret than the config. + // As-if the api secret was set from a different config value originally. + const initialApiSecret = '123' + assert(initialApiSecret !== config.adminApiSecret) + const tenant = await Tenant.query(knex).patchAndFetchById( + config.operatorTenantId, + { apiSecret: initialApiSecret } + ) + assert(tenant) + expect(tenant.apiSecret).toBe(initialApiSecret) + + const error = await tenantService.updateOperatorApiSecretFromConfig() + expect(error).toBe(undefined) + + const updated = await Tenant.query(knex).findById(tenant.id) + assert(updated) + expect(updated.apiSecret).toBe(config.adminApiSecret) + + const cacheUpdated = await tenantCache.get(tenant.id) + assert(cacheUpdated) + expect(cacheUpdated.apiSecret).toBe(config.adminApiSecret) + }) + test('does not update if secret hasnt changed', async (): Promise => { + const tenant = await Tenant.query(knex).findById(config.operatorTenantId) + assert(tenant) + assert(tenant.apiSecret === config.adminApiSecret) + + const error = await tenantService.updateOperatorApiSecretFromConfig() + + expect(error).toBe(undefined) + + const updated = await Tenant.query(knex).findById(tenant.id) + assert(updated) + expect(updated.updatedAt).toStrictEqual(tenant.updatedAt) + }) + test( + 'throws error if operator tenant not found', + withConfigOverride( + () => config, + { operatorTenantId: crypto.randomUUID() }, + async (): Promise => { + const error = await tenantService.updateOperatorApiSecretFromConfig() + + expect(isTenantError(error)).toBe(true) + expect(error).toEqual(TenantError.TenantNotFound) + } + ) + ) + }) +}) diff --git a/packages/backend/src/tenants/service.ts b/packages/backend/src/tenants/service.ts index 64f97fa27a..4cd56dce85 100644 --- a/packages/backend/src/tenants/service.ts +++ b/packages/backend/src/tenants/service.ts @@ -6,6 +6,8 @@ import { CacheDataStore } from '../middleware/cache/data-stores' import type { AuthServiceClient } from '../auth-service-client/client' import { TenantSettingService } from './settings/service' import { TenantSetting } from './settings/model' +import type { IAppConfig } from '../config/app' +import { TenantError } from './errors' export interface TenantService { get: (id: string, includeDeleted?: boolean) => Promise @@ -13,13 +15,14 @@ export interface TenantService { update: (options: UpdateTenantOptions) => Promise delete: (id: string) => Promise getPage: (pagination?: Pagination, sortOrder?: SortOrder) => Promise + updateOperatorApiSecretFromConfig: () => Promise } - export interface ServiceDependencies extends BaseService { knex: TransactionOrKnex tenantCache: CacheDataStore authServiceClient: AuthServiceClient tenantSettingService: TenantSettingService + config: IAppConfig } export async function createTenantService( @@ -37,7 +40,9 @@ export async function createTenantService( update: (options) => updateTenant(deps, options), delete: (id) => deleteTenant(deps, id), getPage: (pagination, sortOrder) => - getTenantPage(deps, pagination, sortOrder) + getTenantPage(deps, pagination, sortOrder), + updateOperatorApiSecretFromConfig: () => + updateOperatorApiSecretFromConfig(deps) } } @@ -186,3 +191,21 @@ async function deleteTenant( throw err } } + +async function updateOperatorApiSecretFromConfig( + deps: ServiceDependencies +): Promise { + const { adminApiSecret, operatorTenantId } = deps.config + + const tenant = await Tenant.query(deps.knex) + .findById(operatorTenantId) + .whereNull('deletedAt') + + if (!tenant) { + return TenantError.TenantNotFound + } + if (tenant.apiSecret !== adminApiSecret) { + await tenant.$query(deps.knex).patch({ apiSecret: adminApiSecret }) + await deps.tenantCache.set(operatorTenantId, tenant) + } +} diff --git a/packages/backend/src/tenants/settings/service.test.ts b/packages/backend/src/tenants/settings/service.test.ts index a95791544e..92592e3f54 100644 --- a/packages/backend/src/tenants/settings/service.test.ts +++ b/packages/backend/src/tenants/settings/service.test.ts @@ -5,7 +5,6 @@ import { Config } from '../../config/app' import { createTestApp, TestContainer } from '../../tests/app' import nock from 'nock' import { truncateTables } from '../../tests/tableManager' -import { Knex } from 'knex' import { Tenant } from '../model' import { TenantService } from '../service' import { faker } from '@faker-js/faker' @@ -26,7 +25,6 @@ import { import { AuthServiceClient } from '../../auth-service-client/client' describe('TenantSetting Service', (): void => { - let knex: Knex let deps: IocContract let appContainer: TestContainer let tenant: Tenant @@ -40,7 +38,6 @@ describe('TenantSetting Service', (): void => { deps = initIocContainer({ ...Config, dbSchema }) appContainer = await createTestApp(deps) - knex = await deps.use('knex') tenantService = await deps.use('tenantService') tenantSettingService = await deps.use('tenantSettingService') authServiceClient = await deps.use('authServiceClient') @@ -64,7 +61,7 @@ describe('TenantSetting Service', (): void => { }) afterEach(async (): Promise => { - await truncateTables(knex, true, dbSchema) + await truncateTables(deps, { truncateTenants: true }) }) afterAll(async (): Promise => { diff --git a/packages/backend/src/tests/tableManager.ts b/packages/backend/src/tests/tableManager.ts index 9467127684..cf293ef36e 100644 --- a/packages/backend/src/tests/tableManager.ts +++ b/packages/backend/src/tests/tableManager.ts @@ -1,4 +1,6 @@ +import { IocContract } from '@adonisjs/fold' import { Knex } from 'knex' +import { AppServices } from '../app' export async function truncateTable( knex: Knex, @@ -9,10 +11,15 @@ export async function truncateTable( } export async function truncateTables( - knex: Knex, - truncateTenants = false, - dbSchema?: string + deps: IocContract, + options?: { truncateTenants?: boolean } ): Promise { + const knex = await deps.use('knex') + const config = await deps.use('config') + const dbSchema = config.dbSchema ?? 'public' + + const truncateTenants = options?.truncateTenants ?? false + const ignoreTables = [ 'knex_migrations', 'knex_migrations_lock', diff --git a/packages/backend/src/webhook/service.test.ts b/packages/backend/src/webhook/service.test.ts index d2d55f09e3..3a574d8cb4 100644 --- a/packages/backend/src/webhook/service.test.ts +++ b/packages/backend/src/webhook/service.test.ts @@ -74,7 +74,7 @@ describe('Webhook Service', (): void => { afterEach(async (): Promise => { jest.useRealTimers() - await truncateTables(knex) + await truncateTables(deps) }) afterAll(async (): Promise => { diff --git a/packages/frontend/app/lib/validate.server.ts b/packages/frontend/app/lib/validate.server.ts index 8e91af96a2..97e8e4404c 100644 --- a/packages/frontend/app/lib/validate.server.ts +++ b/packages/frontend/app/lib/validate.server.ts @@ -130,20 +130,32 @@ export const updateWalletAddressSchema = z }) .merge(uuidSchema) -export const updateTenantSchema = z +export const updateTenantGeneralSchema = z + .object({ + publicName: z.string().optional(), + email: z.string().email().or(z.literal('')) + }) + .merge(uuidSchema) + +export const updateTenantIpSchema = z + .object({ + idpSecret: z.string().optional(), + idpConsentUrl: z.string().optional() + }) + .merge(uuidSchema) + +export const updateTenantSensitiveSchema = z .object({ apiSecret: z .string() .min(10, { message: 'API Secret should be at least 10 characters long' }) - .max(255, { message: 'Maximum length of API Secret is 255 characters' }), - publicName: z.string().optional(), - email: z.string().email().or(z.literal('')), - idpConsentUrl: z.string().optional(), - idpSecret: z.string().optional() + .max(255, { message: 'Maximum length of API Secret is 255 characters' }) }) .merge(uuidSchema) export const createTenantSchema = z .object({}) - .merge(updateTenantSchema) + .merge(updateTenantGeneralSchema) + .merge(updateTenantIpSchema) + .merge(updateTenantSensitiveSchema) .omit({ id: true }) diff --git a/packages/frontend/app/routes/tenants.$tenantId.tsx b/packages/frontend/app/routes/tenants.$tenantId.tsx index 3cd6a6d38f..b85b80b301 100644 --- a/packages/frontend/app/routes/tenants.$tenantId.tsx +++ b/packages/frontend/app/routes/tenants.$tenantId.tsx @@ -12,6 +12,7 @@ import { useSubmit } from '@remix-run/react' import { type FormEvent, useState, useRef } from 'react' +import type { ZodSchema } from 'zod' import { z } from 'zod' import { DangerZone, PageHeader } from '~/components' import { Button, ErrorPanel, Input, PasswordInput } from '~/components/ui' @@ -21,11 +22,16 @@ import { } from '~/components/ConfirmationDialog' import { updateTenant, deleteTenant, whoAmI } from '~/lib/api/tenant.server' import { messageStorage, setMessageAndRedirect } from '~/lib/message.server' -import type { createTenantSchema } from '~/lib/validate.server' -import { updateTenantSchema, uuidSchema } from '~/lib/validate.server' +import { + updateTenantGeneralSchema, + updateTenantIpSchema, + updateTenantSensitiveSchema, + uuidSchema +} from '~/lib/validate.server' import type { ZodFieldErrors } from '~/shared/types' import { checkAuthAndRedirect } from '../lib/kratos_checks.server' import { getTenantInfo } from '~/lib/api/tenant.server' +import type { UpdateTenantInput } from '~/generated/graphql' export async function loader({ request, params }: LoaderFunctionArgs) { const cookies = request.headers.get('cookie') @@ -87,18 +93,12 @@ export default function ViewTenantPage() { )}

-
-
@@ -143,31 +143,26 @@ export default function ViewTenantPage() {

Identity Provider Information

- +
-
@@ -188,28 +183,42 @@ export default function ViewTenantPage() {
{/* Identity Provider Information - END */} {/* Sensitive Info */} -
-
-

Sensitive Information

- -
-
- -
-
- - -
-
- + {me.isOperator && ( +
+
+

Sensitive Information

+ +
+
+
+
+
+ + +
+
+ {!tenantDeleted && !me.isOperator && ( + + )} +
+
+
+
-
+ )} {/* Sensitive - END */} {/* DELETE TENANT - Danger zone */} {!tenantDeleted && me.isOperator && me.id !== tenant.id && ( @@ -237,12 +246,36 @@ export default function ViewTenantPage() { } export async function action({ request }: ActionFunctionArgs) { - const errors: { - fieldErrors: ZodFieldErrors - message: string[] + const actionResponse: { + errors: { + general: { + fieldErrors: ZodFieldErrors + message: string[] + } + ip: { + fieldErrors: ZodFieldErrors + message: string[] + } + sensitive: { + fieldErrors: ZodFieldErrors + message: string[] + } + } } = { - fieldErrors: {}, - message: [] + errors: { + general: { + fieldErrors: {}, + message: [] + }, + ip: { + fieldErrors: {}, + message: [] + }, + sensitive: { + fieldErrors: {}, + message: [] + } + } } const session = await messageStorage.getSession(request.headers.get('cookie')) @@ -250,30 +283,60 @@ export async function action({ request }: ActionFunctionArgs) { const intent = formData.get('intent') formData.delete('intent') + async function handleUpdateFormSubmit( + errorKey: keyof typeof actionResponse.errors, + schema: ZodSchema + ) { + const formEntries = Object.fromEntries(formData) + const result = schema.safeParse(formEntries) + + if (!result.success) { + actionResponse.errors[errorKey].fieldErrors = + result.error.flatten().fieldErrors + return json(actionResponse, { status: 400 }) + } + + const response = await updateTenant(request, { ...result.data }) + + if (!response?.tenant) { + actionResponse.errors[errorKey].message = [ + 'Could not update tenant. Please try again!' + ] + return json(actionResponse, { status: 400 }) + } + + return { formEntries } + } + switch (intent) { - case 'general': - case 'ip': + case 'general': { + const result = await handleUpdateFormSubmit( + intent, + updateTenantGeneralSchema + ) + if (!('formEntries' in result)) return result + break + } + case 'ip': { + const result = await handleUpdateFormSubmit(intent, updateTenantIpSchema) + if (!('formEntries' in result)) return result + break + } case 'sensitive': { - const formEntries = Object.fromEntries(formData) - const result = updateTenantSchema.safeParse(formEntries) - if (!result.success) { - errors.fieldErrors = result.error.flatten().fieldErrors - return json({ errors }, { status: 400 }) - } + const result = await handleUpdateFormSubmit( + intent, + updateTenantSensitiveSchema + ) - const response = await updateTenant(request, { - ...result.data - }) - - if (!response?.tenant) { - errors.message = ['Could not update tenant. Please try again!'] - return json({ errors }, { status: 400 }) - } + if (!('formEntries' in result)) return result const me = await whoAmI(request) - // We update the apiSecret of the session in case it changed. - if (formEntries.apiSecret && me.id === formEntries.id) { - session.set('apiSecret', formEntries.apiSecret) + if ( + result.formEntries.apiSecret && + me.id === result.formEntries.id && + !me.isOperator + ) { + session.set('apiSecret', result.formEntries.apiSecret) } break } @@ -282,10 +345,7 @@ export async function action({ request }: ActionFunctionArgs) { if (!result.success) { return setMessageAndRedirect({ session, - message: { - content: 'Invalid tenant ID.', - type: 'error' - }, + message: { content: 'Invalid tenant ID.', type: 'error' }, location: '.' }) } @@ -294,20 +354,14 @@ export async function action({ request }: ActionFunctionArgs) { if (!response) { return setMessageAndRedirect({ session, - message: { - content: 'Could not delete Tenant.', - type: 'error' - }, + message: { content: 'Could not delete Tenant.', type: 'error' }, location: '.' }) } return setMessageAndRedirect({ session, - message: { - content: 'Tenant was deleted.', - type: 'success' - }, + message: { content: 'Tenant was deleted.', type: 'success' }, location: '/tenants' }) } @@ -317,10 +371,7 @@ export async function action({ request }: ActionFunctionArgs) { return setMessageAndRedirect({ session, - message: { - content: 'Tenant information was updated', - type: 'success' - }, + message: { content: 'Tenant information was updated', type: 'success' }, location: '.' }) }