138 lines
5.4 KiB
PHP
138 lines
5.4 KiB
PHP
<?php
|
|
|
|
use App\Services\Env\EnvFileEditor;
|
|
use App\Services\Env\InvalidEnvContentException;
|
|
use Illuminate\Support\Facades\File;
|
|
use Illuminate\Support\Str;
|
|
|
|
/**
|
|
* EnvFileEditor is the net under App\Livewire\Admin\Integrations' raw .env
|
|
* editor (Part B of the integrations-page work): validate before writing,
|
|
* back up before every write that actually happens, never touch a file that
|
|
* failed either check.
|
|
*
|
|
* Every test here points the editor at a throwaway path under
|
|
* storage/framework/testing — never base_path('.env'), the real file this dev
|
|
* machine boots from.
|
|
*/
|
|
function envEditorPath(): string
|
|
{
|
|
$path = storage_path('framework/testing/env-file-editor-'.Str::random(12).'.env');
|
|
File::ensureDirectoryExists(dirname($path));
|
|
|
|
return $path;
|
|
}
|
|
|
|
afterEach(function () {
|
|
// Backups are deliberately never pruned by the class under test (that is
|
|
// the point — see the console page's own warning) — but a test run is
|
|
// not the installation the warning is about, so it cleans up after itself.
|
|
foreach (glob(storage_path('framework/testing/env-file-editor-*')) as $leftover) {
|
|
@unlink($leftover);
|
|
}
|
|
});
|
|
|
|
it('accepts blank lines, comments, and KEY=value, and nothing else', function () {
|
|
$editor = new EnvFileEditor(envEditorPath());
|
|
|
|
expect($editor->firstInvalidLine("APP_NAME=CluPilot\n\n# a comment\nDB_HOST=mariadb\n"))->toBeNull()
|
|
->and($editor->isValid("APP_NAME=CluPilot\n"))->toBeTrue()
|
|
// A value may itself contain '=' — only the first one delimits the key.
|
|
->and($editor->isValid('MIX=a=b=c'))->toBeTrue()
|
|
// A key may have an empty value.
|
|
->and($editor->isValid('EMPTY_OK='))->toBeTrue();
|
|
|
|
// Neither blank, nor a comment, nor KEY=value — the exact line named in
|
|
// the task, and 1-indexed so it can be shown to an operator directly.
|
|
expect($editor->firstInvalidLine("APP_NAME=CluPilot\nthis is not env syntax\nDB_HOST=mariadb\n"))->toBe(2)
|
|
->and($editor->isValid("APP_NAME=CluPilot\nthis is not env syntax\n"))->toBeFalse();
|
|
});
|
|
|
|
it('refuses to write invalid content, and never touches the file', function () {
|
|
// Mutation target: an invalid file is refused rather than written.
|
|
$path = envEditorPath();
|
|
File::put($path, "ORIGINAL=untouched\n");
|
|
$editor = new EnvFileEditor($path);
|
|
|
|
expect(fn () => $editor->write("ORIGINAL=untouched\nnot env syntax at all\n"))
|
|
->toThrow(InvalidEnvContentException::class);
|
|
|
|
// The file on disk is EXACTLY what it was before the rejected write —
|
|
// not merely "still has ORIGINAL in it somewhere".
|
|
expect(File::get($path))->toBe("ORIGINAL=untouched\n")
|
|
// And no backup was created either: nothing was about to change.
|
|
->and(glob($path.'.bak-*'))->toBe([]);
|
|
});
|
|
|
|
it('refuses an empty file even though it is syntactically valid', function () {
|
|
$path = envEditorPath();
|
|
File::put($path, "ORIGINAL=untouched\n");
|
|
$editor = new EnvFileEditor($path);
|
|
|
|
expect(fn () => $editor->write(''))->toThrow(InvalidEnvContentException::class)
|
|
->and(fn () => $editor->write(" \n\n"))->toThrow(InvalidEnvContentException::class);
|
|
|
|
expect(File::get($path))->toBe("ORIGINAL=untouched\n");
|
|
});
|
|
|
|
it('backs up the previous content before writing the new content', function () {
|
|
// Mutation target: a backup really exists after a save.
|
|
$path = envEditorPath();
|
|
File::put($path, "OLD_KEY=old-value\n");
|
|
$editor = new EnvFileEditor($path);
|
|
|
|
$backupPath = $editor->write("NEW_KEY=new-value\n");
|
|
|
|
expect(File::exists($backupPath))->toBeTrue()
|
|
->and(File::get($backupPath))->toBe("OLD_KEY=old-value\n")
|
|
// The backup sits BESIDE the file, under the same name plus a suffix
|
|
// — not in some other directory an operator would have to be told
|
|
// about separately.
|
|
->and(dirname($backupPath))->toBe(dirname($path))
|
|
->and(File::get($path))->toBe("NEW_KEY=new-value\n");
|
|
});
|
|
|
|
it('does not collide when two writes land in the same second', function () {
|
|
$path = envEditorPath();
|
|
File::put($path, "V=1\n");
|
|
$editor = new EnvFileEditor($path);
|
|
|
|
$first = $editor->write("V=2\n");
|
|
$second = $editor->write("V=3\n");
|
|
|
|
expect($first)->not->toBe($second)
|
|
->and(File::exists($first))->toBeTrue()
|
|
->and(File::exists($second))->toBeTrue();
|
|
});
|
|
|
|
it('creates the file when none existed before, without trying to back up something that was never there', function () {
|
|
// Codex review, P2: File::copy() on a source that does not exist throws,
|
|
// which made the first save on an installation with no .env yet fail
|
|
// outright instead of creating one.
|
|
$path = envEditorPath();
|
|
File::delete($path); // envEditorPath() seeds it — deliberately undone here.
|
|
$editor = new EnvFileEditor($path);
|
|
|
|
$backup = $editor->write("FRESH=value\n");
|
|
|
|
expect($backup)->toBe('')
|
|
->and(File::get($path))->toBe("FRESH=value\n");
|
|
});
|
|
|
|
it('answers empty rather than throwing when asked to back up a file that does not exist', function () {
|
|
$path = envEditorPath();
|
|
File::delete($path);
|
|
|
|
expect((new EnvFileEditor($path))->backup())->toBe('');
|
|
});
|
|
|
|
it('defaults to base_path(.env) when no path is given', function () {
|
|
expect((new EnvFileEditor)->path())->toBe(base_path('.env'));
|
|
});
|
|
|
|
it('reads empty rather than failing when the file does not exist yet', function () {
|
|
$editor = new EnvFileEditor(envEditorPath());
|
|
|
|
expect($editor->read())->toBe('');
|
|
});
|