Dauerhafte Pruefung: Wanderung und linkToInstanceAdmin() bleiben gleich

Die Gleichheit zwischen der eingefrorenen Wanderungsfassung und der
Modellmethode war bisher nur mit einer geloeschten Testdatei belegt - gut
fuer heute, nicht fuer morgen. Nach dem Muster von HostTokenKeyMigrationTest
laedt der neue Test die echte Wanderungsdatei per require() und haelt sie
gegen linkToInstanceAdmin(): einmal die entschluesselte Falle direkt
geprueft (nc_username darf nie Chiffretext werden), einmal beide Wege bei
derselben Ausgangslage gegeneinander.
feat/versandtakt
nexxo 2026-08-03 21:07:19 +02:00
parent 80974108b1
commit 320e485215
1 changed files with 78 additions and 0 deletions

View File

@ -0,0 +1,78 @@
<?php
use App\Models\Customer;
use App\Models\Instance;
use App\Models\Seat;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
/**
* Laedt eine frische Instanz der Wanderung bei jedem Aufruf.
*
* `require`, nicht `require_once`: PHP wertet den `return new class ...`-
* Ausdruck bei jedem Aufruf neu aus, jeder Aufruf bekommt also seine eigene
* Instanz.
*/
function loadSeatNextcloudColumnsMigration(): object
{
return require database_path('migrations/2026_08_03_180000_add_nextcloud_columns_to_seats.php');
}
/**
* Stellt den Vor-Wanderungs-Zustand nach: RefreshDatabase hat up() schon
* einmal auf einer leeren seats-Tabelle laufen lassen, die vier Spalten
* stehen also schon. Ein zweiter Schema::table()-Aufruf auf bereits
* vorhandene Spalten wuerde scheitern hier weg damit, Zeilen bleiben.
*/
function dropSeatNextcloudColumns(): void
{
Schema::table('seats', fn ($table) => $table->dropColumn(['nc_username', 'nc_state', 'nc_error', 'nc_synced_at']));
}
it('verknuepft einen bestehenden Inhaber-Sitz mit dem entschluesselten Anmeldenamen, nicht mit dem Chiffretext', function () {
// Genau die Zusicherung, die die Verschluesselungsfalle aus der
// Nachbesserung fuer die Zukunft absichert: nc_admin_ref ist am
// Instance-Modell 'encrypted' gecastet. Faellt Crypt::decryptString()
// in der Wanderung wieder heraus, landet hier der Chiffretext statt
// 'admin' — dieser Test bricht dann, nicht erst eine echte Instanz.
$customer = Customer::factory()->create();
Instance::factory()->for($customer)->create(['nc_admin_ref' => 'admin', 'status' => 'active']);
$sitz = Seat::factory()->for($customer)->create(['role' => 'owner']);
dropSeatNextcloudColumns();
loadSeatNextcloudColumnsMigration()->up();
$zeile = DB::table('seats')->where('id', $sitz->id)->first();
expect($zeile->nc_username)->toBe('admin')
->and($zeile->nc_state)->toBe('synced');
});
it('kommt bei derselben Ausgangslage zum selben Ergebnis wie Seat::linkToInstanceAdmin()', function () {
// Der Preis des Einfrierens: zwei Fassungen derselben Auswahl koennen
// auseinanderdriften, ohne dass es jemand merkt. Die Erwartung wird
// ueber die Modellmethode eingeholt, BEVOR die Wanderung dieselbe Zeile
// ueber ihren eigenen, eingefrorenen Weg anfasst — derselbe Kunde,
// dieselbe Instanz, derselbe Sitz, zwei Wege dorthin.
//
// Verglichen werden nur nc_username/nc_state, nicht der ganze
// Datensatz: das rohe update() der Wanderung setzt updated_at nicht,
// save() ueber das Modell schon — das ist gewollt (siehe andere
// Nachbefuellungen im Bestand) und keine Abweichung, die dieser Test
// aufdecken soll.
$customer = Customer::factory()->create();
Instance::factory()->for($customer)->create(['nc_admin_ref' => 'gleicher-name', 'status' => 'active']);
$sitz = Seat::factory()->for($customer)->create(['role' => 'owner']);
$sitz->linkToInstanceAdmin();
$erwarteterUsername = $sitz->nc_username;
$erwarteterState = $sitz->nc_state;
dropSeatNextcloudColumns();
loadSeatNextcloudColumnsMigration()->up();
$zeile = DB::table('seats')->where('id', $sitz->id)->first();
expect($zeile->nc_username)->toBe($erwarteterUsername)
->and($zeile->nc_state)->toBe($erwarteterState);
});