From 3c31e8bd4746fab1b5c60579935fab21501b87b6 Mon Sep 17 00:00:00 2001 From: Dat Date: Fri, 17 Jul 2026 16:54:34 +0200 Subject: [PATCH 1/3] Added controller method that returns only current policies not accepted by user Bug: T432337 --- app/Http/Controllers/PoliciesController.php | 22 +++++++++++++++++++++ routes/api.php | 1 + 2 files changed, 23 insertions(+) diff --git a/app/Http/Controllers/PoliciesController.php b/app/Http/Controllers/PoliciesController.php index 1a3dfd65..2d67ee6c 100644 --- a/app/Http/Controllers/PoliciesController.php +++ b/app/Http/Controllers/PoliciesController.php @@ -4,7 +4,9 @@ use App\Http\Resources\PoliciesCollection; use App\Policy; +use App\PolicyAcceptance; use Carbon\CarbonImmutable; +use Illuminate\Http\Request; class PoliciesController extends Controller { public function getCurrentPolicies(): PoliciesCollection { @@ -20,4 +22,24 @@ public function getCurrentPolicies(): PoliciesCollection { return new PoliciesCollection($currentPolicies); } + + public function getMissingPolicies(Request $request): PoliciesCollection { + $now = CarbonImmutable::now(); + + // This works based on the assumption that the latest policy has the highest id given that id is AUTO_INCREMENT + $latestPolicyIds = Policy::where('active_from', '<', $now) + ->selectRaw('MAX(id) as id') + ->groupBy('policy_type') + ->pluck('id'); + + $acceptedPolicyIds = PolicyAcceptance::where('user_id', $request->user()->id) + ->whereIn('policy_id', $latestPolicyIds) + ->pluck('policy_id'); + + $missingPolicies = Policy::whereIn('id', $latestPolicyIds) + ->whereNotIn('id', $acceptedPolicyIds) + ->get(); + + return new PoliciesCollection($missingPolicies); + } } diff --git a/routes/api.php b/routes/api.php index 125bc994..9f8c4acb 100644 --- a/routes/api.php +++ b/routes/api.php @@ -28,6 +28,7 @@ $router->group(['middleware' => ['auth:api']], function () use ($router): void { $router->get('auth/login', ['uses' => 'Auth\LoginController@getLogin']); $router->delete('auth/login', ['uses' => 'Auth\LoginController@deleteLogin']); + $router->get('v1/policies/missing', ['uses' => 'PoliciesController@getMissingPolicies']); // policy acceptances $router->put('v1/policy_acceptances', ['uses' => 'PolicyAcceptanceController@store']); From 191fac253f2dd4d7fdedb84081f938be0d3cfc79 Mon Sep 17 00:00:00 2001 From: Dat Date: Mon, 20 Jul 2026 15:38:20 +0200 Subject: [PATCH 2/3] Add test class --- tests/Routes/PoliciesControllerTest.php | 136 ++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 tests/Routes/PoliciesControllerTest.php diff --git a/tests/Routes/PoliciesControllerTest.php b/tests/Routes/PoliciesControllerTest.php new file mode 100644 index 00000000..19fbfd9e --- /dev/null +++ b/tests/Routes/PoliciesControllerTest.php @@ -0,0 +1,136 @@ + $type, + 'active_from' => $activeFrom, + 'content_vue_file' => $content, + ]); + } + + public function testMissingPoliciesRequiresAuthentication(): void { + $this->json('GET', $this->missingPoliciesRoute) + ->assertStatus(401); + } + + public function testMissingPoliciesReturnsOnlyLatestCurrentPolicyPerType(): void { + $user = User::factory()->create(); + $now = CarbonImmutable::now(); + + $olderTerms = $this->createPolicy('terms-of-use', $now->subDays(10), 'terms-of-use/version-1.vue'); + $latestTerms = $this->createPolicy('terms-of-use', $now->subDays(1), 'terms-of-use/version-2.vue'); + $this->createPolicy('terms-of-use', $now->addDays(1), 'terms-of-use/version-3.vue'); + + $olderHosting = $this->createPolicy('hosting-policy', $now->subDays(20), 'hosting-policy/version-1.vue'); + $latestHosting = $this->createPolicy('hosting-policy', $now->subDays(2), 'hosting-policy/version-2.vue'); + + $response = $this->actingAs($user, 'api') + ->json('GET', $this->missingPoliciesRoute) + ->assertStatus(200) + ->assertJsonStructure([ + 'items' => [ + ['metadata' => ['policy_id', 'type', 'active_from', 'content_vue_file']], + ], + ]); + + $items = collect($response->json('items')); + + $this->assertCount(2, $items); + + $this->assertTrue($items->contains(function (array $item) use ($latestTerms): bool { + return $item['metadata']['policy_id'] === $latestTerms->id + && $item['metadata']['type'] === 'terms-of-use' + && $item['metadata']['active_from'] === $latestTerms->active_from->format('Y-m-d') + && $item['metadata']['content_vue_file'] === 'terms-of-use/version-2.vue'; + })); + + $this->assertTrue($items->contains(function (array $item) use ($latestHosting): bool { + return $item['metadata']['policy_id'] === $latestHosting->id + && $item['metadata']['type'] === 'hosting-policy' + && $item['metadata']['active_from'] === $latestHosting->active_from->format('Y-m-d') + && $item['metadata']['content_vue_file'] === 'hosting-policy/version-2.vue'; + })); + + $this->assertFalse($items->contains(function (array $item) use ($olderTerms): bool { + return $item['metadata']['policy_id'] === $olderTerms->id; + })); + + $this->assertFalse($items->contains(function (array $item) use ($olderHosting): bool { + return $item['metadata']['policy_id'] === $olderHosting->id; + })); + + $this->assertCount(1, $items->where('metadata.type', 'terms-of-use')); + $this->assertCount(1, $items->where('metadata.type', 'hosting-policy')); + } + + public function testMissingPoliciesExcludesAlreadyAcceptedPoliciesForCurrentUser(): void { + $user = User::factory()->create(); + $anotherUser = User::factory()->create(); + $now = CarbonImmutable::now(); + + $terms = $this->createPolicy('terms-of-use', $now->subDays(1), 'terms-of-use/version-2.vue'); + $hosting = $this->createPolicy('hosting-policy', $now->subDays(1), 'hosting-policy/version-1.vue'); + + PolicyAcceptance::create([ + 'user_id' => $user->id, + 'policy_id' => $terms->id, + 'accepted_at' => $now, + ]); + + // Acceptance by another user must not affect current user response + PolicyAcceptance::create([ + 'user_id' => $anotherUser->id, + 'policy_id' => $hosting->id, + 'accepted_at' => $now, + ]); + + $response = $this->actingAs($user, 'api') + ->json('GET', $this->missingPoliciesRoute) + ->assertStatus(200); + + $items = collect($response->json('items')); + + $this->assertCount(1, $items); + $this->assertSame($hosting->id, $items->first()['metadata']['policy_id']); + $this->assertSame('hosting-policy', $items->first()['metadata']['type']); + } + + public function testMissingPoliciesReturnsEmptyListWhenAllCurrentPoliciesAccepted(): void { + $user = User::factory()->create(); + $now = CarbonImmutable::now(); + + $terms = $this->createPolicy('terms-of-use', $now->subDays(1), 'terms-of-use/version-2.vue'); + $hosting = $this->createPolicy('hosting-policy', $now->subDays(1), 'hosting-policy/version-1.vue'); + + PolicyAcceptance::create([ + 'user_id' => $user->id, + 'policy_id' => $terms->id, + 'accepted_at' => $now, + ]); + + PolicyAcceptance::create([ + 'user_id' => $user->id, + 'policy_id' => $hosting->id, + 'accepted_at' => $now, + ]); + + $this->actingAs($user, 'api') + ->json('GET', $this->missingPoliciesRoute) + ->assertStatus(200) + ->assertJson(['items' => []]); + } +} From ea4924f0ed6da72fffa9fd0307cf14ce0be47ed2 Mon Sep 17 00:00:00 2001 From: Dat Date: Mon, 20 Jul 2026 16:22:24 +0200 Subject: [PATCH 3/3] change query condition and variables' names --- app/Http/Controllers/PoliciesController.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/PoliciesController.php b/app/Http/Controllers/PoliciesController.php index 2d67ee6c..17c0558e 100644 --- a/app/Http/Controllers/PoliciesController.php +++ b/app/Http/Controllers/PoliciesController.php @@ -26,17 +26,17 @@ public function getCurrentPolicies(): PoliciesCollection { public function getMissingPolicies(Request $request): PoliciesCollection { $now = CarbonImmutable::now(); - // This works based on the assumption that the latest policy has the highest id given that id is AUTO_INCREMENT - $latestPolicyIds = Policy::where('active_from', '<', $now) + // This works based on the assumption that the active policy has the highest id given that id is AUTO_INCREMENT + $activePolicyIds = Policy::where('active_from', '<=', $now) ->selectRaw('MAX(id) as id') ->groupBy('policy_type') ->pluck('id'); $acceptedPolicyIds = PolicyAcceptance::where('user_id', $request->user()->id) - ->whereIn('policy_id', $latestPolicyIds) + ->whereIn('policy_id', $activePolicyIds) ->pluck('policy_id'); - $missingPolicies = Policy::whereIn('id', $latestPolicyIds) + $missingPolicies = Policy::whereIn('id', $activePolicyIds) ->whereNotIn('id', $acceptedPolicyIds) ->get();