Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions packages/auth/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import { Redis } from 'ioredis'
import { LoggingPlugin } from './graphql/plugin'
import { gnapServerErrorMiddleware } from './shared/gnapErrors'
import { verifyApiSignature } from './shared/utils'
import { TenantService } from './tenant/service'

export interface AppContextData extends DefaultContext {
logger: Logger
Expand Down Expand Up @@ -102,6 +103,7 @@ export interface AppServices {
grantRoutes: Promise<GrantRoutes>
interactionRoutes: Promise<InteractionRoutes>
redis: Promise<Redis>
tenantService: Promise<TenantService>
}

export type AppContainer = IocContract<AppServices>
Expand Down
11 changes: 11 additions & 0 deletions packages/auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import { createInteractionService } from './interaction/service'
import { getTokenIntrospectionOpenAPI } from 'token-introspection'
import { Redis } from 'ioredis'
import { createTenantService } from './tenant/service'

const container = initIocContainer(Config)
const app = new App(container)
Expand Down Expand Up @@ -209,6 +210,16 @@ export function initIocContainer(
return new Redis(config.redisUrl, { tls: config.redisTls })
})

container.singleton(
'tenantService',
async (deps: IocContract<AppServices>) => {
return createTenantService({
logger: await deps.use('logger'),
knex: await deps.use('knex')
})
}
)

return container
}

Expand Down
139 changes: 139 additions & 0 deletions packages/auth/src/tenant/service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { faker } from '@faker-js/faker'
import { Knex } from 'knex'

Check warning on line 2 in packages/auth/src/tenant/service.test.ts

View workflow job for this annotation

GitHub Actions / prerequisite

'Knex' is defined but never used
import { createTestApp, TestContainer } from '../tests/app'
import { truncateTables } from '../tests/tableManager'
import { Config } from '../config/app'
import { IocContract } from '@adonisjs/fold'
import { initIocContainer } from '../'
import { AppServices } from '../app'
import { TenantService } from './service'
import { Tenant } from './service'

Check warning on line 10 in packages/auth/src/tenant/service.test.ts

View workflow job for this annotation

GitHub Actions / prerequisite

'Tenant' is defined but never used

describe('Tenant Service', (): void => {
let deps: IocContract<AppServices>
let appContainer: TestContainer
let tenantService: TenantService

beforeAll(async (): Promise<void> => {
deps = initIocContainer(Config)
appContainer = await createTestApp(deps)

tenantService = await deps.use('tenantService')

// TODO: remove. temporary implementation while other issue is completed
const knex = await deps.use('knex')
await knex.schema.createTable('tenants', (table) => {
table.string('id').primary()
table.string('idpConsentUrl').notNullable()
table.string('idpSecret').notNullable()
})
})

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

afterAll(async (): Promise<void> => {
await appContainer.shutdown()
})

const createTenantData = () => ({
id: faker.string.uuid(),
idpConsentUrl: faker.internet.url(),
idpSecret: faker.string.alphanumeric(32)
})

describe('create', (): void => {
test('creates a tenant', async (): Promise<void> => {
const tenantData = createTenantData()
const tenant = await tenantService.create(tenantData)

expect(tenant).toMatchObject({
id: tenantData.id,
idpConsentUrl: tenantData.idpConsentUrl,
idpSecret: tenantData.idpSecret
})
})

test('fails to create tenant with duplicate id', async (): Promise<void> => {
const tenantData = createTenantData()
await tenantService.create(tenantData)

await expect(tenantService.create(tenantData)).rejects.toThrow()
})
})

describe('get', (): void => {
test('retrieves an existing tenant', async (): Promise<void> => {
const tenantData = createTenantData()
const created = await tenantService.create(tenantData)

const tenant = await tenantService.get(created.id)
expect(tenant).toMatchObject(tenantData)
})

test('returns undefined for non-existent tenant', async (): Promise<void> => {
const tenant = await tenantService.get(faker.string.uuid())
expect(tenant).toBeUndefined()
})
})

describe('update', (): void => {
test('updates an existing tenant', async (): Promise<void> => {
const tenantData = createTenantData()
const created = await tenantService.create(tenantData)

const updateData = {
idpConsentUrl: faker.internet.url(),
idpSecret: faker.string.alphanumeric(32)
}

const updated = await tenantService.update(created.id, updateData)
expect(updated).toMatchObject({
id: created.id,
...updateData
})
})

test('returns undefined for non-existent tenant', async (): Promise<void> => {
const updated = await tenantService.update(faker.string.uuid(), {
idpConsentUrl: faker.internet.url()
})
expect(updated).toBeUndefined()
})

test('can update partial fields', async (): Promise<void> => {
const tenantData = createTenantData()
const created = await tenantService.create(tenantData)

const updateData = {
idpConsentUrl: faker.internet.url()
}

const updated = await tenantService.update(created.id, updateData)
expect(updated).toMatchObject({
id: created.id,
idpConsentUrl: updateData.idpConsentUrl,
idpSecret: created.idpSecret
})
})
})

describe('delete', (): void => {
test('deletes an existing tenant', async (): Promise<void> => {
const tenantData = createTenantData()
const created = await tenantService.create(tenantData)

const result = await tenantService.delete(created.id)
expect(result).toBe(true)

const tenant = await tenantService.get(created.id)
expect(tenant).toBeUndefined()
})

test('returns false for non-existent tenant', async (): Promise<void> => {
const result = await tenantService.delete(faker.string.uuid())
expect(result).toBe(false)
})
})
})
82 changes: 82 additions & 0 deletions packages/auth/src/tenant/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { BaseService } from '../shared/baseService'
import { TransactionOrKnex, Model } from 'objection'

// TODO: remove. temporary implementation while other issue is completed
export class Tenant extends Model {
static tableName = 'tenants'

id!: string
idpConsentUrl!: string
idpSecret!: string
}

export interface CreateOptions {
id: string
idpConsentUrl: string
idpSecret: string
}

export interface TenantService {
create(input: CreateOptions): Promise<Tenant>
get(id: string): Promise<Tenant | undefined>
update(
id: string,
input: Partial<Omit<CreateOptions, 'id'>>
): Promise<Tenant | undefined>
delete(id: string): Promise<boolean>
}

interface ServiceDependencies extends BaseService {
knex: TransactionOrKnex
}

export async function createTenantService({
logger,
knex
}: ServiceDependencies): Promise<TenantService> {
const log = logger.child({
service: 'TenantService'
})
const deps: ServiceDependencies = {
logger: log,
knex
}

return {
create: (input: CreateOptions) => createTenant(deps, input),
get: (id: string) => getTenant(deps, id),
update: (id: string, input: Partial<Omit<CreateOptions, 'id'>>) =>
updateTenant(deps, id, input),
delete: (id: string) => deleteTenant(deps, id)
}
}

async function createTenant(
deps: ServiceDependencies,
input: CreateOptions
): Promise<Tenant> {
return await Tenant.query(deps.knex).insert(input)
}

async function getTenant(
deps: ServiceDependencies,
id: string
): Promise<Tenant | undefined> {
return await Tenant.query(deps.knex).findById(id)
}

async function updateTenant(
deps: ServiceDependencies,
id: string,
input: Partial<Omit<CreateOptions, 'id'>>
): Promise<Tenant | undefined> {
return await Tenant.query(deps.knex).patchAndFetchById(id, input)
}

async function deleteTenant(
deps: ServiceDependencies,
id: string
): Promise<boolean> {
const deleted = await Tenant.query(deps.knex).deleteById(id)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets do a soft delete here, we can already leverage the deletedAt

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated to soft delete

return deleted > 0
}
Loading