tenant = Tenant::factory()->create(); $this->user = User::factory()->create(['tenant_id' => $this->tenant->id]); $this->actingAs($this->user); }); /** * Роутер собран руками (не app()-синглтон): в песочнице синглтон — заглушка, * которая маршрутизирует всё подряд, и тест ничего бы не проверил. */ function client_optout_selector(): ClientSmsRecipientSelector { return new ClientSmsRecipientSelector( new SmsRouter([new MtsSmsProvider(token: 'test', serves: ['mts'], priceKopecks: ['*' => 0])]), new OperatorNormalizer, new AllowedSmsOperators(new OperatorNormalizer), ); } /** @param list $colA */ function optoutXlsx(array $colA): UploadedFile { $ss = new Spreadsheet; $sheet = $ss->getActiveSheet(); foreach (array_values($colA) as $i => $v) { $sheet->setCellValueExplicit('A'.($i + 1), $v, DataType::TYPE_STRING); } $path = tempnam(sys_get_temp_dir(), 'stop').'.xlsx'; (new XlsxWriter($ss))->save($path); return new UploadedFile($path, 'stop-list.xlsx', null, null, true); } it('клиент вписывает номер в «Не писать этим» — и рассылка на него больше не уходит', function () { $this->postJson('/api/sms/optouts', ['phones' => ['+7 999 000-00-01']]) ->assertStatus(201) ->assertJsonPath('added', 1); $plan = client_optout_selector()->build( [['phone' => '79990000001', 'operator' => 'mts']], $this->tenant->id, ); expect($plan->sendable)->toBeEmpty(); }); it('внесённый вручную номер помечен источником «клиент» и хранит комментарий', function () { $this->postJson('/api/sms/optouts', [ 'phones' => ['79990000001'], 'note' => 'просил по телефону не писать', ])->assertStatus(201); $row = ClientSmsOptout::where('tenant_id', $this->tenant->id)->firstOrFail(); expect($row->source)->toBe(ClientSmsOptout::SOURCE_CLIENT) ->and($row->note)->toBe('просил по телефону не писать'); }); it('клиент видит свой список «Не писать этим»', function () { ClientSmsOptout::create(['tenant_id' => $this->tenant->id, 'phone' => '79990000001']); ClientSmsOptout::create(['tenant_id' => $this->tenant->id, 'phone' => '79990000002']); $items = $this->getJson('/api/sms/optouts')->assertOk()->json('items'); expect($items)->toHaveCount(2); }); it('клиент удаляет номер — и он снова получает рассылки', function () { $row = ClientSmsOptout::create(['tenant_id' => $this->tenant->id, 'phone' => '79990000001']); $this->deleteJson("/api/sms/optouts/{$row->id}")->assertOk(); $plan = client_optout_selector()->build( [['phone' => '79990000001', 'operator' => 'mts']], $this->tenant->id, ); expect($plan->sendable)->toHaveCount(1); }); it('непонятые номера возвращаются образцами, а не глотаются молча', function () { $this->postJson('/api/sms/optouts', ['phones' => ['79990000001', 'абырвалг', '123']]) ->assertStatus(201) ->assertJsonPath('added', 1) ->assertJsonPath('rejected', 2) ->assertJsonPath('rejected_samples', ['абырвалг', '123']); }); it('повторное внесение того же номера не плодит записей', function () { $this->postJson('/api/sms/optouts', ['phones' => ['79990000001']])->assertStatus(201); $this->postJson('/api/sms/optouts', ['phones' => ['+79990000001']]) ->assertStatus(201) ->assertJsonPath('added', 0); expect(ClientSmsOptout::where('tenant_id', $this->tenant->id)->count())->toBe(1); }); it('клиент загружает файлом сразу много номеров — все попали, мусор показан образцами', function () { $file = optoutXlsx(['Телефон', '79990000001', '+7 999 000-00-02', 'мусор']); $this->post('/api/sms/optouts/file', ['file' => $file], ['Accept' => 'application/json']) ->assertStatus(201) ->assertJsonPath('added', 2) ->assertJsonPath('rejected', 1) ->assertJsonPath('rejected_samples', ['мусор']); expect(ClientSmsOptout::where('tenant_id', $this->tenant->id)->count())->toBe(2); }); it('чужой стоп-лист не виден и не удаляется', function () { $other = Tenant::factory()->create(); $foreign = ClientSmsOptout::create(['tenant_id' => $other->id, 'phone' => '79990000009']); expect($this->getJson('/api/sms/optouts')->json('items'))->toBeEmpty(); $this->deleteJson("/api/sms/optouts/{$foreign->id}")->assertOk(); expect(ClientSmsOptout::where('tenant_id', $other->id)->count())->toBe(1); });