Skip to content
Merged
32 changes: 25 additions & 7 deletions app/Http/Controllers/PoliciesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,37 @@
use App\Http\Resources\PoliciesCollection;
use App\Policy;
use Carbon\CarbonImmutable;
use Illuminate\Database\Query\Builder;
use Illuminate\Http\Request;

class PoliciesController extends Controller {
public function getCurrentPolicies(): 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');

$currentPolicies = Policy::whereIn('id', $latestPolicyIds)->get();
$currentPolicies = Policy::whereIn('id', $this->activePolicyIdsQuery($now))->get();

return new PoliciesCollection($currentPolicies);
}

public function getMissingPolicies(Request $request): PoliciesCollection {
$now = CarbonImmutable::now();

$missingPolicies = Policy::whereIn('id', $this->activePolicyIdsQuery($now))
Comment thread
tarrow marked this conversation as resolved.
->whereNotExists(function (Builder $query) use ($request): void {
$query->selectRaw('1')
->from('policy_acceptances')
->whereColumn('policy_acceptances.policy_id', 'policies.id')
->where('policy_acceptances.user_id', $request->user()->id);
})->get();

return new PoliciesCollection($missingPolicies);
}

private function activePolicyIdsQuery(CarbonImmutable $now): Builder {
return Policy::query()
->selectRaw('MAX(id) as id')
->where('active_from', '<=', $now)
->groupBy('policy_type')
->toBase();
}
}
1 change: 1 addition & 0 deletions routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,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']);
Expand Down
60 changes: 0 additions & 60 deletions tests/Http/Controllers/PoliciesControllerTest.php

This file was deleted.

179 changes: 179 additions & 0 deletions tests/Routes/PoliciesControllerTest.php
Comment thread
dati18 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<?php

namespace Tests\Routes;

use App\Policy;
use App\PolicyAcceptance;
use App\User;
use Carbon\CarbonImmutable;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Tests\TestCase;

class PoliciesControllerTest extends TestCase {
use DatabaseTransactions;

private string $currentPoliciesRoute = 'v1/policies/current';

private string $missingPoliciesRoute = 'v1/policies/missing';

private function createPolicy(string $type, CarbonImmutable $activeFrom, string $content): Policy {
return Policy::create([
'policy_type' => $type,
'active_from' => $activeFrom,
'content_vue_file' => $content,
]);
}

public function testMissingPoliciesRequiresAuthentication(): void {
$this->json('GET', $this->missingPoliciesRoute)
->assertStatus(401);
}

public function testGetCurrentPolicies(): void {
$now = CarbonImmutable::now();

// Future policy
$this->createPolicy('terms-of-use', $now->addDay(), 'terms-of-use/version-future.vue');

// Older active policy of the same type should be excluded
$this->createPolicy('hosting-policy', $now->subMonths(2), 'hosting-policy/version-1.vue');

// Active policies
$latestActiveToUPolicy = $this->createPolicy('terms-of-use', $now->subMonth(), 'terms-of-use/version-2.vue');
$latestActiveHostingPolicy = $this->createPolicy('hosting-policy', $now->subWeek(), 'hosting-policy/version-2.vue');

$response = $this->json('GET', $this->currentPoliciesRoute);

$response->assertOk();
$response->assertJsonStructure([
'items' => [
'*' => [
'metadata' => [
'policy_id',
'active_from',
'content_vue_file',
'type',
],
],
],
]);

$response->assertJsonFragment([
'policy_id' => $latestActiveToUPolicy->id,
'active_from' => $latestActiveToUPolicy->active_from->format('Y-m-d'),
]);
$response->assertJsonFragment([
'policy_id' => $latestActiveHostingPolicy->id,
'active_from' => $latestActiveHostingPolicy->active_from->format('Y-m-d'),
]);
}

public function testMissingPoliciesReturnsOnlyLatestCurrentPolicyPerType(): void {
$user = User::factory()->create();
$now = CarbonImmutable::now();

// Future policy (should be ignored because it is not active yet)
$this->createPolicy('terms-of-use', $now->addDays(10), 'terms-of-use/version-future.vue');
$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' => []]);
}
}
Loading