diff --git a/app/Http/Controllers/PoliciesController.php b/app/Http/Controllers/PoliciesController.php index c1a0c449..e55e847c 100644 --- a/app/Http/Controllers/PoliciesController.php +++ b/app/Http/Controllers/PoliciesController.php @@ -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(); @@ -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); } } diff --git a/app/Http/Controllers/PolicyController.php b/app/Http/Controllers/PolicyController.php index 8527fdaf..e1816740 100644 --- a/app/Http/Controllers/PolicyController.php +++ b/app/Http/Controllers/PolicyController.php @@ -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()); + } } diff --git a/routes/api.php b/routes/api.php index c7b306f7..0c69b187 100644 --- a/routes/api.php +++ b/routes/api.php @@ -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 @@ -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']); diff --git a/tests/Routes/Policies/PoliciesControllerTest.php b/tests/Routes/Policies/PoliciesControllerTest.php index daa0f355..b7ca5b90 100644 --- a/tests/Routes/Policies/PoliciesControllerTest.php +++ b/tests/Routes/Policies/PoliciesControllerTest.php @@ -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, @@ -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', + ]); + } + } } diff --git a/tests/Routes/Policies/PolicyControllerTest.php b/tests/Routes/Policies/PolicyControllerTest.php index 56829d65..a973c769 100644 --- a/tests/Routes/Policies/PolicyControllerTest.php +++ b/tests/Routes/Policies/PolicyControllerTest.php @@ -3,6 +3,7 @@ namespace Http\Controllers; use App\Policy; +use Carbon\CarbonImmutable; use Illuminate\Foundation\Testing\DatabaseTransactions; use Tests\TestCase; @@ -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, + ]); + } }