parser = new SupplierCsvParser; }); it('parses empty CSV → yields nothing', function () { $rows = iterator_to_array($this->parser->parse('')); expect($rows)->toBeEmpty(); }); it('parses 1 row → yields 1 struct with project/tag/phone', function () { $csv = "Name;Tag;Phone\n" ."B1_example.com;mytag;79991234567\n"; $rows = iterator_to_array($this->parser->parse($csv)); expect($rows)->toHaveCount(1); expect($rows[0])->toMatchArray([ 'project' => 'B1_example.com', 'tag' => 'mytag', 'phone' => '79991234567', ]); }); it('parses 1000 rows without OOM (streaming generator)', function () { $lines = ['Name;Tag;Phone']; for ($i = 1; $i <= 1000; $i++) { $lines[] = "B1_test.com;tag{$i};79991234567"; } $csv = implode("\n", $lines)."\n"; $count = 0; foreach ($this->parser->parse($csv) as $_) { $count++; } expect($count)->toBe(1000); }); it('skips malformed rows with missing columns + logs warning', function () { Log::spy(); $csv = "Name;Tag;Phone\n" ."B1_example.com;mytag;79991234567\n" ."broken-row-only-one-column\n" ."B1_another.com;tag2;79991234500\n"; $rows = iterator_to_array($this->parser->parse($csv)); expect($rows)->toHaveCount(2); expect($rows[0]['project'])->toBe('B1_example.com'); expect($rows[1]['project'])->toBe('B1_another.com'); Log::shouldHaveReceived('warning') ->with('supplier_csv_parser.malformed_row', Mockery::any()) ->once(); }); it('handles BOM + CRLF line endings', function () { $bom = "\xEF\xBB\xBF"; $csv = $bom."Name;Tag;Phone\r\n" ."B1_example.com;mytag;79991234567\r\n"; $rows = iterator_to_array($this->parser->parse($csv)); expect($rows)->toHaveCount(1); expect($rows[0]['project'])->toBe('B1_example.com'); });