user = User::factory()->create(); Passport::actingAs($this->user); }); test('a preset that is not an object is rejected', function () { // `presets: [null]` used to satisfy the bare `array` rule and then blew up // in the frontend when it read `preset.id`. $this->putJson('/api/settings', [ 'settings' => ['presets' => [null]], ])->assertStatus(422)->assertJsonValidationErrors('settings.presets.0'); }); test('a preset without an id or name is rejected', function () { $this->putJson('/api/settings', [ 'settings' => ['presets' => [['settings' => []]]], ])->assertStatus(422) ->assertJsonValidationErrors(['settings.presets.0.id', 'settings.presets.0.name']); }); test('a valid preset is accepted', function () { $this->putJson('/api/settings', [ 'settings' => [ 'presets' => [[ 'id' => 'e0a1b2c3', 'name' => 'Nachtmodus', 'updatedAt' => 1726560000000, 'settings' => ['floatingLines' => ['speed' => 1.5, 'lineMode' => 'glow']], ]], 'activePresetId' => 'e0a1b2c3', ], ])->assertOk()->assertJsonPath('data.activePresetId', 'e0a1b2c3'); }); test('a preset keeps every snapshot field when stored', function () { // Nested rules make validated() drop any key without an explicit rule, so a // preset used to come back as `{floatingLines: ...}` only. $snapshot = [ 'theme' => 'dark', 'appearance' => 'dark', 'accentColor' => 'coral', 'language' => 'en', 'emotionGradientStart' => '#112233', 'emotionGradientEnd' => '#445566', 'timelineZoom' => 1.75, 'showFps' => true, 'floatingLines' => ['speed' => 1.5, 'lineMode' => 'glow'], ]; $this->putJson('/api/settings', [ 'settings' => [ 'presets' => [['id' => 'p1', 'name' => 'Nachtmodus', 'updatedAt' => 1726560000000, 'settings' => $snapshot]], ], ])->assertOk(); expect($this->user->fresh()->settings->settings['presets'][0]['settings'])->toBe($snapshot); }); test('unknown top level settings keys are rejected instead of stored', function () { $this->putJson('/api/settings', [ 'settings' => ['theme' => 'dark', 'isAdmin' => true], ])->assertStatus(422)->assertJsonValidationErrors('settings'); expect($this->user->fresh()->settings)->toBeNull(); }); test('unknown floating line keys are rejected', function () { $this->putJson('/api/settings', [ 'settings' => ['floatingLines' => ['speed' => 1.0, 'somethingElse' => 5]], ])->assertStatus(422)->assertJsonValidationErrors('settings.floatingLines'); }); test('a nested structure inside floating lines is rejected', function () { $this->putJson('/api/settings', [ 'settings' => ['floatingLines' => ['speed' => ['nested' => 'payload']]], ])->assertStatus(422)->assertJsonValidationErrors('settings.floatingLines.speed'); }); test('an oversized string inside floating lines is rejected', function () { $this->putJson('/api/settings', [ 'settings' => ['floatingLines' => ['gradientStops' => str_repeat('x', 5000)]], ])->assertStatus(422)->assertJsonValidationErrors('settings.floatingLines.gradientStops'); }); test('more presets than the schema allows are rejected', function () { $presets = collect(range(1, 51))->map(fn (int $n) => [ 'id' => "preset-{$n}", 'name' => "Preset {$n}", 'settings' => [], ])->all(); $this->putJson('/api/settings', ['settings' => ['presets' => $presets]]) ->assertStatus(422) ->assertJsonValidationErrors('settings.presets'); }); test('known floating line values still pass through unchanged', function () { $this->putJson('/api/settings', [ 'settings' => [ 'theme' => 'dark', 'language' => 'de', 'timelineZoom' => 2.5, 'showFps' => false, 'floatingLines' => [ 'speed' => 1.25, 'lineCount' => 10, 'bgCenter' => '#0a0514', 'lineMode' => 'glow', ], ], ])->assertOk() ->assertJsonPath('data.floatingLines.speed', 1.25) ->assertJsonPath('data.floatingLines.lineMode', 'glow'); });