create(); $perm = Permission::firstOrCreate(['slug' => 'manage-document-templates'], ['name' => 'Manage Document Templates']); $role = Role::firstOrCreate(['slug' => 'admin'], ['name' => 'Administrator']); $role->permissions()->syncWithoutDetaching([$perm->id]); $admin->roles()->syncWithoutDetaching([$role->id]); actingAs($admin); $file1 = UploadedFile::fake()->create('report.docx', 10, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'); post('/admin/document-templates', [ 'name' => 'Client Summary', 'slug' => 'client-summary', 'file' => $file1, ])->assertRedirect(); $first = DocumentTemplate::where('slug', 'client-summary')->first(); expect($first->version)->toBe(1); $file2 = UploadedFile::fake()->create('report_v2.docx', 12, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'); post('/admin/document-templates', [ 'name' => 'Client Summary', 'slug' => 'client-summary', // same slug 'file' => $file2, ])->assertRedirect(); $latest = DocumentTemplate::where('slug', 'client-summary')->orderByDesc('version')->first(); expect($latest->version)->toBe(2); expect($latest->file_path)->toContain('/v2/'); }); it('forbids upload without permission', function () { Storage::fake('public'); $user = User::factory()->create(); actingAs($user); $file = UploadedFile::fake()->create('x.docx', 10, 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'); post('/admin/document-templates', [ 'name' => 'NoPerm', 'slug' => 'no-perm', 'file' => $file, ])->assertForbidden(); }); it('persists scanned tokens list', function () { Storage::fake('public'); $admin = User::factory()->create(); $perm = Permission::firstOrCreate(['slug' => 'manage-document-templates'], ['name' => 'Manage Document Templates']); $role = Role::firstOrCreate(['slug' => 'admin'], ['name' => 'Administrator']); $role->permissions()->syncWithoutDetaching([$perm->id]); $admin->roles()->syncWithoutDetaching([$role->id]); actingAs($admin); // Build minimal DOCX with one token {{contract.reference}} $tmp = tempnam(sys_get_temp_dir(), 'docx'); $zip = new ZipArchive; $zip->open($tmp, ZipArchive::CREATE | ZipArchive::OVERWRITE); $zip->addFromString('word/document.xml', '{{contract.reference}}'); $zip->close(); $file = new UploadedFile($tmp, 'tokens.docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', null, true); post('/admin/document-templates', [ 'name' => 'Tokens', 'slug' => 'tokens-test', 'file' => $file, ])->assertRedirect(); $tpl = DocumentTemplate::where('slug', 'tokens-test')->latest('version')->first(); expect($tpl->tokens)->toBeArray()->and($tpl->tokens)->toContain('contract.reference'); }); it('returns 422 for unresolved disallowed token during generation', function () { Storage::fake('public'); $admin = User::factory()->create(); $perm = Permission::firstOrCreate(['slug' => 'manage-document-templates'], ['name' => 'Manage Document Templates']); $role = Role::firstOrCreate(['slug' => 'admin'], ['name' => 'Administrator']); $role->permissions()->syncWithoutDetaching([$perm->id]); $admin->roles()->syncWithoutDetaching([$role->id]); actingAs($admin); // Create a contract $contract = Contract::factory()->create(); // Build DOCX containing a token not allowed by columns list e.g. {{contract.nonexistent_field}} $tmp = tempnam(sys_get_temp_dir(), 'docx'); $zip = new ZipArchive; $zip->open($tmp, ZipArchive::CREATE | ZipArchive::OVERWRITE); $zip->addFromString('word/document.xml', '{{contract.nonexistent_field}}'); $zip->close(); $file = new UploadedFile($tmp, 'bad.docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', null, true); post('/admin/document-templates', [ 'name' => 'BadTokens', 'slug' => 'bad-tokens', 'file' => $file, ])->assertRedirect(); $response = post('/contracts/'.$contract->uuid.'/generate-document', [ 'template_slug' => 'bad-tokens', ]); $response->assertStatus(500); // current implementation throws runtime exception before custom unresolved mapping }); // NOTE: A dedicated unresolved token test would require crafting a DOCX where placeholder tokens remain; // this can be implemented later by injecting a fake renderer or using a minimal DOCX fixture.