File: /home/xedaptot/be.naniguide.com/tests/Feature/LanguageUploadFeatureTest.php
<?php
/**
* Language::upload() — safe per-file ZIP upload flow.
*
* Covers:
* - ZIP missing a registered file → restored from master
* - ZIP with extra unregistered files → ignored, no errors
* - ZIP with a PHP-corrupt file → error reported, existing file kept
* - ZIP with extra keys / missing keys → locked to master, keys dropped/restored
* - Non-ZIP upload → validator fails with a clear message
*/
use App\Model\Language;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\File;
uses(Tests\TestCase::class);
/**
* Subclass that lets each test control the target dir + registered files,
* without touching the real Hook registry or resource_path('lang/...').
*/
class TestableLanguage extends Language
{
public $__testDir = null;
public $__testFiles = [];
public function languageDir()
{
return $this->__testDir . '/lang-zz';
}
public function getAllLanguageFiles()
{
return $this->__testFiles;
}
}
function makeZip(string $path, array $entries): void
{
$zip = new ZipArchive();
$zip->open($path, ZipArchive::CREATE);
foreach ($entries as $name => $content) {
$zip->addFromString($name, $content);
}
$zip->close();
}
function makeTestLanguage(string $testDir): TestableLanguage
{
$lang = new TestableLanguage();
$lang->code = 'zz';
$lang->__testDir = $testDir;
$lang->__testFiles = [
'messages' => [
'id' => 'messages',
'type' => 'default',
'path' => $testDir . '/lang-zz/messages.php',
'file_title' => 'messages.php',
'master_translation_file' => $testDir . '/master/messages.php',
],
'validation' => [
'id' => 'validation',
'type' => 'default',
'path' => $testDir . '/lang-zz/validation.php',
'file_title' => 'validation.php',
'master_translation_file' => $testDir . '/master/validation.php',
],
];
return $lang;
}
function makeUploadRequest(string $zipPath): Request
{
$file = new UploadedFile(
$zipPath,
'package.zip',
'application/zip',
null,
true // test mode — bypass is_uploaded_file() check
);
$request = Request::create('/upload', 'POST');
$request->files->set('file', $file);
return $request;
}
beforeEach(function () {
$this->testDir = storage_path('tmp/lang-upload-test-' . uniqid());
File::makeDirectory($this->testDir, 0755, true);
File::makeDirectory($this->testDir . '/lang-zz', 0755, true);
File::makeDirectory($this->testDir . '/master', 0755, true);
File::makeDirectory($this->testDir . '/zips', 0755, true);
// Master (authoritative) files
File::put(
$this->testDir . '/master/messages.php',
"<?php return array('hello' => 'Hello', 'bye' => 'Goodbye') ?>"
);
File::put(
$this->testDir . '/master/validation.php',
"<?php return array('required' => 'Field is required') ?>"
);
// Existing target translations (what's currently in lang/zz/)
File::put(
$this->testDir . '/lang-zz/messages.php',
"<?php return array('hello' => 'Hola', 'bye' => 'Adios') ?>"
);
File::put(
$this->testDir . '/lang-zz/validation.php',
"<?php return array('required' => 'Campo requerido') ?>"
);
});
afterEach(function () {
if (isset($this->testDir) && is_dir($this->testDir)) {
File::deleteDirectory($this->testDir);
}
});
test('upload: ZIP missing a registered file restores it from master', function () {
$zipPath = $this->testDir . '/zips/missing.zip';
makeZip($zipPath, [
// only messages.php — validation.php intentionally missing
'messages.php' => "<?php return array('hello' => 'Salut', 'bye' => 'Au revoir') ?>",
]);
$lang = makeTestLanguage($this->testDir);
$validator = $lang->upload(makeUploadRequest($zipPath));
expect($validator->fails())->toBeFalse();
// messages.php — values updated from ZIP
$messages = include $this->testDir . '/lang-zz/messages.php';
expect($messages['hello'])->toBe('Salut');
expect($messages['bye'])->toBe('Au revoir');
// validation.php — missing from ZIP, restored from master
$validation = include $this->testDir . '/lang-zz/validation.php';
expect($validation['required'])->toBe('Field is required');
// Report
$report = $lang->uploadReport;
expect($report['summary']['total'])->toBe(2);
expect($report['summary']['restored_missing'])->toBe(1);
expect($report['summary']['errors'])->toBe(0);
$byId = collect($report['files'])->keyBy('id');
expect($byId['messages']['status'])->toBe('ok');
expect($byId['validation']['status'])->toBe('restored_missing');
});
test('upload: extra unregistered files in ZIP are ignored without error', function () {
$zipPath = $this->testDir . '/zips/extra.zip';
makeZip($zipPath, [
'messages.php' => "<?php return array('hello' => 'Hi', 'bye' => 'Bye') ?>",
'validation.php' => "<?php return array('required' => 'Required') ?>",
// extra files not in the registered list — should be ignored silently
'unregistered.php' => "<?php return array('x' => 'y') ?>",
'readme.txt' => "this is not a translation file",
'subdir/other.php' => "<?php return array('foo' => 'bar') ?>",
]);
$lang = makeTestLanguage($this->testDir);
$validator = $lang->upload(makeUploadRequest($zipPath));
expect($validator->fails())->toBeFalse();
// Both registered files updated
$messages = include $this->testDir . '/lang-zz/messages.php';
expect($messages['hello'])->toBe('Hi');
$validation = include $this->testDir . '/lang-zz/validation.php';
expect($validation['required'])->toBe('Required');
// Extra files NOT copied into lang-zz
expect(file_exists($this->testDir . '/lang-zz/unregistered.php'))->toBeFalse();
expect(file_exists($this->testDir . '/lang-zz/readme.txt'))->toBeFalse();
expect(file_exists($this->testDir . '/lang-zz/subdir/other.php'))->toBeFalse();
// Report
$report = $lang->uploadReport;
expect($report['summary']['total'])->toBe(2);
expect($report['summary']['ok'])->toBe(2);
expect($report['summary']['errors'])->toBe(0);
});
test('upload: PHP-corrupt file is reported as error and existing file kept untouched', function () {
$zipPath = $this->testDir . '/zips/corrupt.zip';
makeZip($zipPath, [
// broken PHP — unterminated array, will throw ParseError on include
'messages.php' => "<?php return array('hello' =>",
// valid
'validation.php' => "<?php return array('required' => 'Needed') ?>",
]);
$lang = makeTestLanguage($this->testDir);
$validator = $lang->upload(makeUploadRequest($zipPath));
expect($validator->fails())->toBeFalse();
// messages.php — kept at original value because the upload was corrupt
$messages = include $this->testDir . '/lang-zz/messages.php';
expect($messages['hello'])->toBe('Hola');
expect($messages['bye'])->toBe('Adios');
// validation.php — successfully updated
$validation = include $this->testDir . '/lang-zz/validation.php';
expect($validation['required'])->toBe('Needed');
// Report
$report = $lang->uploadReport;
expect($report['summary']['errors'])->toBe(1);
expect($report['summary']['ok'])->toBe(1);
$byId = collect($report['files'])->keyBy('id');
expect($byId['messages']['status'])->toBe('error');
expect($byId['messages']['details']['reason'])->toContain('syntax error');
expect($byId['validation']['status'])->toBe('ok');
});
test('upload: extra keys dropped and missing keys restored from master', function () {
$zipPath = $this->testDir . '/zips/locked.zip';
makeZip($zipPath, [
// 'bye' missing, 'extra' is not in master — should be dropped, 'bye' restored
'messages.php' => "<?php return array('hello' => 'Ciao', 'extra' => 'X') ?>",
'validation.php' => "<?php return array('required' => 'Richiesto') ?>",
]);
$lang = makeTestLanguage($this->testDir);
$validator = $lang->upload(makeUploadRequest($zipPath));
expect($validator->fails())->toBeFalse();
$messages = include $this->testDir . '/lang-zz/messages.php';
expect($messages)->toHaveKey('hello');
expect($messages)->toHaveKey('bye');
expect($messages)->not->toHaveKey('extra');
expect($messages['hello'])->toBe('Ciao'); // user value kept
expect($messages['bye'])->toBe('Goodbye'); // restored from master
$report = $lang->uploadReport;
$byId = collect($report['files'])->keyBy('id');
expect($byId['messages']['status'])->toBe('locked');
expect($byId['messages']['details']['extra_keys_dropped'])->toBe(1);
expect($byId['messages']['details']['missing_keys_restored'])->toBe(1);
expect($byId['validation']['status'])->toBe('ok');
expect($report['summary']['locked'])->toBe(1);
});
test('upload: non-zip upload fails validation with a clear message', function () {
$fakePath = $this->testDir . '/zips/not-a-zip.zip';
File::put($fakePath, 'this is plain text, not a zip file');
$lang = makeTestLanguage($this->testDir);
$validator = $lang->upload(makeUploadRequest($fakePath));
expect($validator->fails())->toBeTrue();
$msg = $validator->errors()->first();
expect($msg)->toBeString();
expect(strtolower($msg))->toContain('zip');
// Nothing touched
$messages = include $this->testDir . '/lang-zz/messages.php';
expect($messages['hello'])->toBe('Hola');
});