4 - Harden overflow bucket invariants with server-side guards

This commit is contained in:
myrmidex 2026-03-19 21:49:00 +01:00
parent faff18f82b
commit d06b859652
4 changed files with 41 additions and 1 deletions

View file

@ -22,6 +22,11 @@ public function execute(
// Validate type + allocation type constraints
$this->validateTypeConstraints($type, $allocationType);
// Enforce one overflow bucket per scenario
if ($type === BucketTypeEnum::OVERFLOW && $scenario->buckets()->where('type', BucketTypeEnum::OVERFLOW->value)->exists()) {
throw new InvalidArgumentException('A scenario can only have one overflow bucket');
}
// Validate allocation value based on type
$this->validateAllocationValue($allocationType, $allocationValue);

View file

@ -80,6 +80,15 @@ public function update(Request $request, Bucket $bucket): JsonResponse
$type = BucketTypeEnum::from($validated['type']);
$allocationType = BucketAllocationTypeEnum::from($validated['allocation_type']);
// Prevent changing overflow bucket's type away from overflow
// (changing TO overflow is handled by validateBucketTypeConstraints below)
if ($bucket->type === BucketTypeEnum::OVERFLOW && $type !== BucketTypeEnum::OVERFLOW) {
return response()->json([
'message' => 'Validation failed.',
'errors' => ['type' => ['The overflow bucket\'s type cannot be changed.']],
], 422);
}
$constraintError = $this->validateBucketTypeConstraints($type, $allocationType, $bucket->scenario, $bucket);
if ($constraintError) {
return $constraintError;
@ -115,6 +124,12 @@ public function update(Request $request, Bucket $bucket): JsonResponse
*/
public function destroy(Bucket $bucket): JsonResponse
{
if ($bucket->type === BucketTypeEnum::OVERFLOW) {
return response()->json([
'message' => 'The overflow bucket cannot be deleted.',
], 422);
}
$scenarioId = $bucket->scenario_id;
$deletedPriority = $bucket->priority;

View file

@ -15,10 +15,10 @@ class BucketFactory extends Factory
{
public function definition(): array
{
// Unlimited excluded — use ->overflow() state modifier
$allocationType = $this->faker->randomElement([
BucketAllocationTypeEnum::FIXED_LIMIT,
BucketAllocationTypeEnum::PERCENTAGE,
BucketAllocationTypeEnum::UNLIMITED,
]);
return [

View file

@ -151,6 +151,26 @@ public function test_non_overflow_bucket_cannot_use_unlimited_allocation(): void
);
}
public function test_cannot_create_second_overflow_bucket(): void
{
$this->action->execute(
$this->scenario,
'First Overflow',
BucketAllocationTypeEnum::UNLIMITED,
BucketTypeEnum::OVERFLOW
);
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('A scenario can only have one overflow bucket');
$this->action->execute(
$this->scenario,
'Second Overflow',
BucketAllocationTypeEnum::UNLIMITED,
BucketTypeEnum::OVERFLOW
);
}
public function test_want_bucket_cannot_use_unlimited_allocation(): void
{
$this->expectException(InvalidArgumentException::class);