From ae7695690a2e3d70fa3fc417f184173f3e9d265d Mon Sep 17 00:00:00 2001 From: Dat Date: Fri, 10 Jul 2026 16:58:53 +0200 Subject: [PATCH 1/3] Add endpoint for creating a policy acceptance for a specific user --- routes/api.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/routes/api.php b/routes/api.php index f014ffbc..9690d778 100644 --- a/routes/api.php +++ b/routes/api.php @@ -28,6 +28,9 @@ $router->get('auth/login', ['uses' => 'Auth\LoginController@getLogin']); $router->delete('auth/login', ['uses' => 'Auth\LoginController@deleteLogin']); + // policy acceptances + $router->put('policy_acceptances', ['uses' => 'PolicyAcceptanceController@store']); + // user $router->group(['prefix' => 'user'], function () use ($router): void { $router->post('sendVerifyEmail', ['uses' => 'UserVerificationTokenController@createAndSendForUser']); From a721c9297b4bd1828516c82c4d69b594d5748a28 Mon Sep 17 00:00:00 2001 From: Dat Date: Fri, 10 Jul 2026 17:07:00 +0200 Subject: [PATCH 2/3] add PolicyAcceptanceController class --- .../PolicyAcceptanceController.php | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 app/Http/Controllers/PolicyAcceptanceController.php diff --git a/app/Http/Controllers/PolicyAcceptanceController.php b/app/Http/Controllers/PolicyAcceptanceController.php new file mode 100644 index 00000000..196e1bc8 --- /dev/null +++ b/app/Http/Controllers/PolicyAcceptanceController.php @@ -0,0 +1,43 @@ +validate([ + 'policy_ids' => ['required', 'array'], + 'policy_ids.*' => ['integer'], + ]); + + $policyIds = $request->input('policy_ids'); + + // Check all policy IDs exist before writing anything + $existingIds = Policy::whereIn('id', $policyIds)->pluck('id')->all(); + $missingIds = array_values(array_diff($policyIds, $existingIds)); + + if (!empty($missingIds)) { + return response()->json([ + 'success' => false, + 'message' => 'Some policy IDs do not exist.', + 'data' => ['missing_policy_ids' => $missingIds], + ], 400); + } + + $userId = $request->user()->id; + + foreach ($policyIds as $policyId) { + // Ignore if the user has already accepted this policy + PolicyAcceptance::firstOrCreate( + ['user_id' => $userId, 'policy_id' => $policyId], + ['accepted_at' => now()], + ); + } + + return response()->json(['success' => true]); + } +} From 181bc2dddfa0120515e7007cdb28917dd98fdd50 Mon Sep 17 00:00:00 2001 From: Dat Date: Mon, 13 Jul 2026 17:33:33 +0200 Subject: [PATCH 3/3] add test cases --- .../Routes/PolicyAcceptanceControllerTest.php | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 tests/Routes/PolicyAcceptanceControllerTest.php diff --git a/tests/Routes/PolicyAcceptanceControllerTest.php b/tests/Routes/PolicyAcceptanceControllerTest.php new file mode 100644 index 00000000..17c69d2b --- /dev/null +++ b/tests/Routes/PolicyAcceptanceControllerTest.php @@ -0,0 +1,117 @@ +policy_type = $type; + $policy->active_from = CarbonImmutable::now(); + $policy->content_vue_file = $type . '/version-1.vue'; + $policy->save(); + + return $policy; + } + + public function testUnauthenticatedRequestResponds401(): void { + $this->json('PUT', $this->route) + ->assertStatus(401); + } + + public function testAcceptSinglePolicy(): void { + $user = User::factory()->create(); + $policy = $this->makePolicy(); + + $this->actingAs($user, 'api') + ->json('PUT', $this->route, ['policy_ids' => [$policy->id]]) + ->assertStatus(200) + ->assertJson(['success' => true]); + + $this->assertDatabaseHas('policy_acceptances', [ + 'user_id' => $user->id, + 'policy_id' => $policy->id, + ]); + } + + public function testAcceptMultiplePolicies(): void { + $user = User::factory()->create(); + $termsOfUse = $this->makePolicy('terms-of-use'); + $hostingPolicy = $this->makePolicy('hosting-policy'); + + $this->actingAs($user, 'api') + ->json('PUT', $this->route, ['policy_ids' => [$termsOfUse->id, $hostingPolicy->id]]) + ->assertStatus(200) + ->assertJson(['success' => true]); + + $this->assertDatabaseHas('policy_acceptances', ['user_id' => $user->id, 'policy_id' => $termsOfUse->id]); + $this->assertDatabaseHas('policy_acceptances', ['user_id' => $user->id, 'policy_id' => $hostingPolicy->id]); + } + + public function testAlreadyAcceptedPolicyIsIgnored(): void { + $user = User::factory()->create(); + $policy = $this->makePolicy(); + + PolicyAcceptance::create([ + 'user_id' => $user->id, + 'policy_id' => $policy->id, + 'accepted_at' => now(), + ]); + + $this->actingAs($user, 'api') + ->json('PUT', $this->route, ['policy_ids' => [$policy->id]]) + ->assertStatus(200) + ->assertJson(['success' => true]); + + $this->assertSame(1, PolicyAcceptance::where([ + 'user_id' => $user->id, + 'policy_id' => $policy->id, + ])->count()); + } + + public function testNonExistentPolicyIdReturns400(): void { + $user = User::factory()->create(); + $policy = $this->makePolicy(); + $nonExistentId = 999999; + + $this->actingAs($user, 'api') + ->json('PUT', $this->route, ['policy_ids' => [$policy->id, $nonExistentId]]) + ->assertStatus(400) + ->assertJsonFragment(['success' => false]) + ->assertJsonFragment(['missing_policy_ids' => [$nonExistentId]]); + + // Nothing should have been written + $this->assertDatabaseMissing('policy_acceptances', [ + 'user_id' => $user->id, + 'policy_id' => $policy->id, + ]); + } + + public function testMissingPolicyIdsFieldReturns422(): void { + $user = User::factory()->create(); + + $this->actingAs($user, 'api') + ->json('PUT', $this->route, []) + ->assertStatus(422) + ->assertJsonStructure(['errors' => ['policy_ids']]); + } + + public function testPolicyIdsNotAnArrayReturns422(): void { + $user = User::factory()->create(); + + $this->actingAs($user, 'api') + ->json('PUT', $this->route, ['policy_ids' => 'not-an-array']) + ->assertStatus(422) + ->assertJsonStructure(['errors' => ['policy_ids']]); + } +}