-
Notifications
You must be signed in to change notification settings - Fork 89
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat(auth): tenant service * chore(auth): format * fix(auth): jest test warning about migration * fix(auth): remove temporary code * feat(auth): soft delete tenants * fix(auth): return erroneously removed tests
- Loading branch information
1 parent
ea7e660
commit 349b01e
Showing
5 changed files
with
262 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
import { faker } from '@faker-js/faker' | ||
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 './model' | ||
|
||
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') | ||
}) | ||
|
||
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 | ||
}) | ||
expect(tenant.deletedAt).toBe(undefined) | ||
}) | ||
|
||
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() | ||
}) | ||
|
||
test('returns undefined for soft deleted tenant', async (): Promise<void> => { | ||
const tenantData = createTenantData() | ||
const created = await tenantService.create(tenantData) | ||
await tenantService.delete(created.id) | ||
|
||
const tenant = await tenantService.get(created.id) | ||
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('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 | ||
}) | ||
}) | ||
|
||
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('returns undefined for soft-deleted tenant', async (): Promise<void> => { | ||
const tenantData = createTenantData() | ||
const created = await tenantService.create(tenantData) | ||
await tenantService.delete(created.id) | ||
|
||
const updated = await tenantService.update(created.id, { | ||
idpConsentUrl: faker.internet.url() | ||
}) | ||
expect(updated).toBeUndefined() | ||
}) | ||
}) | ||
|
||
describe('delete', (): void => { | ||
test('soft 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() | ||
|
||
const deletedTenant = await Tenant.query() | ||
.findById(created.id) | ||
.whereNotNull('deletedAt') | ||
expect(deletedTenant).toBeDefined() | ||
expect(deletedTenant?.deletedAt).toBeDefined() | ||
}) | ||
|
||
test('returns false for non-existent tenant', async (): Promise<void> => { | ||
const result = await tenantService.delete(faker.string.uuid()) | ||
expect(result).toBe(false) | ||
}) | ||
|
||
test('returns false for already deleted tenant', async (): Promise<void> => { | ||
const tenantData = createTenantData() | ||
const created = await tenantService.create(tenantData) | ||
|
||
await tenantService.delete(created.id) | ||
const secondDelete = await tenantService.delete(created.id) | ||
expect(secondDelete).toBe(false) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { BaseService } from '../shared/baseService' | ||
import { TransactionOrKnex } from 'objection' | ||
import { Tenant } from './model' | ||
|
||
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) | ||
.whereNull('deletedAt') | ||
.first() | ||
} | ||
|
||
async function updateTenant( | ||
deps: ServiceDependencies, | ||
id: string, | ||
input: Partial<Omit<CreateOptions, 'id'>> | ||
): Promise<Tenant | undefined> { | ||
return await Tenant.query(deps.knex) | ||
.whereNull('deletedAt') | ||
.patchAndFetchById(id, input) | ||
} | ||
|
||
async function deleteTenant( | ||
deps: ServiceDependencies, | ||
id: string | ||
): Promise<boolean> { | ||
const deleted = await Tenant.query(deps.knex) | ||
.patch({ deletedAt: new Date() }) | ||
.whereNull('deletedAt') | ||
.where('id', id) | ||
return deleted > 0 | ||
} |