From 2859b3be31221d41ecd2b32587d94122f6d26b39 Mon Sep 17 00:00:00 2001 From: golobitch Date: Mon, 2 Sep 2024 23:04:47 +0200 Subject: [PATCH 1/3] feat(backend): add tenant name --- .../generated/graphql.ts | 5 +++ .../20240902220115_add_tenant_name.js | 23 +++++++++++++ .../src/graphql/generated/graphql.schema.json | 32 +++++++++++++++++++ .../backend/src/graphql/generated/graphql.ts | 5 +++ .../backend/src/graphql/resolvers/tenant.ts | 4 ++- .../src/graphql/resolvers/tenant_endpoints.ts | 2 -- packages/backend/src/graphql/schema.graphql | 5 +++ .../backend/src/tenant/endpoints/errors.ts | 2 +- .../backend/src/tenant/endpoints/model.ts | 6 ++++ .../backend/src/tenant/endpoints/service.ts | 7 ++-- packages/backend/src/tenant/errors.ts | 7 ++-- packages/backend/src/tenant/model.ts | 3 +- packages/backend/src/tenant/service.ts | 24 +++++++------- packages/frontend/app/generated/graphql.ts | 5 +++ .../src/generated/graphql.ts | 5 +++ test/integration/lib/generated/graphql.ts | 5 +++ 16 files changed, 116 insertions(+), 24 deletions(-) create mode 100644 packages/backend/migrations/20240902220115_add_tenant_name.js diff --git a/localenv/mock-account-servicing-entity/generated/graphql.ts b/localenv/mock-account-servicing-entity/generated/graphql.ts index d2f1d62ed5..5ee276ab0a 100644 --- a/localenv/mock-account-servicing-entity/generated/graphql.ts +++ b/localenv/mock-account-servicing-entity/generated/graphql.ts @@ -351,6 +351,8 @@ export type CreateTenantInput = { idpConsentEndpoint: Scalars['String']['input']; /** IDP Secret */ idpSecret: Scalars['String']['input']; + /** Public name of the tenant */ + name: Scalars['String']['input']; }; export type CreateTenantMutationResponse = { @@ -1315,6 +1317,8 @@ export type Tenant = Model & { id: Scalars['ID']['output']; /** Kratos identity ID */ kratosIdentityId: Scalars['String']['output']; + /** Name of the tenant */ + name: Scalars['String']['output']; /** Date-time of the update */ updatedAt: Scalars['String']['output']; }; @@ -2352,6 +2356,7 @@ export type TenantResolvers, ParentType, ContextType>; id?: Resolver; kratosIdentityId?: Resolver; + name?: Resolver; updatedAt?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/backend/migrations/20240902220115_add_tenant_name.js b/packages/backend/migrations/20240902220115_add_tenant_name.js new file mode 100644 index 0000000000..23618281bc --- /dev/null +++ b/packages/backend/migrations/20240902220115_add_tenant_name.js @@ -0,0 +1,23 @@ +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.up = function (knex) { + return knex.schema + .table('tenants', function (table) { + table.string('name').unique().notNullable() + }) + } + + /** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ + exports.down = function (knex) { + return knex.schema + .table('tenants', function (table) { + table.dropColumn('name') + }) + + } + \ No newline at end of file diff --git a/packages/backend/src/graphql/generated/graphql.schema.json b/packages/backend/src/graphql/generated/graphql.schema.json index 447f462915..88224d7910 100644 --- a/packages/backend/src/graphql/generated/graphql.schema.json +++ b/packages/backend/src/graphql/generated/graphql.schema.json @@ -2229,6 +2229,22 @@ "defaultValue": null, "isDeprecated": false, "deprecationReason": null + }, + { + "name": "name", + "description": "Public name of the tenant", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null } ], "interfaces": null, @@ -7866,6 +7882,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "name", + "description": "Name of the tenant", + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "updatedAt", "description": "Date-time of the update", diff --git a/packages/backend/src/graphql/generated/graphql.ts b/packages/backend/src/graphql/generated/graphql.ts index d2f1d62ed5..5ee276ab0a 100644 --- a/packages/backend/src/graphql/generated/graphql.ts +++ b/packages/backend/src/graphql/generated/graphql.ts @@ -351,6 +351,8 @@ export type CreateTenantInput = { idpConsentEndpoint: Scalars['String']['input']; /** IDP Secret */ idpSecret: Scalars['String']['input']; + /** Public name of the tenant */ + name: Scalars['String']['input']; }; export type CreateTenantMutationResponse = { @@ -1315,6 +1317,8 @@ export type Tenant = Model & { id: Scalars['ID']['output']; /** Kratos identity ID */ kratosIdentityId: Scalars['String']['output']; + /** Name of the tenant */ + name: Scalars['String']['output']; /** Date-time of the update */ updatedAt: Scalars['String']['output']; }; @@ -2352,6 +2356,7 @@ export type TenantResolvers, ParentType, ContextType>; id?: Resolver; kratosIdentityId?: Resolver; + name?: Resolver; updatedAt?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/backend/src/graphql/resolvers/tenant.ts b/packages/backend/src/graphql/resolvers/tenant.ts index 9d023ceda9..8b883cbdde 100644 --- a/packages/backend/src/graphql/resolvers/tenant.ts +++ b/packages/backend/src/graphql/resolvers/tenant.ts @@ -16,7 +16,7 @@ import { import { Tenant } from '../../tenant/model' import { Pagination, SortOrder } from '../../shared/baseModel' import { getPageInfo } from '../../shared/pagination' -import { EndpointType, TenantEndpoint } from '../../tenant/endpoints/model' +import { EndpointType } from '../../tenant/endpoints/model' import { tenantEndpointToGraphql } from './tenant_endpoints' const mapTenantEndpointTypeToModelEndpointType = { @@ -77,6 +77,7 @@ export const createTenant: MutationResolvers['createTenant'] = const tenantService = await ctx.container.use('tenantService') const tenantOrError = await tenantService.create({ + name: args.input.name, idpConsentEndpoint: args.input.idpConsentEndpoint, idpSecret: args.input.idpSecret, endpoints: args.input.endpoints.map((endpoint) => { @@ -103,6 +104,7 @@ export const createTenant: MutationResolvers['createTenant'] = export function tenantToGraphql(tenant: Tenant): SchemaTenant { return { id: tenant.id, + name: tenant.name, kratosIdentityId: tenant.kratosIdentityId, //we should probably paginate this, but for now, that we only have like two endpoints it should be ok endpoints: tenant.endpoints.map(tenantEndpointToGraphql), diff --git a/packages/backend/src/graphql/resolvers/tenant_endpoints.ts b/packages/backend/src/graphql/resolvers/tenant_endpoints.ts index 14197d73ce..bd18fae1ce 100644 --- a/packages/backend/src/graphql/resolvers/tenant_endpoints.ts +++ b/packages/backend/src/graphql/resolvers/tenant_endpoints.ts @@ -36,8 +36,6 @@ export const getTenantEndpoints: TenantResolvers['endpoints'] = order ) - console.log('TENANT ENDPOINTS: ', tenantEndpoints) - const pageInfo = await getPageInfo({ getPage: (pagination_?: Pagination, sortOrder_?: SortOrder) => tenantEndpointService.getPage(parent.id!, pagination_, sortOrder_), diff --git a/packages/backend/src/graphql/schema.graphql b/packages/backend/src/graphql/schema.graphql index eb90571157..d581ce4b42 100644 --- a/packages/backend/src/graphql/schema.graphql +++ b/packages/backend/src/graphql/schema.graphql @@ -1166,6 +1166,9 @@ input CreateTenantInput { "IDP Secret" idpSecret: String! + + "Public name of the tenant" + name: String! } input CreateWalletAddressInput { @@ -1389,6 +1392,8 @@ type TenantEndpoint { type Tenant implements Model { "Tenant ID that is used in subsequent resources" id: ID! + "Name of the tenant" + name: String! "Kratos identity ID" kratosIdentityId: String! "Date-time of creation" diff --git a/packages/backend/src/tenant/endpoints/errors.ts b/packages/backend/src/tenant/endpoints/errors.ts index 92aab4e4dc..d10ac5489b 100644 --- a/packages/backend/src/tenant/endpoints/errors.ts +++ b/packages/backend/src/tenant/endpoints/errors.ts @@ -5,7 +5,7 @@ export enum TenantEndpointError { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -export const isTenantError = (t: any): t is TenantEndpointError => +export const isTenantEndpointError = (t: any): t is TenantEndpointError => Object.values(TenantEndpointError).includes(t) export const errorToCode: { diff --git a/packages/backend/src/tenant/endpoints/model.ts b/packages/backend/src/tenant/endpoints/model.ts index 53860b770c..b0b16c7406 100644 --- a/packages/backend/src/tenant/endpoints/model.ts +++ b/packages/backend/src/tenant/endpoints/model.ts @@ -10,6 +10,12 @@ export class TenantEndpoint extends WeakModel { return 'tenantEndpoints' } + // Tell Objection.js that there is no single id column + // Define the composite primary key + static get idColumn() { + return ['tenantId', 'type']; + } + public type!: EndpointType public value!: string public tenantId!: string diff --git a/packages/backend/src/tenant/endpoints/service.ts b/packages/backend/src/tenant/endpoints/service.ts index 83c314e761..39ae2e0732 100644 --- a/packages/backend/src/tenant/endpoints/service.ts +++ b/packages/backend/src/tenant/endpoints/service.ts @@ -58,11 +58,9 @@ async function getTenantEndpointsPage( pagination?: Pagination, sortOrder?: SortOrder ) { - console.log('GET TENANT ENDPOINTS PAGE') - const data = await TenantEndpoint.query(deps.knex) - .returning(['type', 'value', 'createdAt', 'updatedAt']) + const data = await TenantEndpoint + .query(deps.knex) .getPage(pagination, sortOrder) - console.log('DATA: ', data) return data } @@ -78,7 +76,6 @@ async function createTenantEndpoint( return await TenantEndpoint.query(createOptions.trx) .insert(tenantEndpointsData) - .returning(['type', 'value', 'createdAt', 'updatedAt']) } async function getEndpointsForTenant( diff --git a/packages/backend/src/tenant/errors.ts b/packages/backend/src/tenant/errors.ts index 8d06abd6e2..2ad95af496 100644 --- a/packages/backend/src/tenant/errors.ts +++ b/packages/backend/src/tenant/errors.ts @@ -2,6 +2,7 @@ import { GraphQLErrorCode } from '../graphql/errors' export enum TenantError { UnknownTenant = 'UnknownTenant', + UnableToCreateEndpoint = 'UnableToCreateEndpoint', UnknownError = 'UnknownError' } @@ -13,12 +14,14 @@ export const errorToCode: { [key in TenantError]: GraphQLErrorCode } = { [TenantError.UnknownTenant]: GraphQLErrorCode.NotFound, - [TenantError.UnknownError]: GraphQLErrorCode.InternalServerError + [TenantError.UnknownError]: GraphQLErrorCode.InternalServerError, + [TenantError.UnableToCreateEndpoint]: GraphQLErrorCode.BadUserInput } export const errorToMessage: { [key in TenantError]: string } = { [TenantError.UnknownError]: 'Unknown error', - [TenantError.UnknownTenant]: 'Unknown tenant' + [TenantError.UnknownTenant]: 'Unknown tenant', + [TenantError.UnableToCreateEndpoint]: 'Unable to create endpoint' } diff --git a/packages/backend/src/tenant/model.ts b/packages/backend/src/tenant/model.ts index 6f86ec70f9..7676d6a69e 100644 --- a/packages/backend/src/tenant/model.ts +++ b/packages/backend/src/tenant/model.ts @@ -1,5 +1,5 @@ import { Model } from 'objection' -import { BaseModel, WeakModel } from '../shared/baseModel' +import { BaseModel } from '../shared/baseModel' import { TenantEndpoint } from './endpoints/model' export class Tenant extends BaseModel { @@ -20,6 +20,7 @@ export class Tenant extends BaseModel { } } + public name!: string public kratosIdentityId!: string public deletedAt?: Date public endpoints!: TenantEndpoint[] diff --git a/packages/backend/src/tenant/service.ts b/packages/backend/src/tenant/service.ts index 24e9f1bfb8..691326bd0c 100644 --- a/packages/backend/src/tenant/service.ts +++ b/packages/backend/src/tenant/service.ts @@ -11,11 +11,13 @@ import { import { v4 as uuidv4 } from 'uuid' import { Pagination, SortOrder } from '../shared/baseModel' import { EndpointOptions, TenantEndpointService } from './endpoints/service' -import { TenantEndpoint } from './endpoints/model' +import { isTenantEndpointError } from './endpoints/errors' +import { tr } from '@faker-js/faker' export interface CreateTenantOptions { - idpConsentEndpoint: string + name: string idpSecret: string + idpConsentEndpoint: string endpoints: EndpointOptions[] } @@ -85,16 +87,14 @@ async function createTenant( return deps.knex.transaction(async (trx) => { let tenant: Tenant try { - // create tenant on backend - tenant = await Tenant.query(trx).insert({ - kratosIdentityId: uuidv4() - }) - - await deps.tenantEndpointService.create({ - endpoints: options.endpoints, - tenantId: tenant.id, - trx - }) + const tenantData = { + name: options.name, + kratosIdentityId: uuidv4(), + endpoints: options.endpoints + } + + tenant = await Tenant.query(trx) + .insertGraphAndFetch(tenantData) // call auth admin api const mutation = gql` diff --git a/packages/frontend/app/generated/graphql.ts b/packages/frontend/app/generated/graphql.ts index eaec30b692..5b4f83da8a 100644 --- a/packages/frontend/app/generated/graphql.ts +++ b/packages/frontend/app/generated/graphql.ts @@ -351,6 +351,8 @@ export type CreateTenantInput = { idpConsentEndpoint: Scalars['String']['input']; /** IDP Secret */ idpSecret: Scalars['String']['input']; + /** Public name of the tenant */ + name: Scalars['String']['input']; }; export type CreateTenantMutationResponse = { @@ -1315,6 +1317,8 @@ export type Tenant = Model & { id: Scalars['ID']['output']; /** Kratos identity ID */ kratosIdentityId: Scalars['String']['output']; + /** Name of the tenant */ + name: Scalars['String']['output']; /** Date-time of the update */ updatedAt: Scalars['String']['output']; }; @@ -2352,6 +2356,7 @@ export type TenantResolvers, ParentType, ContextType>; id?: Resolver; kratosIdentityId?: Resolver; + name?: Resolver; updatedAt?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/packages/mock-account-service-lib/src/generated/graphql.ts b/packages/mock-account-service-lib/src/generated/graphql.ts index d2f1d62ed5..5ee276ab0a 100644 --- a/packages/mock-account-service-lib/src/generated/graphql.ts +++ b/packages/mock-account-service-lib/src/generated/graphql.ts @@ -351,6 +351,8 @@ export type CreateTenantInput = { idpConsentEndpoint: Scalars['String']['input']; /** IDP Secret */ idpSecret: Scalars['String']['input']; + /** Public name of the tenant */ + name: Scalars['String']['input']; }; export type CreateTenantMutationResponse = { @@ -1315,6 +1317,8 @@ export type Tenant = Model & { id: Scalars['ID']['output']; /** Kratos identity ID */ kratosIdentityId: Scalars['String']['output']; + /** Name of the tenant */ + name: Scalars['String']['output']; /** Date-time of the update */ updatedAt: Scalars['String']['output']; }; @@ -2352,6 +2356,7 @@ export type TenantResolvers, ParentType, ContextType>; id?: Resolver; kratosIdentityId?: Resolver; + name?: Resolver; updatedAt?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; diff --git a/test/integration/lib/generated/graphql.ts b/test/integration/lib/generated/graphql.ts index d2f1d62ed5..5ee276ab0a 100644 --- a/test/integration/lib/generated/graphql.ts +++ b/test/integration/lib/generated/graphql.ts @@ -351,6 +351,8 @@ export type CreateTenantInput = { idpConsentEndpoint: Scalars['String']['input']; /** IDP Secret */ idpSecret: Scalars['String']['input']; + /** Public name of the tenant */ + name: Scalars['String']['input']; }; export type CreateTenantMutationResponse = { @@ -1315,6 +1317,8 @@ export type Tenant = Model & { id: Scalars['ID']['output']; /** Kratos identity ID */ kratosIdentityId: Scalars['String']['output']; + /** Name of the tenant */ + name: Scalars['String']['output']; /** Date-time of the update */ updatedAt: Scalars['String']['output']; }; @@ -2352,6 +2356,7 @@ export type TenantResolvers, ParentType, ContextType>; id?: Resolver; kratosIdentityId?: Resolver; + name?: Resolver; updatedAt?: Resolver; __isTypeOf?: IsTypeOfResolverFn; }; From d9dda535b158cd20ee7fdbe7a14a6c5ac7cf5249 Mon Sep 17 00:00:00 2001 From: golobitch Date: Tue, 3 Sep 2024 22:19:32 +0200 Subject: [PATCH 2/3] feat(backend): formating + add basic tenant query resolvers test --- packages/backend/jest.config.js | 2 + .../20240902220115_add_tenant_name.js | 32 +++-- .../backend/src/graphql/resolvers/index.ts | 1 - .../src/graphql/resolvers/tenant.test.ts | 115 ++++++++++++++++++ .../backend/src/tenant/endpoints/model.ts | 2 +- .../backend/src/tenant/endpoints/service.ts | 14 ++- packages/backend/src/tenant/service.ts | 12 +- packages/backend/src/tests/app.ts | 13 +- packages/backend/src/tests/tenant.ts | 46 +++++++ 9 files changed, 197 insertions(+), 40 deletions(-) create mode 100644 packages/backend/src/graphql/resolvers/tenant.test.ts create mode 100644 packages/backend/src/tests/tenant.ts diff --git a/packages/backend/jest.config.js b/packages/backend/jest.config.js index e6527265b5..61d32249a3 100644 --- a/packages/backend/jest.config.js +++ b/packages/backend/jest.config.js @@ -18,6 +18,8 @@ process.env.USE_TIGERBEETLE = false process.env.ENABLE_TELEMETRY = false process.env.KRATOS_ADMIN_URL = 'http://127.0.0.1:4434/admin' process.env.KRATOS_ADMIN_EMAIL = 'admin@mail.com' +process.env.AUTH_ADMIN_URL = 'http://example.com/graphql' +process.env.AUTH_ADMIN_API_SECRET = 'verysecuresecret' module.exports = { ...baseConfig, diff --git a/packages/backend/migrations/20240902220115_add_tenant_name.js b/packages/backend/migrations/20240902220115_add_tenant_name.js index 23618281bc..dd9ac5c59c 100644 --- a/packages/backend/migrations/20240902220115_add_tenant_name.js +++ b/packages/backend/migrations/20240902220115_add_tenant_name.js @@ -3,21 +3,17 @@ * @returns { Promise } */ exports.up = function (knex) { - return knex.schema - .table('tenants', function (table) { - table.string('name').unique().notNullable() - }) - } - - /** - * @param { import("knex").Knex } knex - * @returns { Promise } - */ - exports.down = function (knex) { - return knex.schema - .table('tenants', function (table) { - table.dropColumn('name') - }) - - } - \ No newline at end of file + return knex.schema.table('tenants', function (table) { + table.string('name').unique().notNullable() + }) +} + +/** + * @param { import("knex").Knex } knex + * @returns { Promise } + */ +exports.down = function (knex) { + return knex.schema.table('tenants', function (table) { + table.dropColumn('name') + }) +} diff --git a/packages/backend/src/graphql/resolvers/index.ts b/packages/backend/src/graphql/resolvers/index.ts index e6f05f04ae..fa27b2224b 100644 --- a/packages/backend/src/graphql/resolvers/index.ts +++ b/packages/backend/src/graphql/resolvers/index.ts @@ -68,7 +68,6 @@ import { getCombinedPayments } from './combined_payments' import { createOrUpdatePeerByUrl } from './auto-peering' import { getAccountingTransfers } from './accounting_transfer' import { createTenant, getTenant, getTenants } from './tenant' -import { getTenantEndpoints } from './tenant_endpoints' export const resolvers: Resolvers = { UInt8: GraphQLUInt8, diff --git a/packages/backend/src/graphql/resolvers/tenant.test.ts b/packages/backend/src/graphql/resolvers/tenant.test.ts new file mode 100644 index 0000000000..671053df90 --- /dev/null +++ b/packages/backend/src/graphql/resolvers/tenant.test.ts @@ -0,0 +1,115 @@ +import { Knex } from 'knex' +import { IocContract } from '@adonisjs/fold' +import { createTestApp, TestContainer } from '../../tests/app' +import { AppServices } from '../../app' +import { initIocContainer } from '../..' +import { Config, IAppConfig } from '../../config/app' +import { truncateTables } from '../../tests/tableManager' +import { getPageTests } from './page.test' +import { + createTenant, + mockAdminAuthApiTenantCreation +} from '../../tests/tenant' +import { ApolloError, gql } from '@apollo/client' +import { Scope } from 'nock' +import { v4 as uuidv4 } from 'uuid' +import { errorToCode, errorToMessage, TenantError } from '../../tenant/errors' + +describe('Tenant Resolver', (): void => { + let deps: IocContract + let appContainer: TestContainer + let knex: Knex + let config: IAppConfig + let scope: Scope + + beforeAll(async (): Promise => { + deps = initIocContainer(Config) + appContainer = await createTestApp(deps) + knex = appContainer.knex + config = await deps.use('config') + scope = mockAdminAuthApiTenantCreation(config.authAdminApiUrl).persist() + }) + + afterEach(async (): Promise => { + await truncateTables(knex) + }) + + afterAll(async (): Promise => { + scope.done() + appContainer.apolloClient.stop() + await appContainer.shutdown() + }) + + describe('Tenant Queries', (): void => { + getPageTests({ + getClient: () => appContainer.apolloClient, + createModel: async () => createTenant(deps), + pagedQuery: 'tenants' + }) + + test('should return error if tenant does not exists', async (): Promise => { + try { + await appContainer.apolloClient + .query({ + query: gql` + query GetTenant($id: ID!) { + tenant(id: $id) { + id + name + kratosIdentityId + } + } + `, + variables: { + id: uuidv4() + } + }) + .then((query) => { + if (query.data) return query.data.tenant + throw new Error('Data was empty') + }) + } catch (error) { + expect(error).toBeInstanceOf(ApolloError) + expect((error as ApolloError).graphQLErrors).toContainEqual( + expect.objectContaining({ + message: errorToMessage[TenantError.UnknownTenant], + extensions: expect.objectContaining({ + code: errorToCode[TenantError.UnknownTenant] + }) + }) + ) + } + }) + + test('should get correct tenant', async (): Promise => { + const tenant = await createTenant(deps) + + const response = await appContainer.apolloClient + .query({ + query: gql` + query GetTenant($id: ID!) { + tenant(id: $id) { + id + name + kratosIdentityId + } + } + `, + variables: { + id: tenant.id + } + }) + .then((query) => { + if (query.data) return query.data.tenant + throw new Error('Data was empty') + }) + + expect(response).toEqual({ + __typename: 'Tenant', + id: tenant.id, + name: tenant.name, + kratosIdentityId: tenant.kratosIdentityId + }) + }) + }) +}) diff --git a/packages/backend/src/tenant/endpoints/model.ts b/packages/backend/src/tenant/endpoints/model.ts index b0b16c7406..50e3b568ac 100644 --- a/packages/backend/src/tenant/endpoints/model.ts +++ b/packages/backend/src/tenant/endpoints/model.ts @@ -13,7 +13,7 @@ export class TenantEndpoint extends WeakModel { // Tell Objection.js that there is no single id column // Define the composite primary key static get idColumn() { - return ['tenantId', 'type']; + return ['tenantId', 'type'] } public type!: EndpointType diff --git a/packages/backend/src/tenant/endpoints/service.ts b/packages/backend/src/tenant/endpoints/service.ts index 39ae2e0732..58e02ad4f0 100644 --- a/packages/backend/src/tenant/endpoints/service.ts +++ b/packages/backend/src/tenant/endpoints/service.ts @@ -58,10 +58,11 @@ async function getTenantEndpointsPage( pagination?: Pagination, sortOrder?: SortOrder ) { - const data = await TenantEndpoint - .query(deps.knex) - .getPage(pagination, sortOrder) - return data + const data = await TenantEndpoint.query(deps.knex).getPage( + pagination, + sortOrder + ) + return data } async function createTenantEndpoint( @@ -74,8 +75,9 @@ async function createTenantEndpoint( tenantId: createOptions.tenantId })) - return await TenantEndpoint.query(createOptions.trx) - .insert(tenantEndpointsData) + return await TenantEndpoint.query(createOptions.trx).insert( + tenantEndpointsData + ) } async function getEndpointsForTenant( diff --git a/packages/backend/src/tenant/service.ts b/packages/backend/src/tenant/service.ts index 691326bd0c..7827329d25 100644 --- a/packages/backend/src/tenant/service.ts +++ b/packages/backend/src/tenant/service.ts @@ -11,8 +11,6 @@ import { import { v4 as uuidv4 } from 'uuid' import { Pagination, SortOrder } from '../shared/baseModel' import { EndpointOptions, TenantEndpointService } from './endpoints/service' -import { isTenantEndpointError } from './endpoints/errors' -import { tr } from '@faker-js/faker' export interface CreateTenantOptions { name: string @@ -61,6 +59,7 @@ async function getTenantsPage( sortOrder?: SortOrder ): Promise { return await Tenant.query(deps.knex) + .withGraphFetched('endpoints') .getPage(pagination, sortOrder) } @@ -68,9 +67,7 @@ async function getTenant( deps: ServiceDependencies, id: string ): Promise { - return Tenant.query(deps.knex) - .withGraphFetched('endpoints') - .findById(id) + return Tenant.query(deps.knex).withGraphFetched('endpoints').findById(id) } async function createTenant( @@ -92,9 +89,8 @@ async function createTenant( kratosIdentityId: uuidv4(), endpoints: options.endpoints } - - tenant = await Tenant.query(trx) - .insertGraphAndFetch(tenantData) + + tenant = await Tenant.query(trx).insertGraphAndFetch(tenantData) // call auth admin api const mutation = gql` diff --git a/packages/backend/src/tests/app.ts b/packages/backend/src/tests/app.ts index cbe82b4704..4acc3dc5ed 100644 --- a/packages/backend/src/tests/app.ts +++ b/packages/backend/src/tests/app.ts @@ -1,19 +1,18 @@ import Axios from 'axios' import { Knex } from 'knex' -import fetch from 'cross-fetch' import { IocContract } from '@adonisjs/fold' import { ApolloClient, ApolloLink, + createHttpLink, InMemoryCache, - NormalizedCacheObject, - createHttpLink + NormalizedCacheObject } from '@apollo/client' -import { setContext } from '@apollo/client/link/context' import { start, gracefulShutdown } from '..' -import { onError } from '@apollo/client/link/error' import { App, AppServices } from '../app' +import { setContext } from '@apollo/client/link/context' +import { onError } from '@apollo/client/link/error' export const testAccessToken = 'test-app-access' @@ -23,6 +22,7 @@ export interface TestContainer { app: App knex: Knex apolloClient: ApolloClient + authApolloClient: ApolloClient connectionUrl: string shutdown: () => Promise container: IocContract @@ -38,7 +38,6 @@ export const createTestApp = async ( config.autoPeeringServerPort = 0 config.openPaymentsUrl = 'https://op.example' config.walletAddressUrl = 'https://wallet.example/.well-known/pay' - const logger = await container.use('logger') const app = new App(container) await start(container, app) @@ -57,6 +56,7 @@ export const createTestApp = async ( .persist() const knex = await container.use('knex') + const logger = await container.use('logger') const httpLink = createHttpLink({ uri: `http://localhost:${app.getAdminPort()}/graphql`, @@ -108,6 +108,7 @@ export const createTestApp = async ( openPaymentsPort: app.getOpenPaymentsPort(), knex, apolloClient: client, + authApolloClient: await container.use('apolloClient'), connectionUrl: config.databaseUrl, shutdown: async () => { nock.cleanAll() diff --git a/packages/backend/src/tests/tenant.ts b/packages/backend/src/tests/tenant.ts new file mode 100644 index 0000000000..bb90953f09 --- /dev/null +++ b/packages/backend/src/tests/tenant.ts @@ -0,0 +1,46 @@ +import { IocContract } from '@adonisjs/fold' +import { AppServices } from '../app' +import { Tenant } from '../tenant/model' +import { CreateTenantOptions } from '../tenant/service' +import { v4 as uuidv4 } from 'uuid' +import { EndpointType } from '../tenant/endpoints/model' +import nock from 'nock' + +export function mockAdminAuthApiTenantCreation(mockedUrl: string) { + const url = new URL(mockedUrl) + return nock(`${url.protocol}//${url.host}`) + .post(/.*/) + .reply(200, { + data: { + createTenant: { + tenant: { + id: uuidv4() + } + } + } + }) +} + +export async function createTenant( + deps: IocContract +): Promise { + const tenantService = await deps.use('tenantService') + + const options: CreateTenantOptions = { + name: uuidv4(), + idpConsentEndpoint: `https://example.com/${uuidv4()}`, + idpSecret: `secret-${uuidv4()}`, + endpoints: [ + { + type: EndpointType.RatesUrl, + value: `https://example.com/rates/${uuidv4()}` + }, + { + type: EndpointType.WebhookBaseUrl, + value: `https://example.com/webhook/${uuidv4()}` + } + ] + } + + return tenantService.create(options) as Promise +} From 1fd2431277e9ee9cddc743f4db79c3124c13742d Mon Sep 17 00:00:00 2001 From: golobitch Date: Wed, 4 Sep 2024 23:31:57 +0200 Subject: [PATCH 3/3] test(tenant): add more tests for resolver and service --- .../src/graphql/resolvers/tenant.test.ts | 50 +++++++++++++++ packages/backend/src/tenant/service.test.ts | 63 +++++++++++++++++++ packages/backend/src/tests/tenant.ts | 39 ++++++------ 3 files changed, 133 insertions(+), 19 deletions(-) diff --git a/packages/backend/src/graphql/resolvers/tenant.test.ts b/packages/backend/src/graphql/resolvers/tenant.test.ts index 671053df90..fd57d81b4b 100644 --- a/packages/backend/src/graphql/resolvers/tenant.test.ts +++ b/packages/backend/src/graphql/resolvers/tenant.test.ts @@ -14,6 +14,7 @@ import { ApolloError, gql } from '@apollo/client' import { Scope } from 'nock' import { v4 as uuidv4 } from 'uuid' import { errorToCode, errorToMessage, TenantError } from '../../tenant/errors' +import { TenantEndpointType } from '../generated/graphql' describe('Tenant Resolver', (): void => { let deps: IocContract @@ -112,4 +113,53 @@ describe('Tenant Resolver', (): void => { }) }) }) + + describe('Create Tenant', (): void => { + it('should create new tenant', async (): Promise => { + const mutation = gql` + mutation CreateTenant($input: CreateTenantInput!) { + createTenant(input: $input) { + tenant { + id + name + } + } + } + ` + + const variables = { + input: { + name: 'My Tenant', + idpConsentEndpoint: 'https://example.com/consent', + idpSecret: 'myVerySecureSecret', + endpoints: [ + { + type: TenantEndpointType.RatesUrl, + value: 'https://example.com/rates' + }, + { + type: TenantEndpointType.WebhookBaseUrl, + value: 'https://example.com/webhook' + } + ] + } + } + + const response = await appContainer.apolloClient + .mutate({ + mutation, + variables + }) + .then((query) => { + if (query.data) return query.data.createTenant + throw new Error('Data was empty') + }) + + expect(response.tenant).toEqual({ + __typename: 'Tenant', + id: response.tenant.id, + name: variables.input.name + }) + }) + }) }) diff --git a/packages/backend/src/tenant/service.test.ts b/packages/backend/src/tenant/service.test.ts index e69de29bb2..cb9a5207b8 100644 --- a/packages/backend/src/tenant/service.test.ts +++ b/packages/backend/src/tenant/service.test.ts @@ -0,0 +1,63 @@ +import { IocContract } from '@adonisjs/fold' +import { AppServices } from '../app' +import { createTestApp, TestContainer } from '../tests/app' +import { TenantService } from './service' +import { initIocContainer } from '..' +import { Config, IAppConfig } from '../config/app' +import { truncateTables } from '../tests/tableManager' +import { + createTenant, + mockAdminAuthApiTenantCreation, + randomTenant +} from '../tests/tenant' +import assert from 'assert' +import { isTenantError } from './errors' +import { Scope } from 'nock' +import { v4 as uuidv4 } from 'uuid' +import { Pagination, SortOrder } from '../shared/baseModel' +import { getPageTests } from '../shared/baseModel.test' + +describe('Tenant Service', (): void => { + let deps: IocContract + let appContainer: TestContainer + let tenantService: TenantService + let scope: Scope + let config: IAppConfig + + beforeAll(async (): Promise => { + deps = initIocContainer(Config) + appContainer = await createTestApp(deps) + tenantService = await deps.use('tenantService') + config = await deps.use('config') + scope = mockAdminAuthApiTenantCreation(config.authAdminApiUrl).persist() + }) + + afterEach(async (): Promise => { + await truncateTables(appContainer.knex) + }) + + afterAll(async (): Promise => { + scope.done() + await appContainer.shutdown() + }) + + describe('get', (): void => { + it('can get tenant by id', async (): Promise => { + const tenant = await tenantService.create(randomTenant()) + assert.ok(!isTenantError(tenant)) + await expect(tenantService.get(tenant.id)).resolves.toEqual(tenant) + }) + + it('cannot get unknown tenant', async (): Promise => { + await expect(tenantService.get(uuidv4())).resolves.toBeUndefined() + }) + }) + + describe('getPage', (): void => { + getPageTests({ + createModel: () => createTenant(deps), + getPage: (pagination?: Pagination, sortOrder?: SortOrder) => + tenantService.getPage(pagination, sortOrder) + }) + }) +}) diff --git a/packages/backend/src/tests/tenant.ts b/packages/backend/src/tests/tenant.ts index bb90953f09..c9de071cea 100644 --- a/packages/backend/src/tests/tenant.ts +++ b/packages/backend/src/tests/tenant.ts @@ -1,11 +1,29 @@ import { IocContract } from '@adonisjs/fold' import { AppServices } from '../app' import { Tenant } from '../tenant/model' -import { CreateTenantOptions } from '../tenant/service' import { v4 as uuidv4 } from 'uuid' import { EndpointType } from '../tenant/endpoints/model' +import { faker } from '@faker-js/faker' import nock from 'nock' +export function randomTenant() { + return { + name: faker.company.name(), + idpConsentEndpoint: faker.internet.url(), + idpSecret: faker.string.sample(10), + endpoints: [ + { + type: EndpointType.RatesUrl, + value: faker.internet.url() + }, + { + type: EndpointType.WebhookBaseUrl, + value: faker.internet.url() + } + ] + } +} + export function mockAdminAuthApiTenantCreation(mockedUrl: string) { const url = new URL(mockedUrl) return nock(`${url.protocol}//${url.host}`) @@ -25,22 +43,5 @@ export async function createTenant( deps: IocContract ): Promise { const tenantService = await deps.use('tenantService') - - const options: CreateTenantOptions = { - name: uuidv4(), - idpConsentEndpoint: `https://example.com/${uuidv4()}`, - idpSecret: `secret-${uuidv4()}`, - endpoints: [ - { - type: EndpointType.RatesUrl, - value: `https://example.com/rates/${uuidv4()}` - }, - { - type: EndpointType.WebhookBaseUrl, - value: `https://example.com/webhook/${uuidv4()}` - } - ] - } - - return tenantService.create(options) as Promise + return tenantService.create(randomTenant()) as Promise }