Skip to content

Commit fbc21c0

Browse files
authored
endpoints for upcoming, current and all policies of a type (#1214)
adds routes ``` /v1/policies/{policy_type}/current /v1/policies/{policy_type}/upcoming /v1/policies/{policy_type} ``` --- Two observations: **1. api route order matters** this one works: ```php $router->get('v1/policies/current', ['uses' => 'PoliciesController@getCurrentPolicies']); $router->get('v1/policies/{policy_type}', ['uses' => 'PoliciesController@getPoliciesByType']); ``` this one doesnt: `v1/policies/{policy_type}` swallows the later ones: ```php $router->get('v1/policies/{policy_type}', ['uses' => 'PoliciesController@getPoliciesByType']); $router->get('v1/policies/current', ['uses' => 'PoliciesController@getCurrentPolicies']); ``` **2. Explicit nulling of `active_from`** Due to our PolicyFactory implementation we *need* to set `active_from` to `null` if we want it to be not filled. this works: ```php Policy::factory()->create([ 'policy_type' => 'terms-of-use', 'active_from' => null, ]); ``` this fills with `'active_from' => now()` ```php Policy::factory()->create([ 'policy_type' => 'terms-of-use', ]); ``` https://phabricator.wikimedia.org/T432714
1 parent fdd2d7a commit fbc21c0

5 files changed

Lines changed: 284 additions & 8 deletions

File tree

app/Http/Controllers/PoliciesController.php

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,18 @@
77
use Carbon\CarbonImmutable;
88
use Illuminate\Database\Query\Builder;
99
use Illuminate\Http\Request;
10+
use Illuminate\Support\Facades\Validator;
11+
use Illuminate\Validation\Rule;
1012

1113
class PoliciesController extends Controller {
14+
private function activePolicyIdsQuery(CarbonImmutable $now): Builder {
15+
return Policy::query()
16+
->selectRaw('MAX(id) as id')
17+
->where('active_from', '<=', $now)
18+
->groupBy('policy_type')
19+
->toBase();
20+
}
21+
1222
public function getCurrentPolicies(): PoliciesCollection {
1323
$now = CarbonImmutable::now();
1424

@@ -31,11 +41,21 @@ public function getMissingPolicies(Request $request): PoliciesCollection {
3141
return new PoliciesCollection($missingPolicies);
3242
}
3343

34-
private function activePolicyIdsQuery(CarbonImmutable $now): Builder {
35-
return Policy::query()
36-
->selectRaw('MAX(id) as id')
37-
->where('active_from', '<=', $now)
38-
->groupBy('policy_type')
39-
->toBase();
44+
public function getPoliciesByType($policyType): PoliciesCollection {
45+
$validator = Validator::make(
46+
[
47+
'policy_type' => $policyType,
48+
],
49+
[
50+
'policy_type' => ['required', 'string', Rule::in(['terms-of-use', 'hosting-policy'])],
51+
]
52+
);
53+
54+
$validator->validate();
55+
$validated = $validator->safe();
56+
57+
$policies = Policy::where('policy_type', $validated['policy_type'])->get();
58+
59+
return new PoliciesCollection($policies);
4060
}
4161
}

app/Http/Controllers/PolicyController.php

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,60 @@ public function getPolicyByTypeAndActiveFrom($policyType, $activeFrom): PolicyRe
3434

3535
return new PolicyResource($policy);
3636
}
37+
38+
public function getCurrentPolicyByType($policyType): PolicyResource {
39+
$validator = Validator::make(
40+
[
41+
'policy_type' => $policyType,
42+
],
43+
[
44+
'policy_type' => ['required', 'string', Rule::in(['terms-of-use', 'hosting-policy'])],
45+
]
46+
);
47+
$validator->validate();
48+
$validated = $validator->safe();
49+
50+
$policy = Policy::where('policy_type', $validated['policy_type'])
51+
->where('active_from', '<=', today())
52+
->latest('active_from')
53+
->orderByDesc('id')
54+
->first();
55+
56+
if (!$policy) {
57+
abort(404, 'Policy not found.');
58+
}
59+
60+
return new PolicyResource($policy);
61+
}
62+
63+
public function getUpcomingPolicyByType($policyType): PolicyResource {
64+
$validator = Validator::make(
65+
[
66+
'policy_type' => $policyType,
67+
],
68+
[
69+
'policy_type' => ['required', 'string', Rule::in(['terms-of-use', 'hosting-policy'])],
70+
]
71+
);
72+
$validator->validate();
73+
$validated = $validator->safe();
74+
75+
$policies = Policy::where('policy_type', $validated['policy_type'])
76+
->where(function ($query) {
77+
$query->where('active_from', '>', today())
78+
->orWhereNull('active_from');
79+
})
80+
->orderBy('active_from')
81+
->get();
82+
83+
if ($policies->count() < 1) {
84+
abort(404, 'Policy not found.');
85+
}
86+
87+
if ($policies->count() > 1) {
88+
abort(500, 'Multiple policies found.');
89+
}
90+
91+
return new PolicyResource($policies->first());
92+
}
3793
}

routes/api.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@
2121
$router->post('user/resetPassword', ['uses' => 'Auth\ResetPasswordController@reset']);
2222
$router->post('contact/sendMessage', ['uses' => 'ContactController@sendMessage']);
2323
$router->post('complaint/sendMessage', ['uses' => 'ComplaintController@sendMessage']);
24-
$router->get('v1/policies/current', ['uses' => 'PoliciesController@getCurrentPolicies']);
25-
$router->get('v1/policies/{policy_type}/by_active_from/{active_from}', ['uses' => 'PolicyController@getPolicyByTypeAndActiveFrom']);
2624

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

63+
$router->get('v1/policies/current', ['uses' => 'PoliciesController@getCurrentPolicies']);
64+
$router->get('v1/policies/{policy_type}/current', ['uses' => 'PolicyController@getCurrentPolicyByType']);
65+
$router->get('v1/policies/{policy_type}/upcoming', ['uses' => 'PolicyController@getUpcomingPolicyByType']);
66+
$router->get('v1/policies/{policy_type}/by_active_from/{active_from}', ['uses' => 'PolicyController@getPolicyByTypeAndActiveFrom']);
67+
$router->get('v1/policies/{policy_type}', ['uses' => 'PoliciesController@getPoliciesByType']);
68+
6569
$router->apiResource('wiki', 'PublicWikiController')->only(['index', 'show']);
6670
$router->apiResource('reusePrototype', 'PublicWikiController')->only(['index']);
6771
$router->apiResource('wikiConversionData', 'ConversionMetricController')->only(['index']);

tests/Routes/Policies/PoliciesControllerTest.php

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ class PoliciesControllerTest extends TestCase {
1616

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

19+
private string $allTermsOfUsePoliciesRoute = 'v1/policies/terms-of-use';
20+
21+
private string $allHostingPolicyPoliciesRoute = 'v1/policies/hosting-policy';
22+
1923
private function createPolicy(string $type, CarbonImmutable $activeFrom, string $content): Policy {
2024
return Policy::create([
2125
'policy_type' => $type,
@@ -176,4 +180,84 @@ public function testMissingPoliciesReturnsEmptyListWhenAllCurrentPoliciesAccepte
176180
->assertStatus(200)
177181
->assertJson(['items' => []]);
178182
}
183+
184+
public function testGetAllTermsOfUsePolicies(): void {
185+
$now = CarbonImmutable::now();
186+
187+
// Hosting Policies; should get excluded
188+
$this->createPolicy('hosting-policy', $now->addDay(), 'hosting-policy/version-3.vue');
189+
$this->createPolicy('hosting-policy', $now->subWeek(), 'hosting-policy/version-2.vue');
190+
$this->createPolicy('hosting-policy', $now->subMonth(), 'hosting-policy/version-1.vue');
191+
192+
$termsOfUsePolicies = [
193+
$this->createPolicy('terms-of-use', $now->addDay(), 'terms-of-use/version-future.vue'),
194+
$this->createPolicy('terms-of-use', $now->subWeek(), 'terms-of-use/version-2.vue'),
195+
$this->createPolicy('terms-of-use', $now->subMonth(), 'terms-of-use/version-1.vue'),
196+
];
197+
198+
$response = $this->json('GET', $this->allTermsOfUsePoliciesRoute);
199+
200+
$response->assertOk();
201+
$response->assertJsonStructure([
202+
'items' => [
203+
'*' => [
204+
'metadata' => [
205+
'policy_id',
206+
'active_from',
207+
'content_vue_file',
208+
'type',
209+
],
210+
],
211+
],
212+
]);
213+
214+
foreach ($termsOfUsePolicies as $policy) {
215+
$response->assertJsonFragment([
216+
'policy_id' => $policy->id,
217+
'active_from' => $policy->active_from->format('Y-m-d'),
218+
'content_vue_file' => $policy->content_vue_file,
219+
'type' => 'terms-of-use',
220+
]);
221+
}
222+
}
223+
224+
public function testGetAllHostingPolicies(): void {
225+
$now = CarbonImmutable::now();
226+
227+
// Terms of Use; should get excluded
228+
$this->createPolicy('terms-of-use', $now->addDay(), 'terms-of-use/version-3.vue');
229+
$this->createPolicy('terms-of-use', $now->subWeek(), 'terms-of-use/version-2.vue');
230+
$this->createPolicy('terms-of-use', $now->subMonth(), 'terms-of-use/version-1.vue');
231+
232+
$hostingPolicies = [
233+
$this->createPolicy('hosting-policy', $now->addDay(), 'hosting-policy/version-3.vue'),
234+
$this->createPolicy('hosting-policy', $now->subWeek(), 'hosting-policy/version-2.vue'),
235+
$this->createPolicy('hosting-policy', $now->subMonth(), 'hosting-policy/version-1.vue'),
236+
];
237+
238+
$response = $this->json('GET', $this->allHostingPolicyPoliciesRoute);
239+
240+
$response->assertOk();
241+
$response->assertJsonStructure([
242+
'items' => [
243+
'*' => [
244+
'metadata' => [
245+
'policy_id',
246+
'active_from',
247+
'content_vue_file',
248+
'type',
249+
],
250+
],
251+
],
252+
]);
253+
254+
foreach ($hostingPolicies as $policy) {
255+
$response->assertJsonFragment([
256+
'policy_id' => $policy->id,
257+
'active_from' => $policy->active_from->format('Y-m-d'),
258+
'content_vue_file' => $policy->content_vue_file,
259+
'type' => 'hosting-policy',
260+
]);
261+
}
262+
}
179263
}

tests/Routes/Policies/PolicyControllerTest.php

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace Http\Controllers;
44

55
use App\Policy;
6+
use Carbon\CarbonImmutable;
67
use Illuminate\Foundation\Testing\DatabaseTransactions;
78
use Tests\TestCase;
89

@@ -46,4 +47,115 @@ public function testMissingPolicyByTypeAndActiveFromReturns404(): void {
4647
$request = $this->getJson('v1/policies/hosting-policy/by_active_from/2026-07-01');
4748
$request->assertNotFound();
4849
}
50+
51+
public function testUpcomingTermsOfUseMissing(): void {
52+
Policy::query()->delete();
53+
54+
$request = $this->getJson('v1/policies/terms-of-use/upcoming');
55+
$request->assertNotFound();
56+
}
57+
58+
public function testUpcomingHostingPolicyMissing(): void {
59+
Policy::query()->delete();
60+
61+
$request = $this->getJson('v1/policies/hosting-policy/upcoming');
62+
$request->assertNotFound();
63+
}
64+
65+
public function testUpcomingTermsOfUseMultiple(): void {
66+
Policy::query()->delete();
67+
68+
$now = CarbonImmutable::now();
69+
70+
Policy::factory()->create([
71+
'policy_type' => 'terms-of-use',
72+
'active_from' => $now->addWeek(),
73+
]);
74+
75+
Policy::factory()->create([
76+
'policy_type' => 'terms-of-use',
77+
'active_from' => null,
78+
]);
79+
80+
$request = $this->getJson('v1/policies/terms-of-use/upcoming');
81+
$request->assertServerError();
82+
}
83+
84+
public function testUpcomingTermsOfUse(): void {
85+
Policy::query()->delete();
86+
87+
$now = CarbonImmutable::now();
88+
89+
Policy::factory()->create([
90+
'policy_type' => 'terms-of-use',
91+
'active_from' => $now->addWeek(),
92+
'content_vue_file' => 'terms-of-use/version-1.vue',
93+
]);
94+
95+
$request = $this->getJson('v1/policies/terms-of-use/upcoming');
96+
97+
$request->assertOk();
98+
$request->assertJsonStructure([
99+
'metadata' => [
100+
'policy_id',
101+
'active_from',
102+
'content_vue_file',
103+
'type',
104+
],
105+
]);
106+
107+
$request->assertJsonFragment([
108+
'active_from' => $now->addWeek()->format('Y-m-d'),
109+
'type' => 'terms-of-use',
110+
'content_vue_file' => 'terms-of-use/version-1.vue',
111+
]);
112+
}
113+
114+
public function testCurrentTermsOfUseMissing(): void {
115+
Policy::query()->delete();
116+
117+
$request = $this->getJson('v1/policies/terms-of-use/current');
118+
$request->assertNotFound();
119+
}
120+
121+
public function testCurrentHostingPolicyMissing(): void {
122+
Policy::query()->delete();
123+
124+
$request = $this->getJson('v1/policies/hosting-policy/current');
125+
$request->assertNotFound();
126+
}
127+
128+
public function testCurrentTermsOfUse() {
129+
$now = CarbonImmutable::now();
130+
131+
Policy::factory()->create([
132+
'policy_type' => 'terms-of-use',
133+
'active_from' => $now->subMonth(),
134+
'content_vue_file' => 'terms-of-use/version-1.vue',
135+
]);
136+
137+
$current = Policy::factory()->create([
138+
'policy_type' => 'terms-of-use',
139+
'active_from' => $now->subWeek(),
140+
'content_vue_file' => 'terms-of-use/version-2.vue',
141+
]);
142+
143+
$request = $this->getJson('v1/policies/terms-of-use/current');
144+
145+
$request->assertOk();
146+
$request->assertJsonStructure([
147+
'metadata' => [
148+
'policy_id',
149+
'active_from',
150+
'content_vue_file',
151+
'type',
152+
],
153+
]);
154+
155+
$request->assertJsonFragment([
156+
'active_from' => $current->active_from->format('Y-m-d'),
157+
'type' => $current->policy_type,
158+
'content_vue_file' => $current->content_vue_file,
159+
]);
160+
}
49161
}

0 commit comments

Comments
 (0)