Skip to content

Commit d7ef864

Browse files
authored
Merge branch 'master' into III-6844-match-apikeys
2 parents 5285c95 + cb74d37 commit d7ef864

File tree

8 files changed

+246
-3
lines changed

8 files changed

+246
-3
lines changed

docs/architecture.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,10 @@ SAPI3 is an external search service (Elasticsearch-based) that:
7878
- `/organizers` - Search organizers
7979
- `/offers` - Search events and places combined
8080

81+
## Request Processing
82+
83+
All API requests inside udb3-backend are processed synchronously, except for the export call. This means after creating or updating an entity, no waiting is needed before making subsequent requests to the same entity.
84+
8185
## Docker Services
8286

8387
| Service | Port | Purpose |

docs/features/search.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Acceptance tests for the SAPI3 (Search API 3) integration.
1212
## Test Files
1313

1414
- `auth.feature` - Authentication tests
15+
- `pagination.feature` - Pagination and sorting tests
1516
- `search-proxy.feature` - Search endpoint proxy tests
1617
- `facets.feature` - Facet response tests
1718
- `contributors.feature` - Contributor filtering
@@ -22,3 +23,21 @@ Acceptance tests for the SAPI3 (Search API 3) integration.
2223
1. Create entity (event/place/organizer) via API
2324
2. Poll SAPI3 endpoint waiting for indexing (max 5 seconds)
2425
3. Assert search results
26+
27+
## Query Parameters
28+
29+
SAPI3 supports two types of query parameters:
30+
31+
- **URL parameters**: Direct query string parameters (e.g., `labels=my-label`)
32+
- **Advanced query parameter**: The `q` parameter using Lucene syntax (e.g., `q=labels:my-label`)
33+
34+
## Test Isolation
35+
36+
Search tests can use scenario-based label isolation to prevent interference from other tests:
37+
38+
- Tag scenarios with `@labelIsolation` to enable isolation
39+
- A unique label (`scenario-{uuid}`) is automatically generated per scenario
40+
- The label is added to all fixtures created during the scenario
41+
- Search queries automatically filter by this label
42+
43+
This ensures each scenario only sees its own data, regardless of what other tests create.

features/State/RequestState.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ final class RequestState
1010
private string $apiKey = '';
1111
private string $jwt = '';
1212
private string $clientId = '';
13+
private array $urlParams = [];
1314

1415
private string $acceptHeader = '';
1516
private string $contentTypeHeader = '';
@@ -47,6 +48,25 @@ public function setClientId(string $clientId): void
4748
$this->clientId = $clientId;
4849
}
4950

51+
public function getUrlParams(): array
52+
{
53+
return $this->urlParams;
54+
}
55+
56+
public function setUrlParam(string $key, string $value): void
57+
{
58+
if (empty($value)) {
59+
unset($this->urlParams[$key]);
60+
} else {
61+
$this->urlParams[$key] = $value;
62+
}
63+
}
64+
65+
public function clearUrlParams(): void
66+
{
67+
$this->urlParams = [];
68+
}
69+
5070
public function getJwt(): string
5171
{
5272
return $this->jwt;

features/Steps/AuthorizationSteps.php

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,14 @@ public function iAmNotUsingAnUitidV1ApiKey(): void
155155
$this->requestState->setApiKey('');
156156
}
157157

158+
/**
159+
* @Given I am using an invalid UiTID v1 API key
160+
*/
161+
public function iAmUsingAnInvalidUitidV1ApiKey(): void
162+
{
163+
$this->requestState->setApiKey('invalid-api-key');
164+
}
165+
158166
/**
159167
* @Given I am using a x-client-id header for client :clientId
160168
*/
@@ -170,4 +178,44 @@ public function iAmNotUsingAXClientIdHeader(): void
170178
{
171179
$this->requestState->setClientId('');
172180
}
181+
182+
/**
183+
* @Given I am using an invalid x-client-id header
184+
*/
185+
public function iAmUsingAnInvalidXClientIdHeader(): void
186+
{
187+
$this->requestState->setClientId('invalid-client-id');
188+
}
189+
190+
/**
191+
* @Given I am using an API key URL parameter of consumer :consumerName
192+
*/
193+
public function iAmUsingAnApiKeyUrlParameterOfConsumer(string $consumerName): void
194+
{
195+
$this->requestState->setUrlParam('apiKey', $this->config['apiKeys'][$consumerName]);
196+
}
197+
198+
/**
199+
* @Given I am not using an API key URL parameter
200+
*/
201+
public function iAmNotUsingAnApiKeyUrlParameter(): void
202+
{
203+
$this->requestState->setUrlParam('apiKey', '');
204+
}
205+
206+
/**
207+
* @Given I am using a clientId URL parameter for client :clientId
208+
*/
209+
public function iAmUsingAClientIdUrlParameterForClient(string $clientId): void
210+
{
211+
$this->requestState->setUrlParam('clientId', $this->config['clients'][$clientId]['client_id']);
212+
}
213+
214+
/**
215+
* @Given I am not using a clientId URL parameter
216+
*/
217+
public function iAmNotUsingAClientIdUrlParameter(): void
218+
{
219+
$this->requestState->setUrlParam('clientId', '');
220+
}
173221
}

features/Support/HttpClient.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,18 @@
1212
final class HttpClient
1313
{
1414
private Client $client;
15+
private array $urlParams;
1516

1617
public function __construct(
1718
string $jwt,
1819
string $apiKey,
1920
string $clientId,
2021
string $contentTypeHeader,
2122
string $acceptHeader,
22-
string $baseUrl
23+
string $baseUrl,
24+
array $urlParams = []
2325
) {
26+
$this->urlParams = $urlParams;
2427
$headers = [];
2528

2629
if (!empty($jwt)) {
@@ -87,7 +90,17 @@ public function patchJSON(string $url, string $json): ResponseInterface
8790

8891
public function get(string $url): ResponseInterface
8992
{
90-
return $this->client->get($url);
93+
return $this->client->get($this->appendUrlParams($url));
94+
}
95+
96+
private function appendUrlParams(string $url): string
97+
{
98+
if (empty($this->urlParams)) {
99+
return $url;
100+
}
101+
102+
$separator = str_contains($url, '?') ? '&' : '?';
103+
return $url . $separator . http_build_query($this->urlParams);
91104
}
92105

93106
public function getWithParameters(string $url, array $parameters, VariableState $variableState): ResponseInterface

features/bootstrap/FeatureContext.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ private function getHttpClient(): HttpClient
6868
$this->requestState->getClientId(),
6969
$this->requestState->getContentTypeHeader(),
7070
$this->requestState->getAcceptHeader(),
71-
$this->requestState->getBaseUrl()
71+
$this->requestState->getBaseUrl(),
72+
$this->requestState->getUrlParams()
7273
);
7374
}
7475

features/search/auth.feature

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ Feature: Test the Search API v3 authentication
77
And I am not authorized
88
And I am not using an UiTID v1 API key
99
And I am not using a x-client-id header
10+
And I am not using an API key URL parameter
11+
And I am not using a clientId URL parameter
1012

1113
Scenario: Search without authentication
1214
When I send a GET request to "/events"
@@ -22,6 +24,20 @@ Feature: Test the Search API v3 authentication
2224
When I send a GET request to "/events"
2325
Then the response status should be "401"
2426

27+
Scenario: Search with an invalid API key
28+
Given I am using an invalid UiTID v1 API key
29+
When I send a GET request to "/events"
30+
Then the response status should be "401"
31+
And the JSON response should be:
32+
"""
33+
{
34+
"title": "Unauthorized",
35+
"type": "https:\/\/api.publiq.be\/probs\/auth\/unauthorized",
36+
"status": 401,
37+
"detail": "The provided api key invalid-api-key is invalid"
38+
}
39+
"""
40+
2541
Scenario: Search with an API key that will be matched to a client id
2642
Given I am using an UiTID v1 API key of consumer "apiKeyMatchedToClientIdWithSearchScope"
2743
When I send a GET request to "/events"
@@ -39,6 +55,20 @@ Feature: Test the Search API v3 authentication
3955
When I send a GET request to "/events"
4056
Then the response status should be "403"
4157

58+
Scenario: Search with an invalid client id
59+
Given I am using an invalid x-client-id header
60+
When I send a GET request to "/events"
61+
Then the response status should be "403"
62+
And the JSON response should be:
63+
"""
64+
{
65+
"title": "Forbidden",
66+
"type": "https:\/\/api.publiq.be\/probs\/auth\/forbidden",
67+
"status": 403,
68+
"detail": "The provided client id invalid-client-id is not allowed to access this API."
69+
}
70+
"""
71+
4272
Scenario: Search with a client access token of a client that has access to Search API v3
4373
Given I am authorized with an OAuth client access token for "test_client_sapi3_only"
4474
And I am using the Search API v3 base URL
@@ -56,3 +86,13 @@ Feature: Test the Search API v3 authentication
5686
And I am using the Search API v3 base URL
5787
When I send a GET request to "/events"
5888
Then the response status should be "403"
89+
90+
Scenario: Search with API key URL parameter that has access to Search API v3
91+
Given I am using an API key URL parameter of consumer "uitdatabank"
92+
When I send a GET request to "/events"
93+
Then the response status should be "200"
94+
95+
Scenario: Search with clientId URL parameter that has access to Search API v3
96+
Given I am using a clientId URL parameter for client "test_client_sapi3_only"
97+
When I send a GET request to "/events"
98+
Then the response status should be "200"

features/search/pagination.feature

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
@sapi3
2+
Feature: Test the Search API v3 pagination and sorting
3+
4+
Background:
5+
Given I am using the Search API v3 base URL
6+
And I am using a x-client-id header for client "test_client_sapi3_only"
7+
And I send and accept "application/json"
8+
9+
Scenario: Default itemsPerPage should be 30
10+
When I send a GET request to "/offers"
11+
Then the response status should be "200"
12+
And the JSON response at "itemsPerPage" should be 30
13+
14+
Scenario: Custom limit is accepted
15+
When I send a GET request to "/offers" with parameters:
16+
| limit | 50 |
17+
Then the response status should be "200"
18+
And the JSON response at "itemsPerPage" should be 50
19+
20+
Scenario: Limit above 2000 returns error
21+
When I send a GET request to "/offers" with parameters:
22+
| limit | 3000 |
23+
Then the response status should be "404"
24+
And the JSON response should be:
25+
"""
26+
{
27+
"title": "Not Found",
28+
"type": "https:\/\/api.publiq.be\/probs\/url\/not-found",
29+
"status": 404,
30+
"detail": "The \"limit\" parameter should be between 0 and 2000"
31+
}
32+
"""
33+
34+
Scenario: Different start is possible
35+
When I send a GET request to "/offers" with parameters:
36+
| start | 10 |
37+
Then the response status should be "200"
38+
39+
Scenario: Start above 10000 returns error
40+
When I send a GET request to "/offers" with parameters:
41+
| start | 1000000 |
42+
Then the response status should be "404"
43+
And the JSON response should be:
44+
"""
45+
{
46+
"title": "Not Found",
47+
"type": "https:\/\/api.publiq.be\/probs\/url\/not-found",
48+
"status": 404,
49+
"detail": "The \"start\" parameter should be between 0 and 10000"
50+
}
51+
"""
52+
53+
Scenario: Sort by availableTo ascending
54+
When I send a GET request to "/offers" with parameters:
55+
| sort[availableTo] | asc |
56+
Then the response status should be "200"
57+
58+
Scenario: Sort by availableTo descending
59+
When I send a GET request to "/offers" with parameters:
60+
| sort[availableTo] | desc |
61+
Then the response status should be "200"
62+
63+
Scenario: Sort by completeness ascending
64+
When I send a GET request to "/offers" with parameters:
65+
| sort[completeness] | asc |
66+
Then the response status should be "200"
67+
68+
Scenario: Sort by created ascending
69+
When I send a GET request to "/offers" with parameters:
70+
| sort[created] | asc |
71+
Then the response status should be "200"
72+
73+
Scenario: Sort by distance ascending
74+
When I send a GET request to "/offers" with parameters:
75+
| coordinates | 50.8511740,4.3386740 |
76+
| distance | 10km |
77+
| sort[distance] | asc |
78+
Then the response status should be "200"
79+
80+
Scenario: Sort by modified ascending
81+
When I send a GET request to "/offers" with parameters:
82+
| sort[modified] | asc |
83+
Then the response status should be "200"
84+
85+
Scenario: Sort by modified descending
86+
When I send a GET request to "/offers" with parameters:
87+
| sort[modified] | desc |
88+
Then the response status should be "200"
89+
90+
Scenario: Sort by score ascending
91+
When I send a GET request to "/offers" with parameters:
92+
| sort[score] | asc |
93+
Then the response status should be "200"
94+
95+
Scenario: Sort by score descending
96+
When I send a GET request to "/offers" with parameters:
97+
| sort[score] | desc |
98+
Then the response status should be "200"

0 commit comments

Comments
 (0)