ldc-arcadia-genesis-php-client/tests/Unit/Utils/StringEncoderTest.php

351 lines
10 KiB
PHP
Raw Normal View History

<?php
declare(strict_types=1);
use Uwil\ArcadiaGenesisPhpClient\Utils\StringEncoder;
describe('toUtf8()', function (): void {
it('returns an empty string when given an empty string', function (): void {
// Given
$input = '';
// When
$result = StringEncoder::toUtf8($input, 'UTF-8');
// Then
expect($result)->toBe('');
});
it('passes through a valid UTF-8 string with multibyte characters', function (): void {
// Given
$input = 'Héllo café résumé';
// When
$result = StringEncoder::toUtf8($input, 'UTF-8');
// Then
expect($result)->toBe('Héllo café résumé');
});
it('throws ValueError for an invalid UTF-8 string', function (): void {
// Given
$input = "Invalid \xC3\x28 sequence";
// When / Then
StringEncoder::toUtf8($input, 'UTF-8');
})->throws(\ValueError::class, 'not valid UTF-8');
it('strips the UTF-8 BOM by default', function (): void {
// Given
$input = "\xEF\xBB\xBFBonjour";
// When
$result = StringEncoder::toUtf8($input, 'UTF-8');
// Then
expect($result)->toBe('Bonjour');
});
it('preserves the UTF-8 BOM when stripBom is false', function (): void {
// Given
$input = "\xEF\xBB\xBFBonjour";
// When
$result = StringEncoder::toUtf8($input, 'UTF-8', stripBom: false);
// Then
expect($result)->toBe("\xEF\xBB\xBFBonjour");
});
it('converts Windows-1252 special characters to UTF-8', function (int $byte, string $expectedUtf8): void {
// Given
$input = chr($byte);
// When
$result = StringEncoder::toUtf8($input, 'Windows-1252');
// Then
expect($result)->toBe($expectedUtf8);
})->with([
'euro sign (0x80)' => [0x80, '€'],
'single low-9 quote (0x82)' => [0x82, '‚'],
'latin small f (0x83)' => [0x83, 'ƒ'],
'double low-9 quote (0x84)' => [0x84, '„'],
'ellipsis (0x85)' => [0x85, '…'],
'dagger (0x86)' => [0x86, '†'],
'double dagger (0x87)' => [0x87, '‡'],
'circumflex (0x88)' => [0x88, 'ˆ'],
'per mille (0x89)' => [0x89, '‰'],
'S caron (0x8A)' => [0x8A, 'Š'],
'left single guillemet (0x8B)' => [0x8B, '‹'],
'OE ligature (0x8C)' => [0x8C, 'Œ'],
'Z caron (0x8E)' => [0x8E, 'Ž'],
'left single quote (0x91)' => [0x91, "\u{2018}"],
'right single quote (0x92)' => [0x92, "\u{2019}"],
'left double quote (0x93)' => [0x93, "\u{201C}"],
'right double quote (0x94)' => [0x94, "\u{201D}"],
'bullet (0x95)' => [0x95, '•'],
'en dash (0x96)' => [0x96, '–'],
'em dash (0x97)' => [0x97, '—'],
'tilde (0x98)' => [0x98, '˜'],
'trademark (0x99)' => [0x99, '™'],
's caron (0x9A)' => [0x9A, 'š'],
'right single guillemet (0x9B)' => [0x9B, '›'],
'oe ligature (0x9C)' => [0x9C, 'œ'],
'z caron (0x9E)' => [0x9E, 'ž'],
'Y diaeresis (0x9F)' => [0x9F, 'Ÿ'],
]);
it('converts Windows-1252 high bytes (0xA0-0xFF) as ISO-8859-1 identity', function (): void {
// Given
$input = chr(0xE9);
// When
$result = StringEncoder::toUtf8($input, 'Windows-1252');
// Then the result is the UTF-8 encoded é
expect($result)->toBe('é');
});
it('converts a full Windows-1252 sentence with mixed ranges', function (): void {
// Given
$input = "B\x9Cuf \xE0 5\x80";
// When
$result = StringEncoder::toUtf8($input, 'Windows-1252');
// Then
expect($result)->toBe('Bœuf à 5€');
});
it('handles undefined Windows-1252 bytes (0x81, 0x8D, 0x8F, 0x90, 0x9D) via Latin-1 fallback', function (int $byte): void {
// Given
$input = chr($byte);
// When
$result = StringEncoder::toUtf8($input, 'Windows-1252');
// Then
expect(mb_check_encoding($result, 'UTF-8'))->toBeTrue()
->and(mb_strlen($result, 'UTF-8'))->toBe(1);
})->with([0x81, 0x8D, 0x8F, 0x90, 0x9D]);
it('converts ISO-8859-15 euro sign to UTF-8', function (): void {
// Given
$input = chr(0xA4);
// When
$result = StringEncoder::toUtf8($input, 'ISO-8859-15');
// Then
expect($result)->toBe('€');
});
it('accepts encoding aliases', function (string $alias, string $expected): void {
// Given
$input = "caf\xE9"; // é in Latin-1/Win-1252
// When
$result = StringEncoder::toUtf8($input, $alias);
// Then
expect($result)->toBe($expected);
})->with([
'latin1' => ['latin1', 'café'],
'latin-1' => ['latin-1', 'café'],
'win1252' => ['win1252', 'café'],
'windows1252' => ['windows1252', 'café'],
'cp1252' => ['cp1252', 'café'],
]);
it('throws ValueError for an unsupported encoding', function (): void {
// Given / When / Then
StringEncoder::toUtf8('hello', 'EBCDIC-42');
})->throws(\ValueError::class, 'Unsupported encoding');
});
describe('detectAndConvertToUtf8()', function (): void {
it('returns an empty string when given an empty string', function (): void {
// Given
$input = '';
// When
$result = StringEncoder::detectAndConvertToUtf8($input);
// Then
expect($result)->toBe('');
});
it('detects and passes through valid UTF-8 with a hint', function (): void {
// Given
$input = 'Héllo world 🌍';
// When
$result = StringEncoder::detectAndConvertToUtf8($input, 'Windows-1252');
// Then
expect($result)->toBe('Héllo world 🌍');
});
it('strips the UTF-8 BOM from auto-detected UTF-8 strings', function (): void {
// Given
$input = "\xEF\xBB\xBFBonjour";
// When
$result = StringEncoder::detectAndConvertToUtf8($input);
// Then
expect($result)->toBe('Bonjour');
});
it('uses encoding hints to guide detection', function (): void {
// Given
$input = "5\x80";
// When
$result = StringEncoder::detectAndConvertToUtf8($input, 'Windows-1252');
// Then
expect($result)->toBe('5€');
});
it('produces valid UTF-8 output regardless of detection result', function (): void {
// Given
$input = "\xFE\x01\xFE\x01";
// When / Then
try {
$result = StringEncoder::detectAndConvertToUtf8($input);
expect(mb_check_encoding($result, 'UTF-8'))->toBeTrue();
} catch (\RuntimeException $e) {
expect($e->getMessage())->toContain('Unable to detect encoding');
}
});
});
describe('arrayToUtf8()', function (): void {
it('handles an empty array', function (): void {
// Given
$input = [];
// When
$result = StringEncoder::arrayToUtf8($input, 'UTF-8');
// Then
expect($result)->toBe([]);
});
it('converts an array of Windows-1252 strings with special characters', function (): void {
// Given
$input = ["5\x80", "B\x9Cuf"];
// When
$result = StringEncoder::arrayToUtf8($input, 'Windows-1252');
// Then
expect($result)->toBe(['5€', 'Bœuf']);
});
});
describe('supportedEncodings()', function (): void {
it('returns a non-empty list of strings', function (): void {
// Given / When
$result = StringEncoder::supportedEncodings();
// Then
expect($result)
->toBeArray()
->not->toBeEmpty()
->each->toBeString();
});
it('includes all canonical encodings', function (): void {
// Given
$expected = ['UTF-8', 'ISO-8859-15', 'Windows-1252'];
// When
$result = StringEncoder::supportedEncodings();
// Then
foreach ($expected as $encoding) {
expect($result)->toContain($encoding);
}
});
it('includes common aliases', function (): void {
// Given
$aliases = ['latin1', 'latin-1', 'latin9', 'cp1252', 'utf8', 'utf-8'];
// When
$result = StringEncoder::supportedEncodings();
// Then
foreach ($aliases as $alias) {
expect($result)->toContain($alias);
}
});
it('contains no duplicates', function (): void {
// Given
$result = StringEncoder::supportedEncodings();
// When / Then
expect($result)->toBe(array_values(array_unique($result)));
});
});
describe('encoding resolution (via toUtf8)', function (): void {
it('resolves case-insensitively', function (string $variant): void {
// Given
$input = 'hello';
// When
$result = StringEncoder::toUtf8($input, $variant);
// Then
expect($result)->toBe('hello');
})->with(['utf-8', 'UTF-8', 'Utf-8', 'UTF8', 'utf8']);
it('trims whitespace around encoding names', function (): void {
// Given
$input = 'hello';
// When
$result = StringEncoder::toUtf8($input, ' utf-8 ');
// Then
expect($result)->toBe('hello');
});
});
describe('regressions', function (): void {
it('correctly converts "Bœuf" from Windows-1252 (œ = 0x9C)', function (): void {
// Given
$input = "B\x9Cuf";
// When
$result = StringEncoder::toUtf8($input, 'Windows-1252');
// Then
expect($result)->toBe('Bœuf')
->and(mb_check_encoding($result, 'UTF-8'))->toBeTrue();
});
it('round-trips UTF-8 → identity without corruption', function (): void {
// Given
$input = 'Héllo 世界 🎵 Ÿ œ € ™';
// When
$result = StringEncoder::toUtf8($input, 'UTF-8');
// Then
expect($result)->toBe($input);
});
});