Skip to content

Commit a68907d

Browse files
Fluf22millotp
andauthored
feat(clients): add api key helper test (#3338)
Co-authored-by: Pierre Millot <[email protected]>
1 parent 14f42e3 commit a68907d

File tree

31 files changed

+481
-187
lines changed

31 files changed

+481
-187
lines changed

clients/algoliasearch-client-csharp/algoliasearch/Utils/SearchClientExtensions.cs

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -76,14 +76,15 @@ public GetTaskResponse WaitForAppTask(long taskId, int maxRetries = DefaultMaxRe
7676
/// <summary>
7777
/// Helper method that waits for an API key task to be processed.
7878
/// </summary>
79-
/// <param name="operation">The `operation` that was done on a `key`.</param>
8079
/// <param name="key">The key that has been added, deleted or updated.</param>
80+
/// <param name="operation">The `operation` that was done on a `key`.</param>
8181
/// <param name="apiKey">Necessary to know if an `update` operation has been processed, compare fields of the response with it. (optional - mandatory if operation is UPDATE)</param>
8282
/// <param name="maxRetries">The maximum number of retry. 50 by default. (optional)</param>
8383
/// <param name="timeout">The function to decide how long to wait between retries. Math.Min(retryCount * 200, 5000) by default. (optional)</param>
8484
/// <param name="requestOptions">The requestOptions to send along with the query, they will be merged with the transporter requestOptions. (optional)</param>
8585
/// <param name="ct">Cancellation token (optional)</param>
86-
public async Task<GetApiKeyResponse> WaitForApiKeyAsync(ApiKeyOperation operation, string key,
86+
public async Task<GetApiKeyResponse> WaitForApiKeyAsync(string key,
87+
ApiKeyOperation operation,
8788
ApiKey apiKey = default, int maxRetries = DefaultMaxRetries, Func<int, int> timeout = null,
8889
RequestOptions requestOptions = null, CancellationToken ct = default)
8990
{
@@ -112,36 +113,35 @@ public async Task<GetApiKeyResponse> WaitForApiKeyAsync(ApiKeyOperation operatio
112113
}, maxRetries, timeout, ct).ConfigureAwait(false);
113114
}
114115

115-
var addedKey = new GetApiKeyResponse();
116-
117-
// check the status of the getApiKey method
118-
await RetryUntil(async () =>
116+
return await RetryUntil(async () =>
119117
{
120118
try
121119
{
122-
addedKey = await GetApiKeyAsync(key, requestOptions, ct).ConfigureAwait(false);
123-
// magic number to signify we found the key
124-
return -2;
120+
return await GetApiKeyAsync(key, requestOptions, ct).ConfigureAwait(false);
125121
}
126122
catch (AlgoliaApiException e)
127123
{
128-
return e.HttpErrorCode;
124+
if (e.HttpErrorCode is 404)
125+
{
126+
return null;
127+
}
128+
129+
throw;
129130
}
130-
}, (status) =>
131+
}, (response) =>
131132
{
132133
return operation switch
133134
{
134135
ApiKeyOperation.Add =>
135136
// stop either when the key is created or when we don't receive 404
136-
status is -2 or not 404 and not 0,
137+
response is not null,
137138
ApiKeyOperation.Delete =>
138139
// stop when the key is not found
139-
status == 404,
140+
response is null,
140141
_ => false
141142
};
142143
},
143144
maxRetries, timeout, ct);
144-
return addedKey;
145145
}
146146

147147
/// <summary>
@@ -154,10 +154,10 @@ await RetryUntil(async () =>
154154
/// <param name="timeout">The function to decide how long to wait between retries. Math.Min(retryCount * 200, 5000) by default. (optional)</param>
155155
/// <param name="requestOptions">The requestOptions to send along with the query, they will be merged with the transporter requestOptions. (optional)</param>
156156
/// <param name="ct">Cancellation token (optional)</param>
157-
public GetApiKeyResponse WaitForApiKey(ApiKeyOperation operation, string key, ApiKey apiKey = default,
157+
public GetApiKeyResponse WaitForApiKey(string key, ApiKeyOperation operation, ApiKey apiKey = default,
158158
int maxRetries = DefaultMaxRetries, Func<int, int> timeout = null, RequestOptions requestOptions = null,
159159
CancellationToken ct = default) =>
160-
AsyncHelper.RunSync(() => WaitForApiKeyAsync(operation, key, apiKey, maxRetries, timeout, requestOptions, ct));
160+
AsyncHelper.RunSync(() => WaitForApiKeyAsync(key, operation, apiKey, maxRetries, timeout, requestOptions, ct));
161161

162162

163163
/// <summary>

clients/algoliasearch-client-kotlin/client/src/commonMain/kotlin/com/algolia/client/extensions/SearchClient.kt

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ import kotlin.time.Duration.Companion.seconds
2020
/**
2121
* Wait for an API key to be added, updated or deleted based on a given `operation`.
2222
*
23-
* @param operation The `operation` that was done on a `key`.
2423
* @param key The `key` that has been added, deleted or updated.
24+
* @param operation The `operation` that was done on a `key`.
2525
* @param apiKey Necessary to know if an `update` operation has been processed, compare fields of
2626
* the response with it.
2727
* @param maxRetries The maximum number of retries. 50 by default. (optional)
@@ -31,16 +31,16 @@ import kotlin.time.Duration.Companion.seconds
3131
* the transporter requestOptions. (optional)
3232
*/
3333
public suspend fun SearchClient.waitForApiKey(
34-
operation: ApiKeyOperation,
3534
key: String,
35+
operation: ApiKeyOperation,
3636
apiKey: ApiKey? = null,
3737
maxRetries: Int = 50,
3838
timeout: Duration = Duration.INFINITE,
3939
initialDelay: Duration = 200.milliseconds,
4040
maxDelay: Duration = 5.seconds,
4141
requestOptions: RequestOptions? = null,
42-
) {
43-
when (operation) {
42+
): GetApiKeyResponse? {
43+
return when (operation) {
4444
ApiKeyOperation.Add -> waitKeyCreation(
4545
key = key,
4646
maxRetries = maxRetries,
@@ -226,25 +226,25 @@ public suspend fun SearchClient.waitKeyDelete(
226226
initialDelay: Duration = 200.milliseconds,
227227
maxDelay: Duration = 5.seconds,
228228
requestOptions: RequestOptions? = null,
229-
) {
230-
retryUntil(
229+
): GetApiKeyResponse? {
230+
return retryUntil(
231231
timeout = timeout,
232232
maxRetries = maxRetries,
233233
initialDelay = initialDelay,
234234
maxDelay = maxDelay,
235235
retry = {
236236
try {
237-
val response = getApiKey(key, requestOptions)
238-
Result.success(response)
237+
return@retryUntil getApiKey(key, requestOptions)
239238
} catch (e: AlgoliaApiException) {
240-
Result.failure(e)
239+
if (e.httpErrorCode == 404) {
240+
return@retryUntil null
241+
}
242+
243+
throw e
241244
}
242245
},
243246
until = { result ->
244-
result.fold(
245-
onSuccess = { false },
246-
onFailure = { (it as AlgoliaApiException).httpErrorCode == 404 },
247-
)
247+
result == null
248248
},
249249
)
250250
}

clients/algoliasearch-client-php/lib/Support/Helpers.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,13 +151,13 @@ public static function retryForApiKeyUntil(
151151

152152
// In case of an addition, if there was no error, the $key has been added as it should be
153153
if ('add' === $operation) {
154-
return;
154+
return $response;
155155
}
156156

157157
// In case of an update, check if the key has been updated as it should be
158158
if ('update' === $operation) {
159159
if (self::isKeyUpdated($response, $apiKey)) {
160-
return;
160+
return $response;
161161
}
162162
}
163163

@@ -166,9 +166,9 @@ public static function retryForApiKeyUntil(
166166
// In case of a deletion, if there was an error, the $key has been deleted as it should be
167167
if (
168168
'delete' === $operation
169-
&& 'Key does not exist' === $e->getMessage()
169+
&& $e->getCode() === 404
170170
) {
171-
return;
171+
return null;
172172
}
173173

174174
// Else try again ...

clients/algoliasearch-client-ruby/lib/algolia/transport/transport.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def request(call_type, method, path, body, opts = {})
6868
)
6969
if outcome == FAILURE
7070
decoded_error = JSON.parse(response.error, :symbolize_names => true)
71-
raise Algolia::AlgoliaHttpError.new(decoded_error[:status], decoded_error[:message])
71+
raise Algolia::AlgoliaHttpError.new(response.status, decoded_error[:message])
7272
end
7373

7474
return response unless outcome == RETRY

generators/src/main/java/com/algolia/codegen/cts/tests/ParametersWithDataType.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
import java.util.Map.Entry;
1111
import java.util.stream.Collectors;
1212
import java.util.stream.IntStream;
13-
import org.apache.commons.lang3.StringUtils;
1413
import org.openapitools.codegen.*;
1514

1615
@SuppressWarnings("unchecked")
@@ -128,7 +127,6 @@ private Map<String, Object> traverseParams(
128127
String finalParamName = getFinalParamName(paramName);
129128

130129
testOutput.put("key", finalParamName);
131-
testOutput.put("isKeyAllUpperCase", StringUtils.isAllUpperCase(finalParamName));
132130
testOutput.put("useAnonymousKey", !finalParamName.matches("(.*)_[0-9]$") && depth != 0);
133131
testOutput.put("parent", parent);
134132
testOutput.put("isRoot", "".equals(parent));

generators/src/main/java/com/algolia/codegen/cts/tests/TestsClient.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,10 @@ public void run(Map<String, CodegenModel> models, Map<String, CodegenOperation>
169169
} else {
170170
stepOut.put("match", step.expected.match);
171171
}
172+
} else if (step.expected.match == null) {
173+
stepOut.put("match", Map.of());
174+
stepOut.put("matchIsJSON", false);
175+
stepOut.put("matchIsNull", true);
172176
}
173177
}
174178
steps.add(stepOut);

playground/swift/playground/playground/main.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ Task {
4444
taskIDs.append(saveObjRes.taskID)
4545
}
4646
for taskID in taskIDs {
47-
try await client.waitForTask(with: taskID, in: indexName)
47+
try await client.waitForTask(indexName: indexName, taskID: taskID)
4848
}
4949

5050
let searchParams = SearchSearchParamsObject(query: "Jimmy")

scripts/cts/runCts.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { startTestServer } from './testServer';
99
import { assertChunkWrapperValid } from './testServer/chunkWrapper.js';
1010
import { assertValidReplaceAllObjects } from './testServer/replaceAllObjects.js';
1111
import { assertValidTimeouts } from './testServer/timeout.js';
12+
import { assertValidWaitForApiKey } from './testServer/waitForApiKey.js';
1213

1314
async function runCtsOne(
1415
language: Language,
@@ -152,5 +153,6 @@ export async function runCts(
152153
assertValidTimeouts(languages.length);
153154
assertChunkWrapperValid(languages.length - skip('dart') - skip('scala'));
154155
assertValidReplaceAllObjects(languages.length - skip('dart') - skip('scala'));
156+
assertValidWaitForApiKey(languages.length - skip('dart') - skip('scala'));
155157
}
156158
}

scripts/cts/testServer/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { gzipServer } from './gzip';
1010
import { replaceAllObjectsServer } from './replaceAllObjects';
1111
import { timeoutServer } from './timeout';
1212
import { timeoutServerBis } from './timeoutBis';
13+
import { waitForApiKeyServer } from './waitForApiKey';
1314

1415
export async function startTestServer(): Promise<() => Promise<void>> {
1516
const servers = await Promise.all([
@@ -18,6 +19,7 @@ export async function startTestServer(): Promise<() => Promise<void>> {
1819
timeoutServerBis(),
1920
replaceAllObjectsServer(),
2021
chunkWrapperServer(),
22+
waitForApiKeyServer(),
2123
]);
2224

2325
return async () => {
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import type { Server } from 'http';
2+
3+
import { expect } from 'chai';
4+
import type { Express } from 'express';
5+
6+
import { setupServer } from '.';
7+
8+
const retryCount: Record<
9+
string,
10+
{
11+
add: number;
12+
update: number;
13+
delete: number;
14+
}
15+
> = {};
16+
17+
export function assertValidWaitForApiKey(expectedCount: number): void {
18+
expect(Object.keys(retryCount).length).to.be.equal(expectedCount);
19+
for (const retry of Object.values(retryCount)) {
20+
expect(retry).to.deep.equal({
21+
add: 0,
22+
update: 0,
23+
delete: 0,
24+
});
25+
}
26+
}
27+
28+
function addRoutes(app: Express): void {
29+
app.get('/1/keys/:key', (req, res) => {
30+
const lang = req.params.key.split('-').at(-1) as string;
31+
if (!retryCount[lang]) {
32+
retryCount[lang] = {
33+
add: 0,
34+
update: 0,
35+
delete: 0,
36+
};
37+
}
38+
const retry = retryCount[lang];
39+
if (req.params.key === `api-key-add-operation-test-${lang}`) {
40+
if (retry.add < 3) {
41+
res.status(404).json({ message: `API key doesn't exist` });
42+
} else if (retry.add === 3) {
43+
res.status(200).json({
44+
value: req.params.key,
45+
description: 'my new api key',
46+
acl: ['search', 'addObject'],
47+
validity: 300,
48+
maxQueriesPerIPPerHour: 100,
49+
maxHitsPerQuery: 20,
50+
createdAt: 1720094400,
51+
});
52+
53+
retry.add = -1;
54+
} else {
55+
expect(retry.add).to.be.lessThan(3);
56+
return;
57+
}
58+
59+
retry.add += 1;
60+
} else if (req.params.key === `api-key-update-operation-test-${lang}`) {
61+
if (retry.update < 3) {
62+
res.status(200).json({
63+
value: req.params.key,
64+
description: 'my new api key',
65+
acl: ['search', 'addObject'],
66+
validity: 300,
67+
maxQueriesPerIPPerHour: 100,
68+
maxHitsPerQuery: 20,
69+
createdAt: 1720094400,
70+
});
71+
} else if (retry.update === 3) {
72+
res.status(200).json({
73+
value: req.params.key,
74+
description: 'my updated api key',
75+
acl: ['search', 'addObject', 'deleteObject'],
76+
indexes: ['Movies', 'Books'],
77+
referers: ['*google.com', '*algolia.com'],
78+
validity: 305,
79+
maxQueriesPerIPPerHour: 95,
80+
maxHitsPerQuery: 20,
81+
createdAt: 1720094400,
82+
});
83+
84+
retry.update = -1;
85+
} else {
86+
expect(retry.update).to.be.lessThan(3);
87+
return;
88+
}
89+
90+
retry.update += 1;
91+
} else if (req.params.key === `api-key-delete-operation-test-${lang}`) {
92+
if (retry.delete < 3) {
93+
res.status(200).json({
94+
value: req.params.key,
95+
description: 'my updated api key',
96+
acl: ['search', 'addObject', 'deleteObject'],
97+
validity: 305,
98+
maxQueriesPerIPPerHour: 95,
99+
maxHitsPerQuery: 20,
100+
createdAt: 1720094400,
101+
});
102+
} else if (retry.delete === 3) {
103+
res.status(404).json({ message: `API key doesn't exist` });
104+
105+
retry.delete = -1;
106+
} else {
107+
expect(retry.delete).to.be.lessThan(3);
108+
return;
109+
}
110+
111+
retry.delete += 1;
112+
} else {
113+
throw new Error(`Invalid API key ${req.params.key}`);
114+
}
115+
});
116+
}
117+
118+
export function waitForApiKeyServer(): Promise<Server> {
119+
return setupServer('waitForApiKey', 6681, addRoutes);
120+
}

specs/search/helpers/waitForApiKey.yml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@ method:
77
summary: Wait for an API key operation
88
description: Waits for an API key to be added, updated, or deleted.
99
parameters:
10-
- in: query
11-
name: operation
12-
description: Whether the API key was created, updated, or deleted.
13-
required: true
14-
schema:
15-
$ref: '#/apiKeyOperation'
1610
- in: query
1711
name: key
1812
description: API key to wait for.
1913
required: true
2014
schema:
2115
type: string
16+
- in: query
17+
name: operation
18+
description: Whether the API key was created, updated, or deleted.
19+
required: true
20+
schema:
21+
$ref: '#/apiKeyOperation'
2222
- in: query
2323
name: apiKey
2424
description: Used to compare fields of the `getApiKey` response on an `update` operation, to check if the `key` has been updated.

0 commit comments

Comments
 (0)