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 vid/project/phone/time', function () { $csv = "vid;project;tag;phone;phones;time\n" ."1234;B1_example.com;;79991234567;79991234567;1715432400\n"; $rows = iterator_to_array($this->parser->parse($csv)); expect($rows)->toHaveCount(1); expect($rows[0])->toMatchArray([ 'vid' => '1234', 'project' => 'B1_example.com', 'phone' => '79991234567', 'time' => 1715432400, ]); }); it('parses 1000 rows without OOM (streaming generator)', function () { $lines = ['vid;project;tag;phone;phones;time']; for ($i = 1; $i <= 1000; $i++) { $lines[] = "{$i};B1_test.com;;79991234567;79991234567;1715432400"; } $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 = "vid;project;tag;phone;phones;time\n" ."1234;B1_example.com;;79991234567;79991234567;1715432400\n" ."broken-row-only-one-column\n" ."5678;B1_another.com;;79991234567;79991234567;1715432500\n"; $rows = iterator_to_array($this->parser->parse($csv)); expect($rows)->toHaveCount(2); expect($rows[0]['vid'])->toBe('1234'); expect($rows[1]['vid'])->toBe('5678'); 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."vid;project;tag;phone;phones;time\r\n" ."1234;B1_example.com;;79991234567;79991234567;1715432400\r\n"; $rows = iterator_to_array($this->parser->parse($csv)); expect($rows)->toHaveCount(1); expect($rows[0]['vid'])->toBe('1234'); });