update
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
import { test } from '@japa/runner'
|
||||
import testUtils from '@adonisjs/core/services/test_utils'
|
||||
import Contact from '#base_modules/Contact/model/contact'
|
||||
import Tenant from '#base_modules/Tenant/model/tenant'
|
||||
import { createAdminWithToken } from '#tests/utils/auth'
|
||||
|
||||
test.group('Contacts API', (group) => {
|
||||
group.each.setup(() => testUtils.db().wrapInGlobalTransaction())
|
||||
test('rejects an unauthenticated list request', async ({ client }) => {
|
||||
const response = await client.get('/api/contacts').header('X-Tenant-Id', '1')
|
||||
|
||||
response.assertStatus(401)
|
||||
})
|
||||
|
||||
test('lists only contacts owned by the selected tenant', async ({ assert, client }) => {
|
||||
const { admin, token } = await createAdminWithToken([])
|
||||
const tenant = await Tenant.create({ name: 'Acme GmbH', slug: 'acme-gmbh', status: 'active' })
|
||||
await tenant.related('admins').attach([admin.id])
|
||||
await Contact.create({
|
||||
tenantId: tenant.id,
|
||||
displayName: 'Ada Lovelace',
|
||||
email: 'ada@example.test',
|
||||
phone: null,
|
||||
status: 'active',
|
||||
})
|
||||
|
||||
const response = await client
|
||||
.get('/api/contacts')
|
||||
.header('X-Tenant-Id', String(tenant.id))
|
||||
.bearerToken(token)
|
||||
|
||||
response.assertStatus(200)
|
||||
assert.deepEqual(response.body(), {
|
||||
data: [
|
||||
{
|
||||
id: 1,
|
||||
display_name: 'Ada Lovelace',
|
||||
email: 'ada@example.test',
|
||||
phone: null,
|
||||
status: 'active',
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
test('creates a contact within the resolved tenant', async ({ assert, client }) => {
|
||||
const { admin, token } = await createAdminWithToken([])
|
||||
const tenant = await Tenant.create({
|
||||
name: 'Create GmbH',
|
||||
slug: 'create-gmbh',
|
||||
status: 'active',
|
||||
})
|
||||
await tenant.related('admins').attach([admin.id])
|
||||
|
||||
const response = await client
|
||||
.post('/api/contacts')
|
||||
.header('X-Tenant-Id', String(tenant.id))
|
||||
.bearerToken(token)
|
||||
.json({ display_name: 'Grace Hopper', email: 'grace@example.test', phone: '+1-555-0100' })
|
||||
|
||||
response.assertStatus(201)
|
||||
const created = response.body().data
|
||||
assert.deepInclude(created, {
|
||||
display_name: 'Grace Hopper',
|
||||
email: 'grace@example.test',
|
||||
phone: '+1-555-0100',
|
||||
status: 'active',
|
||||
})
|
||||
const persistedContact = await Contact.find(created.id)
|
||||
assert.equal(persistedContact?.tenantId, tenant.id)
|
||||
})
|
||||
|
||||
test('rejects a contact without a display name', async ({ assert, client }) => {
|
||||
const { admin, token } = await createAdminWithToken([])
|
||||
const tenant = await Tenant.create({
|
||||
name: 'Validation GmbH',
|
||||
slug: 'validation-gmbh',
|
||||
status: 'active',
|
||||
})
|
||||
await tenant.related('admins').attach([admin.id])
|
||||
|
||||
const response = await client
|
||||
.post('/api/contacts')
|
||||
.header('X-Tenant-Id', String(tenant.id))
|
||||
.bearerToken(token)
|
||||
.json({ email: 'missing-name@example.test' })
|
||||
|
||||
response.assertStatus(422)
|
||||
assert.equal(response.body().errors[0].field, 'display_name')
|
||||
})
|
||||
|
||||
test('shows a contact only inside the resolved tenant', async ({ assert, client }) => {
|
||||
const { admin, token } = await createAdminWithToken([])
|
||||
const tenant = await Tenant.create({ name: 'Show GmbH', slug: 'show-gmbh', status: 'active' })
|
||||
await tenant.related('admins').attach([admin.id])
|
||||
const contact = await Contact.create({
|
||||
tenantId: tenant.id,
|
||||
displayName: 'Katherine Johnson',
|
||||
email: 'katherine@example.test',
|
||||
phone: null,
|
||||
status: 'active',
|
||||
})
|
||||
|
||||
const response = await client
|
||||
.get(`/api/contacts/${contact.id}`)
|
||||
.header('X-Tenant-Id', String(tenant.id))
|
||||
.bearerToken(token)
|
||||
|
||||
response.assertStatus(200)
|
||||
assert.deepEqual(response.body(), {
|
||||
data: {
|
||||
id: contact.id,
|
||||
display_name: 'Katherine Johnson',
|
||||
email: 'katherine@example.test',
|
||||
phone: null,
|
||||
status: 'active',
|
||||
},
|
||||
})
|
||||
|
||||
const otherTenant = await Tenant.create({
|
||||
name: 'Other GmbH',
|
||||
slug: 'other-gmbh',
|
||||
status: 'active',
|
||||
})
|
||||
const foreignContact = await Contact.create({
|
||||
tenantId: otherTenant.id,
|
||||
displayName: 'Tenant Two Contact',
|
||||
email: null,
|
||||
phone: null,
|
||||
status: 'active',
|
||||
})
|
||||
const foreignResponse = await client
|
||||
.get(`/api/contacts/${foreignContact.id}`)
|
||||
.header('X-Tenant-Id', String(tenant.id))
|
||||
.bearerToken(token)
|
||||
foreignResponse.assertStatus(404)
|
||||
})
|
||||
|
||||
test('updates a contact inside the resolved tenant', async ({ assert, client }) => {
|
||||
const { admin, token } = await createAdminWithToken([])
|
||||
const tenant = await Tenant.create({
|
||||
name: 'Update GmbH',
|
||||
slug: 'update-gmbh',
|
||||
status: 'active',
|
||||
})
|
||||
await tenant.related('admins').attach([admin.id])
|
||||
const contact = await Contact.create({
|
||||
tenantId: tenant.id,
|
||||
displayName: 'Original Name',
|
||||
email: null,
|
||||
phone: null,
|
||||
status: 'active',
|
||||
})
|
||||
|
||||
const response = await client
|
||||
.put(`/api/contacts/${contact.id}`)
|
||||
.header('X-Tenant-Id', String(tenant.id))
|
||||
.bearerToken(token)
|
||||
.json({ display_name: 'Updated Name', email: 'updated@example.test' })
|
||||
|
||||
response.assertStatus(200)
|
||||
assert.deepEqual(response.body(), {
|
||||
data: {
|
||||
id: contact.id,
|
||||
display_name: 'Updated Name',
|
||||
email: 'updated@example.test',
|
||||
phone: null,
|
||||
status: 'active',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test('deletes a contact inside the resolved tenant', async ({ assert, client }) => {
|
||||
const { admin, token } = await createAdminWithToken([])
|
||||
const tenant = await Tenant.create({
|
||||
name: 'Delete GmbH',
|
||||
slug: 'delete-gmbh',
|
||||
status: 'active',
|
||||
})
|
||||
await tenant.related('admins').attach([admin.id])
|
||||
const contact = await Contact.create({
|
||||
tenantId: tenant.id,
|
||||
displayName: 'Delete Me',
|
||||
email: null,
|
||||
phone: null,
|
||||
status: 'active',
|
||||
})
|
||||
|
||||
const response = await client
|
||||
.delete(`/api/contacts/${contact.id}`)
|
||||
.header('X-Tenant-Id', String(tenant.id))
|
||||
.bearerToken(token)
|
||||
|
||||
response.assertStatus(200)
|
||||
assert.equal(await Contact.find(contact.id), null)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
import { test } from '@japa/runner'
|
||||
import testUtils from '@adonisjs/core/services/test_utils'
|
||||
import CustomField from '#base_modules/CustomField/model/custom_field'
|
||||
import Tenant from '#base_modules/Tenant/model/tenant'
|
||||
import { createAdminWithToken } from '#tests/utils/auth'
|
||||
|
||||
test.group('Custom fields API', (group) => {
|
||||
group.each.setup(() => testUtils.db().withGlobalTransaction())
|
||||
|
||||
test('rejects an unauthenticated list request', async ({ client }) => {
|
||||
const response = await client.get('/api/custom-fields')
|
||||
|
||||
response.assertStatus(401)
|
||||
})
|
||||
|
||||
test('lists fields owned by the resolved tenant', async ({ assert, client }) => {
|
||||
const { admin, token } = await createAdminWithToken([])
|
||||
const tenant = await Tenant.create({
|
||||
name: 'Metadata GmbH',
|
||||
slug: 'metadata-gmbh',
|
||||
status: 'active',
|
||||
})
|
||||
await tenant.related('admins').attach([admin.id])
|
||||
await CustomField.create({
|
||||
tenantId: tenant.id,
|
||||
entityType: 'contact',
|
||||
name: 'Customer tier',
|
||||
key: 'customer_tier',
|
||||
fieldType: 'text',
|
||||
required: false,
|
||||
options: null,
|
||||
status: 'active',
|
||||
})
|
||||
|
||||
const response = await client
|
||||
.get('/api/custom-fields')
|
||||
.header('X-Tenant-Id', String(tenant.id))
|
||||
.bearerToken(token)
|
||||
|
||||
response.assertStatus(200)
|
||||
assert.equal(response.body().data[0].key, 'customer_tier')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,70 @@
|
||||
import { test } from '@japa/runner'
|
||||
import db from '@adonisjs/lucid/services/db'
|
||||
import testUtils from '@adonisjs/core/services/test_utils'
|
||||
import Tenant from '#base_modules/Tenant/model/tenant'
|
||||
import { createAdminWithToken } from '#tests/utils/auth'
|
||||
|
||||
test.group('Tenant context', (group) => {
|
||||
group.each.setup(() => testUtils.db().wrapInGlobalTransaction())
|
||||
|
||||
test('rejects an authenticated tenant-scoped request without an X-Tenant-Id header', async ({
|
||||
assert,
|
||||
client,
|
||||
}) => {
|
||||
const { token } = await createAdminWithToken([])
|
||||
|
||||
const response = await client.get('/tenants/current').bearerToken(token)
|
||||
|
||||
response.assertStatus(400)
|
||||
assert.equal(response.body().code, 'E_TENANT_REQUIRED')
|
||||
})
|
||||
|
||||
test('resolves a tenant only for an authenticated member', async ({ assert, client }) => {
|
||||
const { admin, token } = await createAdminWithToken([])
|
||||
const tenant = await Tenant.create({ name: 'Acme GmbH', slug: 'acme-gmbh', status: 'active' })
|
||||
await tenant.related('admins').attach([admin.id])
|
||||
|
||||
const response = await client
|
||||
.get('/tenants/current')
|
||||
.header('X-Tenant-Id', String(tenant.id))
|
||||
.bearerToken(token)
|
||||
|
||||
response.assertStatus(200)
|
||||
assert.deepEqual(response.body(), {
|
||||
tenant: { id: tenant.id, name: tenant.name, slug: tenant.slug },
|
||||
})
|
||||
})
|
||||
|
||||
test('rejects a tenant selected by an admin who is not a member', async ({ assert, client }) => {
|
||||
const { token } = await createAdminWithToken([])
|
||||
const [tenant] = await db
|
||||
.table('tenants')
|
||||
.insert({ name: 'Other GmbH', slug: 'other-gmbh', status: 'active' })
|
||||
.returning(['id'])
|
||||
|
||||
const response = await client
|
||||
.get('/tenants/current')
|
||||
.header('X-Tenant-Id', String(tenant.id))
|
||||
.bearerToken(token)
|
||||
|
||||
response.assertStatus(403)
|
||||
assert.equal(response.body().code, 'E_TENANT_ACCESS_DENIED')
|
||||
})
|
||||
|
||||
test('rejects membership in an inactive tenant', async ({ assert, client }) => {
|
||||
const { admin, token } = await createAdminWithToken([])
|
||||
const [tenant] = await db
|
||||
.table('tenants')
|
||||
.insert({ name: 'Inactive GmbH', slug: 'inactive-gmbh', status: 'inactive' })
|
||||
.returning(['id'])
|
||||
await db.table('admin_tenants').insert({ admin_id: admin.id, tenant_id: tenant.id })
|
||||
|
||||
const response = await client
|
||||
.get('/tenants/current')
|
||||
.header('X-Tenant-Id', String(tenant.id))
|
||||
.bearerToken(token)
|
||||
|
||||
response.assertStatus(403)
|
||||
assert.equal(response.body().code, 'E_TENANT_ACCESS_DENIED')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,71 @@
|
||||
import { test } from '@japa/runner'
|
||||
import { join } from 'node:path'
|
||||
import { renderTemplate } from '#services/module_generator/writers/template_renderer'
|
||||
|
||||
const TEMPLATE_DIR = join(process.cwd(), 'app', 'templates', 'backend')
|
||||
const API_TEMPLATE_DIR = join(TEMPLATE_DIR, 'api')
|
||||
|
||||
const context = {
|
||||
name: 'widget',
|
||||
capName: 'Widget',
|
||||
moduleName: 'Widget',
|
||||
modelName: 'Widgets',
|
||||
pascalCaseName: 'Widget',
|
||||
snakeCaseName: 'widget',
|
||||
snake: 'widget',
|
||||
swaggerExample: '{ "id": 42, "name": "Example widget" }',
|
||||
createFields: [{ column_name: 'name', sampleValue: "'Example widget'" }],
|
||||
updateFields: [{ column_name: 'name', sampleValue: "'Updated widget'" }],
|
||||
}
|
||||
|
||||
test.group('ERP module generator templates', () => {
|
||||
test('generates authenticated, permission-authorized API CRUD routes', ({ assert }) => {
|
||||
const routes = renderTemplate(join(API_TEMPLATE_DIR, 'routes.hbs'), context)
|
||||
const controller = renderTemplate(join(API_TEMPLATE_DIR, 'controller.hbs'), context)
|
||||
|
||||
assert.include(routes, "import { middleware } from '#start/kernel'")
|
||||
assert.include(routes, '.use([middleware.auth()])')
|
||||
assert.include(controller, "import WidgetPolicy from '#modules/Widget/policy/widget_policy'")
|
||||
assert.include(controller, "authorize('viewWidgetList')")
|
||||
assert.include(controller, "authorize('viewWidget')")
|
||||
assert.include(controller, "authorize('createWidget')")
|
||||
assert.include(controller, "authorize('updateWidget')")
|
||||
assert.include(controller, "authorize('deleteWidget')")
|
||||
})
|
||||
|
||||
test('generates concrete AutoSwagger examples for every API response status', ({ assert }) => {
|
||||
const controller = renderTemplate(join(API_TEMPLATE_DIR, 'controller.hbs'), context)
|
||||
|
||||
assert.include(controller, '@responseBody 200 - [{ "id": 42, "name": "Example widget" }]')
|
||||
assert.include(controller, '@responseBody 201 - { "id": 42, "name": "Example widget" }')
|
||||
assert.include(controller, '@responseBody 401 - { "message": "Unauthorized" }')
|
||||
assert.include(controller, '@responseBody 403 - { "message": "Forbidden" }')
|
||||
assert.include(
|
||||
controller,
|
||||
'@responseBody 422 - { "errors": [{ "message": "Validation failed", "rule": "required", "field": "name" }] }'
|
||||
)
|
||||
assert.notInclude(controller, '<Error422>')
|
||||
})
|
||||
|
||||
test('generates API specs covering every CRUD endpoint plus auth and permission boundaries', ({
|
||||
assert,
|
||||
}) => {
|
||||
const spec = renderTemplate(join(TEMPLATE_DIR, 'tests', 'api.spec.hbs'), context)
|
||||
|
||||
for (const methodAndPath of [
|
||||
'GET /api/widget -> list',
|
||||
'POST /api/widget -> create',
|
||||
'GET /api/widget/:id -> show',
|
||||
'PUT /api/widget/:id -> update',
|
||||
'DELETE /api/widget/:id -> destroy',
|
||||
]) {
|
||||
assert.include(spec, methodAndPath)
|
||||
}
|
||||
|
||||
assert.include(spec, 'rejects unauthenticated requests')
|
||||
assert.include(spec, 'rejects a token without the required permission')
|
||||
assert.include(spec, 'assertStatus(401)')
|
||||
assert.include(spec, 'assertStatus(403)')
|
||||
assert.include(spec, 'assertStatus(404)')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
import { test } from '@japa/runner'
|
||||
import testUtils from '@adonisjs/core/services/test_utils'
|
||||
import Tenant from '#base_modules/Tenant/model/tenant'
|
||||
import TenantAccessService from '#base_modules/Tenant/services/tenant_access_service'
|
||||
import { createAdminWithToken } from '#tests/utils/auth'
|
||||
|
||||
test.group('TenantAccessService', (group) => {
|
||||
group.each.setup(() => testUtils.db().wrapInGlobalTransaction())
|
||||
|
||||
test('resolves an active tenant for a member admin', async ({ assert }) => {
|
||||
const { admin } = await createAdminWithToken([])
|
||||
const tenant = await Tenant.create({
|
||||
name: 'Service GmbH',
|
||||
slug: 'service-gmbh',
|
||||
status: 'active',
|
||||
})
|
||||
await tenant.related('admins').attach([admin.id])
|
||||
|
||||
const resolved = await new TenantAccessService().resolveForAdmin(admin.id, tenant.id)
|
||||
|
||||
assert.deepEqual(resolved, { id: tenant.id, name: tenant.name, slug: tenant.slug })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user