This commit is contained in:
2026-09-02 09:19:43 +03:00
parent 74cb5b6176
commit 962f489142
34 changed files with 1190 additions and 124 deletions
@@ -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 })
})
})