Skip to content
32 changes: 26 additions & 6 deletions app/Http/Controllers/PoliciesController.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,18 @@
use Carbon\CarbonImmutable;
use Illuminate\Database\Query\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;

class PoliciesController extends Controller {
private function activePolicyIdsQuery(CarbonImmutable $now): Builder {
return Policy::query()
->selectRaw('MAX(id) as id')
->where('active_from', '<=', $now)
->groupBy('policy_type')
->toBase();
}

public function getCurrentPolicies(): PoliciesCollection {
$now = CarbonImmutable::now();

Expand All @@ -31,11 +41,21 @@ public function getMissingPolicies(Request $request): PoliciesCollection {
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();
public function getPoliciesByType($policyType): PoliciesCollection {
$validator = Validator::make(
[
'policy_type' => $policyType,
],
[
'policy_type' => ['required', 'string', Rule::in(['terms-of-use', 'hosting-policy'])],
]
);

$validator->validate();
$validated = $validator->safe();

$policies = Policy::where('policy_type', $validated['policy_type'])->get();

return new PoliciesCollection($policies);
}
}
56 changes: 56 additions & 0 deletions app/Http/Controllers/PolicyController.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,60 @@ public function getPolicyByTypeAndActiveFrom($policyType, $activeFrom): PolicyRe

return new PolicyResource($policy);
}

public function getCurrentPolicyByType($policyType): PolicyResource {
$validator = Validator::make(
[
'policy_type' => $policyType,
],
[
'policy_type' => ['required', 'string', Rule::in(['terms-of-use', 'hosting-policy'])],
]
);
$validator->validate();
$validated = $validator->safe();

$policy = Policy::where('policy_type', $validated['policy_type'])
->where('active_from', '<=', today())
->latest('active_from')
->orderByDesc('id')
->first();

if (!$policy) {
abort(404, 'Policy not found.');
}

return new PolicyResource($policy);
}

public function getUpcomingPolicyByType($policyType): PolicyResource {
$validator = Validator::make(
[
'policy_type' => $policyType,
],
[
'policy_type' => ['required', 'string', Rule::in(['terms-of-use', 'hosting-policy'])],
]
);
$validator->validate();
$validated = $validator->safe();

$policies = Policy::where('policy_type', $validated['policy_type'])
->where(function ($query) {
$query->where('active_from', '>', today())
->orWhereNull('active_from');
})
->orderBy('active_from')
->get();

if ($policies->count() < 1) {
abort(404, 'Policy not found.');
}

if ($policies->count() > 1) {
abort(500, 'Multiple policies found.');
}

return new PolicyResource($policies->first());
}
}
8 changes: 6 additions & 2 deletions routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@
$router->post('user/resetPassword', ['uses' => 'Auth\ResetPasswordController@reset']);
$router->post('contact/sendMessage', ['uses' => 'ContactController@sendMessage']);
$router->post('complaint/sendMessage', ['uses' => 'ComplaintController@sendMessage']);
$router->get('v1/policies/current', ['uses' => 'PoliciesController@getCurrentPolicies']);
$router->get('v1/policies/{policy_type}/by_active_from/{active_from}', ['uses' => 'PolicyController@getPolicyByTypeAndActiveFrom']);

$router->post('auth/login', ['uses' => 'Auth\LoginController@postLogin'])->name('login');
// Authed
Expand Down Expand Up @@ -62,6 +60,12 @@
->middleware(AuthorisedUsersForDeletedWikiMetricsMiddleware::class);
});

$router->get('v1/policies/current', ['uses' => 'PoliciesController@getCurrentPolicies']);
$router->get('v1/policies/{policy_type}/current', ['uses' => 'PolicyController@getCurrentPolicyByType']);
$router->get('v1/policies/{policy_type}/upcoming', ['uses' => 'PolicyController@getUpcomingPolicyByType']);
$router->get('v1/policies/{policy_type}/by_active_from/{active_from}', ['uses' => 'PolicyController@getPolicyByTypeAndActiveFrom']);
$router->get('v1/policies/{policy_type}', ['uses' => 'PoliciesController@getPoliciesByType']);

$router->apiResource('wiki', 'PublicWikiController')->only(['index', 'show']);
$router->apiResource('reusePrototype', 'PublicWikiController')->only(['index']);
$router->apiResource('wikiConversionData', 'ConversionMetricController')->only(['index']);
Expand Down
84 changes: 84 additions & 0 deletions tests/Routes/Policies/PoliciesControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ class PoliciesControllerTest extends TestCase {

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

private string $allTermsOfUsePoliciesRoute = 'v1/policies/terms-of-use';

private string $allHostingPolicyPoliciesRoute = 'v1/policies/hosting-policy';

private function createPolicy(string $type, CarbonImmutable $activeFrom, string $content): Policy {
return Policy::create([
'policy_type' => $type,
Expand Down Expand Up @@ -176,4 +180,84 @@ public function testMissingPoliciesReturnsEmptyListWhenAllCurrentPoliciesAccepte
->assertStatus(200)
->assertJson(['items' => []]);
}

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

// Hosting Policies; should get excluded
$this->createPolicy('hosting-policy', $now->addDay(), 'hosting-policy/version-3.vue');
$this->createPolicy('hosting-policy', $now->subWeek(), 'hosting-policy/version-2.vue');
$this->createPolicy('hosting-policy', $now->subMonth(), 'hosting-policy/version-1.vue');

$termsOfUsePolicies = [
$this->createPolicy('terms-of-use', $now->addDay(), 'terms-of-use/version-future.vue'),
$this->createPolicy('terms-of-use', $now->subWeek(), 'terms-of-use/version-2.vue'),
$this->createPolicy('terms-of-use', $now->subMonth(), 'terms-of-use/version-1.vue'),
];

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

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

foreach ($termsOfUsePolicies as $policy) {
$response->assertJsonFragment([
'policy_id' => $policy->id,
'active_from' => $policy->active_from->format('Y-m-d'),
'content_vue_file' => $policy->content_vue_file,
'type' => 'terms-of-use',
]);
}
}

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

// Terms of Use; should get excluded
$this->createPolicy('terms-of-use', $now->addDay(), 'terms-of-use/version-3.vue');
$this->createPolicy('terms-of-use', $now->subWeek(), 'terms-of-use/version-2.vue');
$this->createPolicy('terms-of-use', $now->subMonth(), 'terms-of-use/version-1.vue');

$hostingPolicies = [
$this->createPolicy('hosting-policy', $now->addDay(), 'hosting-policy/version-3.vue'),
$this->createPolicy('hosting-policy', $now->subWeek(), 'hosting-policy/version-2.vue'),
$this->createPolicy('hosting-policy', $now->subMonth(), 'hosting-policy/version-1.vue'),
];

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

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

foreach ($hostingPolicies as $policy) {
$response->assertJsonFragment([
'policy_id' => $policy->id,
'active_from' => $policy->active_from->format('Y-m-d'),
'content_vue_file' => $policy->content_vue_file,
'type' => 'hosting-policy',
]);
}
}
}
112 changes: 112 additions & 0 deletions tests/Routes/Policies/PolicyControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Http\Controllers;

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

Expand Down Expand Up @@ -46,4 +47,115 @@ public function testMissingPolicyByTypeAndActiveFromReturns404(): void {
$request = $this->getJson('v1/policies/hosting-policy/by_active_from/2026-07-01');
$request->assertNotFound();
}

public function testUpcomingTermsOfUseMissing(): void {
Policy::query()->delete();

$request = $this->getJson('v1/policies/terms-of-use/upcoming');
$request->assertNotFound();
}

public function testUpcomingHostingPolicyMissing(): void {
Policy::query()->delete();

$request = $this->getJson('v1/policies/hosting-policy/upcoming');
$request->assertNotFound();
}

public function testUpcomingTermsOfUseMultiple(): void {
Policy::query()->delete();

$now = CarbonImmutable::now();

Policy::factory()->create([
'policy_type' => 'terms-of-use',
'active_from' => $now->addWeek(),
]);

Policy::factory()->create([
'policy_type' => 'terms-of-use',
'active_from' => null,
]);

$request = $this->getJson('v1/policies/terms-of-use/upcoming');
$request->assertServerError();
}

public function testUpcomingTermsOfUse(): void {
Policy::query()->delete();

$now = CarbonImmutable::now();

Policy::factory()->create([
'policy_type' => 'terms-of-use',
'active_from' => $now->addWeek(),
'content_vue_file' => 'terms-of-use/version-1.vue',
]);

$request = $this->getJson('v1/policies/terms-of-use/upcoming');

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

$request->assertJsonFragment([
'active_from' => $now->addWeek()->format('Y-m-d'),
'type' => 'terms-of-use',
'content_vue_file' => 'terms-of-use/version-1.vue',
]);
}

public function testCurrentTermsOfUseMissing(): void {
Policy::query()->delete();

$request = $this->getJson('v1/policies/terms-of-use/current');
$request->assertNotFound();
}

public function testCurrentHostingPolicyMissing(): void {
Policy::query()->delete();

$request = $this->getJson('v1/policies/hosting-policy/current');
$request->assertNotFound();
}

public function testCurrentTermsOfUse() {
$now = CarbonImmutable::now();

Policy::factory()->create([
'policy_type' => 'terms-of-use',
'active_from' => $now->subMonth(),
'content_vue_file' => 'terms-of-use/version-1.vue',
]);

$current = Policy::factory()->create([
'policy_type' => 'terms-of-use',
'active_from' => $now->subWeek(),
'content_vue_file' => 'terms-of-use/version-2.vue',
]);

$request = $this->getJson('v1/policies/terms-of-use/current');

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

$request->assertJsonFragment([
'active_from' => $current->active_from->format('Y-m-d'),
'type' => $current->policy_type,
'content_vue_file' => $current->content_vue_file,
]);
}
}
Loading