Queue::fake()); it('creates a site project with valid payload', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(['tenant_id' => $tenant->id]); $response = $this->actingAs($user)->postJson('/api/projects', [ 'name' => 'Окна СПб', 'signal_type' => 'site', 'signal_identifier' => 'okna-spb.ru', 'daily_limit_target' => 50, 'regions' => [], 'delivery_days_mask' => 127, ]); $response->assertCreated(); $response->assertJsonPath('data.sync_status', 'pending'); expect(Project::where('signal_identifier', 'okna-spb.ru')->exists())->toBeTrue(); Queue::assertPushed(SyncSupplierProjectJob::class); }); it('rejects invalid site domain', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(['tenant_id' => $tenant->id]); $response = $this->actingAs($user)->postJson('/api/projects', [ 'name' => 'X', 'signal_type' => 'site', 'signal_identifier' => 'not a domain', 'daily_limit_target' => 50, 'regions' => [], 'delivery_days_mask' => 127, ]); $response->assertStatus(422); $response->assertJsonValidationErrors(['signal_identifier']); }); it('creates a call project with valid 11-digit phone', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(['tenant_id' => $tenant->id]); $response = $this->actingAs($user)->postJson('/api/projects', [ 'name' => 'Натяжные', 'signal_type' => 'call', 'signal_identifier' => '79161234567', 'daily_limit_target' => 30, 'regions' => [], 'delivery_days_mask' => 127, ]); $response->assertCreated(); }); it('rejects call signal_identifier not starting with 7', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(['tenant_id' => $tenant->id]); $response = $this->actingAs($user)->postJson('/api/projects', [ 'name' => 'X', 'signal_type' => 'call', 'signal_identifier' => '89991234567', 'daily_limit_target' => 30, 'regions' => [], 'delivery_days_mask' => 127, ]); $response->assertStatus(422); }); it('creates sms project with senders + keyword', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(['tenant_id' => $tenant->id]); $response = $this->actingAs($user)->postJson('/api/projects', [ 'name' => 'Ипотека', 'signal_type' => 'sms', 'sms_senders' => ['TINKOFF'], 'sms_keyword' => 'ипотека', 'daily_limit_target' => 100, 'regions' => [], 'delivery_days_mask' => 127, ]); $response->assertCreated(); $project = Project::where('name', 'Ипотека')->first(); expect($project->sms_senders)->toBe(['TINKOFF']); expect($project->sms_keyword)->toBe('ипотека'); }); it('rejects sms project without sms_senders', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(['tenant_id' => $tenant->id]); $response = $this->actingAs($user)->postJson('/api/projects', [ 'name' => 'X', 'signal_type' => 'sms', 'daily_limit_target' => 100, 'regions' => [], 'delivery_days_mask' => 127, ]); $response->assertStatus(422); $response->assertJsonValidationErrors(['sms_senders']); }); it('rejects when tenant exceeds max_projects limit', function () { $tenant = Tenant::factory()->create(['limits' => ['max_projects' => 1]]); $user = User::factory()->create(['tenant_id' => $tenant->id]); Project::factory()->create(['tenant_id' => $tenant->id]); $response = $this->actingAs($user)->postJson('/api/projects', [ 'name' => 'second', 'signal_type' => 'site', 'signal_identifier' => 'second.ru', 'daily_limit_target' => 10, 'regions' => [], 'delivery_days_mask' => 127, ]); $response->assertStatus(403); }); it('forces tenant_id from auth user (not from payload)', function () { $tenantA = Tenant::factory()->create(); $tenantB = Tenant::factory()->create(); $userA = User::factory()->create(['tenant_id' => $tenantA->id]); $this->actingAs($userA)->postJson('/api/projects', [ 'tenant_id' => $tenantB->id, // попытка инъекции 'name' => 'X', 'signal_type' => 'site', 'signal_identifier' => 'x.ru', 'daily_limit_target' => 10, 'regions' => [], 'delivery_days_mask' => 127, ]); $project = Project::where('signal_identifier', 'x.ru')->latest()->first(); expect($project->tenant_id)->toBe($tenantA->id); }); it('rejects site domain with consecutive dots', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(['tenant_id' => $tenant->id]); $response = $this->actingAs($user)->postJson('/api/projects', [ 'name' => 'X', 'signal_type' => 'site', 'signal_identifier' => 'okna..spb.ru', 'daily_limit_target' => 50, 'regions' => [], 'delivery_days_mask' => 127, ]); $response->assertStatus(422); $response->assertJsonValidationErrors(['signal_identifier']); }); // Plan 6 — subject-level regions[] support. it('creates project with subject-level regions array', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(['tenant_id' => $tenant->id]); $response = $this->actingAs($user)->postJson('/api/projects', [ 'name' => 'Regions Test Project', 'signal_type' => 'site', 'signal_identifier' => 'regions-test.example', 'daily_limit_target' => 50, 'delivery_days_mask' => 127, 'regions' => [82, 83], // Москва + СПб ]); $response->assertStatus(201); $created = Project::where('name', 'Regions Test Project')->firstOrFail(); expect($created->regions)->toBe([82, 83]); }); it('dual-writes region_mask=255 + region_mode=include for backward-compat', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(['tenant_id' => $tenant->id]); $response = $this->actingAs($user)->postJson('/api/projects', [ 'name' => 'Dual Write Test', 'signal_type' => 'site', 'signal_identifier' => 'dualwrite.example', 'daily_limit_target' => 50, 'delivery_days_mask' => 127, 'regions' => [77], ]); $response->assertStatus(201); $created = Project::where('name', 'Dual Write Test')->firstOrFail(); expect($created->region_mask)->toBe(255); expect($created->region_mode)->toBe('include'); }); it('rejects regions code out of 1..89 range with 422', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(['tenant_id' => $tenant->id]); $response = $this->actingAs($user)->postJson('/api/projects', [ 'name' => 'Invalid Code Test', 'signal_type' => 'site', 'signal_identifier' => 'invalid.example', 'daily_limit_target' => 50, 'delivery_days_mask' => 127, 'regions' => [90, 100], ]); $response->assertStatus(422); $response->assertJsonValidationErrors(['regions.0', 'regions.1']); });