Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(backend): add tenant name #2937

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions localenv/mock-account-servicing-entity/generated/graphql.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions packages/backend/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '[email protected]'
process.env.AUTH_ADMIN_URL = 'http://example.com/graphql'
process.env.AUTH_ADMIN_API_SECRET = 'verysecuresecret'

module.exports = {
...baseConfig,
Expand Down
19 changes: 19 additions & 0 deletions packages/backend/migrations/20240902220115_add_tenant_name.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema.table('tenants', function (table) {
table.string('name').unique().notNullable()
})
}

/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.table('tenants', function (table) {
table.dropColumn('name')
})
}
32 changes: 32 additions & 0 deletions packages/backend/src/graphql/generated/graphql.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions packages/backend/src/graphql/generated/graphql.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion packages/backend/src/graphql/resolvers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
165 changes: 165 additions & 0 deletions packages/backend/src/graphql/resolvers/tenant.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
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'
import { TenantEndpointType } from '../generated/graphql'

describe('Tenant Resolver', (): void => {
let deps: IocContract<AppServices>
let appContainer: TestContainer
let knex: Knex
let config: IAppConfig
let scope: Scope

beforeAll(async (): Promise<void> => {
deps = initIocContainer(Config)
appContainer = await createTestApp(deps)
knex = appContainer.knex
config = await deps.use('config')
scope = mockAdminAuthApiTenantCreation(config.authAdminApiUrl).persist()
})

afterEach(async (): Promise<void> => {
await truncateTables(knex)
})

afterAll(async (): Promise<void> => {
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<void> => {
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<void> => {
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
})
})
})

describe('Create Tenant', (): void => {
it('should create new tenant', async (): Promise<void> => {
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
})
})
})
})
4 changes: 3 additions & 1 deletion packages/backend/src/graphql/resolvers/tenant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -77,6 +77,7 @@ export const createTenant: MutationResolvers<ApolloContext>['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) => {
Expand All @@ -103,6 +104,7 @@ export const createTenant: MutationResolvers<ApolloContext>['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),
Expand Down
2 changes: 0 additions & 2 deletions packages/backend/src/graphql/resolvers/tenant_endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,6 @@ export const getTenantEndpoints: TenantResolvers<ApolloContext>['endpoints'] =
order
)

console.log('TENANT ENDPOINTS: ', tenantEndpoints)

const pageInfo = await getPageInfo({
getPage: (pagination_?: Pagination, sortOrder_?: SortOrder) =>
tenantEndpointService.getPage(parent.id!, pagination_, sortOrder_),
Expand Down
5 changes: 5 additions & 0 deletions packages/backend/src/graphql/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -1166,6 +1166,9 @@ input CreateTenantInput {

"IDP Secret"
idpSecret: String!

"Public name of the tenant"
name: String!
}

input CreateWalletAddressInput {
Expand Down Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion packages/backend/src/tenant/endpoints/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
6 changes: 6 additions & 0 deletions packages/backend/src/tenant/endpoints/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading