Files
InvoiceShelf/tests/Feature/Admin/AdminSettingsTest.php
T
Darko Gjorgjijoski 5ef7804e60 refactor: adopt modular domain architecture (#747)
* refactor: stabilize model identities for domain migration

* refactor: extract module platform context

* refactor: assign models to domain contexts

* refactor: extract ai platform context

* refactor: extract storage platform context

* refactor: extract mail platform context

* refactor: extract pdf platform context

* refactor: extract operations platform context

* refactor: move installation into operations platform

* refactor: extract money domain context

* refactor: extract taxation domain context

* refactor: extract catalog domain context

* refactor: extract metadata domain context

* refactor: extract reporting domain context

* refactor: extract purchases domain context

* refactor: extract receivables domain context

* refactor: extract accounts domain context

* refactor: complete reporting statement boundary

* refactor: extract contacts domain context

* refactor: extract sales domain context

* refactor: remove legacy application layers

* fix: migrate legacy bouncer role identities
2026-08-05 17:40:03 +02:00

347 lines
12 KiB
PHP

<?php
use App\Domains\Accounts\Models\User;
use Illuminate\Support\Facades\Artisan;
use Laravel\Sanctum\Sanctum;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory;
use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory;
use function Pest\Laravel\getJson;
use function Pest\Laravel\postJson;
beforeEach(function () {
Artisan::call('db:seed', ['--class' => 'DatabaseSeeder', '--force' => true]);
Artisan::call('db:seed', ['--class' => 'DemoSeeder', '--force' => true]);
$user = User::find(1);
$this->withHeaders([
'company' => $user->companies()->first()->id,
]);
Sanctum::actingAs(
$user,
['*']
);
});
test('company settings config uses canonical v2 links', function () {
$links = collect(config('invoiceshelf.setting_menu'))->pluck('link');
expect($links)->toContain('/admin/settings/exchange-rate');
expect($links)->toContain('/admin/settings/payment-modes');
expect($links)->toContain('/admin/settings/expense-categories');
expect($links)->toContain('/admin/settings/roles');
expect($links)->toContain('/admin/settings/mail-config');
expect($links)->not->toContain('/admin/settings/roles-settings');
expect($links)->not->toContain('/admin/settings/exchange-rate-provider');
expect($links)->not->toContain('/admin/settings/payment-mode');
expect($links)->not->toContain('/admin/settings/expense-category');
expect($links)->not->toContain('/admin/settings/mail-configuration');
$bootstrapMenu = json_encode(getJson('/api/v1/bootstrap')
->assertOk()
->json('setting_menu'));
expect($bootstrapMenu)->not->toContain('roles-settings');
expect($bootstrapMenu)->not->toContain('exchange-rate-provider');
expect($bootstrapMenu)->not->toContain('payment-mode');
expect($bootstrapMenu)->not->toContain('expense-category');
expect($bootstrapMenu)->not->toContain('mail-configuration');
});
test('super admin bootstrap uses administration mode when requested', function () {
getJson('/api/v1/bootstrap?admin_mode=1')
->assertOk()
->assertJsonPath('admin_mode', true)
->assertJsonPath('current_company', null)
->assertJsonCount(0, 'setting_menu');
});
test('bootstrap without administration mode hydrates the selected company', function () {
$companyId = User::find(1)->companies()->first()->id;
getJson('/api/v1/bootstrap')
->assertOk()
->assertJsonPath('current_company.id', $companyId);
});
test('get global mail configuration', function () {
getJson('/api/v1/mail/config')
->assertOk()
->assertJsonStructure([
'mail_driver',
'from_name',
'from_mail',
]);
});
test('get global mail drivers returns capability-backed drivers', function () {
$drivers = getJson('/api/v1/mail/drivers')
->assertOk()
->json();
expect($drivers)->toContain('smtp', 'mail', 'sendmail', 'ses');
if (class_exists(HttpClient::class) && class_exists(MailgunTransportFactory::class)) {
expect($drivers)->toContain('mailgun');
} else {
expect($drivers)->not->toContain('mailgun');
}
if (class_exists(HttpClient::class) && class_exists(PostmarkTransportFactory::class)) {
expect($drivers)->toContain('postmark');
} else {
expect($drivers)->not->toContain('postmark');
}
});
test('save global mail configuration', function () {
postJson('/api/v1/mail/config', [
'mail_driver' => 'smtp',
'mail_host' => 'smtp.example.com',
'mail_port' => 587,
'mail_username' => 'demo-user',
'mail_password' => 'secret',
'mail_encryption' => 'tls',
'mail_scheme' => 'smtp',
'mail_url' => 'smtp://smtp.example.com',
'mail_timeout' => 30,
'mail_local_domain' => 'invoiceshelf.test',
'from_name' => 'InvoiceShelf',
'from_mail' => 'hello@example.com',
])
->assertOk()
->assertJson([
'success' => 'mail_variables_save_successfully',
]);
$this->assertDatabaseHas('settings', [
'option' => 'mail_driver',
'value' => 'smtp',
]);
$this->assertDatabaseHas('settings', [
'option' => 'from_mail',
'value' => 'hello@example.com',
]);
$this->assertDatabaseHas('settings', [
'option' => 'mail_timeout',
'value' => '30',
]);
});
test('save global postmark configuration', function () {
postJson('/api/v1/mail/config', [
'mail_driver' => 'postmark',
'mail_postmark_token' => 'postmark-token',
'mail_postmark_message_stream_id' => 'outbound',
'from_name' => 'InvoiceShelf',
'from_mail' => 'billing@example.com',
])
->assertOk()
->assertJson([
'success' => 'mail_variables_save_successfully',
]);
$this->assertDatabaseHas('settings', [
'option' => 'mail_driver',
'value' => 'postmark',
]);
$this->assertDatabaseHas('settings', [
'option' => 'mail_postmark_token',
'value' => 'postmark-token',
]);
});
test('get pdf configuration', function () {
getJson('/api/v1/pdf/config')
->assertOk()
->assertJsonStructure([
'pdf_driver',
'gotenberg_host',
'pdf_paper_width',
'pdf_paper_height',
'pdf_orientation',
'pdf_margin_top',
'pdf_margin_right',
'pdf_margin_bottom',
'pdf_margin_left',
]);
});
/**
* Page geometry is saved for whichever driver is selected. It used to hang off
* gotenberg_papersize, so picking dompdf meant having no paper size at all and
* switching drivers threw the setting away.
*/
test('save pdf configuration stores the page setup for dompdf too', function () {
postJson('/api/v1/pdf/config', [
'pdf_driver' => 'dompdf',
'pdf_paper_width' => '8.5in',
'pdf_paper_height' => '14in',
'pdf_orientation' => 'landscape',
'pdf_margin_top' => '5mm',
'pdf_margin_right' => '6mm',
'pdf_margin_bottom' => '7mm',
'pdf_margin_left' => '8mm',
])
->assertOk()
->assertJson(['success' => 'pdf_variables_save_successfully']);
foreach ([
'pdf_driver' => 'dompdf',
'pdf_paper_width' => '8.5in',
'pdf_paper_height' => '14in',
'pdf_orientation' => 'landscape',
'pdf_margin_top' => '5mm',
'pdf_margin_right' => '6mm',
'pdf_margin_bottom' => '7mm',
'pdf_margin_left' => '8mm',
] as $option => $value) {
$this->assertDatabaseHas('settings', compact('option', 'value'));
}
});
test('pdf configuration rejects a length with no unit', function () {
postJson('/api/v1/pdf/config', [
'pdf_driver' => 'dompdf',
'pdf_paper_width' => '210',
'pdf_paper_height' => '297mm',
'pdf_orientation' => 'portrait',
])->assertStatus(422)->assertJsonValidationErrors('pdf_paper_width');
});
test('pdf configuration rejects an unknown orientation', function () {
postJson('/api/v1/pdf/config', [
'pdf_driver' => 'dompdf',
'pdf_paper_width' => '210mm',
'pdf_paper_height' => '297mm',
'pdf_orientation' => 'sideways',
])->assertStatus(422)->assertJsonValidationErrors('pdf_orientation');
});
/**
* A zero margin is a deliberate choice. Runtime configuration must not discard
* it as empty, so this pins the behavior through the API round trip.
*/
test('page numbers can be turned on and read back', function () {
postJson('/api/v1/pdf/config', [
'pdf_driver' => 'gotenberg',
'gotenberg_host' => 'https://pdf.example.com',
'pdf_paper_width' => '210mm',
'pdf_paper_height' => '297mm',
'pdf_orientation' => 'portrait',
'pdf_page_numbers' => true,
])->assertOk();
getJson('/api/v1/pdf/config')->assertOk()->assertJson(['pdf_page_numbers' => true]);
});
/**
* The dompdf form does not render the page-numbers control, since dompdf cannot
* repeat a footer. Saving from it must leave the stored choice alone rather than
* writing an absent field as false.
*/
test('saving from the dompdf form leaves the page-number choice alone', function () {
postJson('/api/v1/pdf/config', [
'pdf_driver' => 'gotenberg',
'gotenberg_host' => 'https://pdf.example.com',
'pdf_paper_width' => '210mm',
'pdf_paper_height' => '297mm',
'pdf_orientation' => 'portrait',
'pdf_page_numbers' => true,
])->assertOk();
postJson('/api/v1/pdf/config', [
'pdf_driver' => 'dompdf',
'pdf_paper_width' => '210mm',
'pdf_paper_height' => '297mm',
'pdf_orientation' => 'portrait',
])->assertOk();
getJson('/api/v1/pdf/config')->assertOk()->assertJson(['pdf_page_numbers' => true]);
});
test('page numbers can be turned back off', function () {
postJson('/api/v1/pdf/config', [
'pdf_driver' => 'gotenberg',
'gotenberg_host' => 'https://pdf.example.com',
'pdf_paper_width' => '210mm',
'pdf_paper_height' => '297mm',
'pdf_orientation' => 'portrait',
'pdf_page_numbers' => false,
])->assertOk();
// Stored as the string '0', which !empty() would have discarded.
$this->assertDatabaseHas('settings', ['option' => 'pdf_page_numbers', 'value' => '0']);
getJson('/api/v1/pdf/config')->assertOk()->assertJson(['pdf_page_numbers' => false]);
});
test('a zero margin survives the round trip', function () {
postJson('/api/v1/pdf/config', [
'pdf_driver' => 'dompdf',
'pdf_paper_width' => '210mm',
'pdf_paper_height' => '297mm',
'pdf_orientation' => 'portrait',
'pdf_margin_top' => '0mm',
'pdf_margin_right' => '0mm',
'pdf_margin_bottom' => '0mm',
'pdf_margin_left' => '0mm',
])->assertOk();
getJson('/api/v1/pdf/config')->assertOk()->assertJson(['pdf_margin_top' => '0mm']);
});
test('get app version', function () {
getJson('/api/v1/app/version')
->assertOk()
->assertJsonStructure([
'version',
'channel',
]);
});
/**
* The SDK forwards the pdfa value unvalidated, so an unsupported one would only
* fail later as an HTTP error from the Gotenberg service. The setting is a fixed
* list checked against what gotenberg:8 can actually produce.
*/
test('the archival format must be one gotenberg can produce', function () {
postJson('/api/v1/pdf/config', [
'pdf_driver' => 'gotenberg',
'gotenberg_host' => 'https://pdf.example.com',
'gotenberg_pdfa' => 'PDF/A-9z',
'pdf_paper_width' => '210mm',
'pdf_paper_height' => '297mm',
'pdf_orientation' => 'portrait',
])->assertStatus(422)->assertJsonValidationErrors('gotenberg_pdfa');
});
test('the archival format round trips, and off is a real choice', function () {
postJson('/api/v1/pdf/config', [
'pdf_driver' => 'gotenberg',
'gotenberg_host' => 'https://pdf.example.com',
'gotenberg_pdfa' => 'PDF/A-3b',
'pdf_paper_width' => '210mm',
'pdf_paper_height' => '297mm',
'pdf_orientation' => 'portrait',
])->assertOk();
getJson('/api/v1/pdf/config')->assertOk()->assertJson(['gotenberg_pdfa' => 'PDF/A-3b']);
postJson('/api/v1/pdf/config', [
'pdf_driver' => 'gotenberg',
'gotenberg_host' => 'https://pdf.example.com',
'gotenberg_pdfa' => '',
'pdf_paper_width' => '210mm',
'pdf_paper_height' => '297mm',
'pdf_orientation' => 'portrait',
])->assertOk();
getJson('/api/v1/pdf/config')->assertOk()->assertJson(['gotenberg_pdfa' => '']);
});