Skip to content

Commit

Permalink
feat(clients): add api key helper test (#3338)
Browse files Browse the repository at this point in the history
Co-authored-by: Pierre Millot <pierre.millot@algolia.com>
  • Loading branch information
Fluf22 and millotp authored Jul 18, 2024
1 parent 14f42e3 commit a68907d
Show file tree
Hide file tree
Showing 31 changed files with 481 additions and 187 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,15 @@ public GetTaskResponse WaitForAppTask(long taskId, int maxRetries = DefaultMaxRe
/// <summary>
/// Helper method that waits for an API key task to be processed.
/// </summary>
/// <param name="operation">The `operation` that was done on a `key`.</param>
/// <param name="key">The key that has been added, deleted or updated.</param>
/// <param name="operation">The `operation` that was done on a `key`.</param>
/// <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>
/// <param name="maxRetries">The maximum number of retry. 50 by default. (optional)</param>
/// <param name="timeout">The function to decide how long to wait between retries. Math.Min(retryCount * 200, 5000) by default. (optional)</param>
/// <param name="requestOptions">The requestOptions to send along with the query, they will be merged with the transporter requestOptions. (optional)</param>
/// <param name="ct">Cancellation token (optional)</param>
public async Task<GetApiKeyResponse> WaitForApiKeyAsync(ApiKeyOperation operation, string key,
public async Task<GetApiKeyResponse> WaitForApiKeyAsync(string key,
ApiKeyOperation operation,
ApiKey apiKey = default, int maxRetries = DefaultMaxRetries, Func<int, int> timeout = null,
RequestOptions requestOptions = null, CancellationToken ct = default)
{
Expand Down Expand Up @@ -112,36 +113,35 @@ public async Task<GetApiKeyResponse> WaitForApiKeyAsync(ApiKeyOperation operatio
}, maxRetries, timeout, ct).ConfigureAwait(false);
}

var addedKey = new GetApiKeyResponse();

// check the status of the getApiKey method
await RetryUntil(async () =>
return await RetryUntil(async () =>
{
try
{
addedKey = await GetApiKeyAsync(key, requestOptions, ct).ConfigureAwait(false);
// magic number to signify we found the key
return -2;
return await GetApiKeyAsync(key, requestOptions, ct).ConfigureAwait(false);
}
catch (AlgoliaApiException e)
{
return e.HttpErrorCode;
if (e.HttpErrorCode is 404)
{
return null;
}
throw;
}
}, (status) =>
}, (response) =>
{
return operation switch
{
ApiKeyOperation.Add =>
// stop either when the key is created or when we don't receive 404
status is -2 or not 404 and not 0,
response is not null,
ApiKeyOperation.Delete =>
// stop when the key is not found
status == 404,
response is null,
_ => false
};
},
maxRetries, timeout, ct);
return addedKey;
}

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


/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import kotlin.time.Duration.Companion.seconds
/**
* Wait for an API key to be added, updated or deleted based on a given `operation`.
*
* @param operation The `operation` that was done on a `key`.
* @param key The `key` that has been added, deleted or updated.
* @param operation The `operation` that was done on a `key`.
* @param apiKey Necessary to know if an `update` operation has been processed, compare fields of
* the response with it.
* @param maxRetries The maximum number of retries. 50 by default. (optional)
Expand All @@ -31,16 +31,16 @@ import kotlin.time.Duration.Companion.seconds
* the transporter requestOptions. (optional)
*/
public suspend fun SearchClient.waitForApiKey(
operation: ApiKeyOperation,
key: String,
operation: ApiKeyOperation,
apiKey: ApiKey? = null,
maxRetries: Int = 50,
timeout: Duration = Duration.INFINITE,
initialDelay: Duration = 200.milliseconds,
maxDelay: Duration = 5.seconds,
requestOptions: RequestOptions? = null,
) {
when (operation) {
): GetApiKeyResponse? {
return when (operation) {
ApiKeyOperation.Add -> waitKeyCreation(
key = key,
maxRetries = maxRetries,
Expand Down Expand Up @@ -226,25 +226,25 @@ public suspend fun SearchClient.waitKeyDelete(
initialDelay: Duration = 200.milliseconds,
maxDelay: Duration = 5.seconds,
requestOptions: RequestOptions? = null,
) {
retryUntil(
): GetApiKeyResponse? {
return retryUntil(
timeout = timeout,
maxRetries = maxRetries,
initialDelay = initialDelay,
maxDelay = maxDelay,
retry = {
try {
val response = getApiKey(key, requestOptions)
Result.success(response)
return@retryUntil getApiKey(key, requestOptions)
} catch (e: AlgoliaApiException) {
Result.failure(e)
if (e.httpErrorCode == 404) {
return@retryUntil null
}

throw e
}
},
until = { result ->
result.fold(
onSuccess = { false },
onFailure = { (it as AlgoliaApiException).httpErrorCode == 404 },
)
result == null
},
)
}
Expand Down
8 changes: 4 additions & 4 deletions clients/algoliasearch-client-php/lib/Support/Helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,13 +151,13 @@ public static function retryForApiKeyUntil(

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

// In case of an update, check if the key has been updated as it should be
if ('update' === $operation) {
if (self::isKeyUpdated($response, $apiKey)) {
return;
return $response;
}
}

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

// Else try again ...
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def request(call_type, method, path, body, opts = {})
)
if outcome == FAILURE
decoded_error = JSON.parse(response.error, :symbolize_names => true)
raise Algolia::AlgoliaHttpError.new(decoded_error[:status], decoded_error[:message])
raise Algolia::AlgoliaHttpError.new(response.status, decoded_error[:message])
end

return response unless outcome == RETRY
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import java.util.Map.Entry;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.*;

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

testOutput.put("key", finalParamName);
testOutput.put("isKeyAllUpperCase", StringUtils.isAllUpperCase(finalParamName));
testOutput.put("useAnonymousKey", !finalParamName.matches("(.*)_[0-9]$") && depth != 0);
testOutput.put("parent", parent);
testOutput.put("isRoot", "".equals(parent));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,10 @@ public void run(Map<String, CodegenModel> models, Map<String, CodegenOperation>
} else {
stepOut.put("match", step.expected.match);
}
} else if (step.expected.match == null) {
stepOut.put("match", Map.of());
stepOut.put("matchIsJSON", false);
stepOut.put("matchIsNull", true);
}
}
steps.add(stepOut);
Expand Down
2 changes: 1 addition & 1 deletion playground/swift/playground/playground/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ Task {
taskIDs.append(saveObjRes.taskID)
}
for taskID in taskIDs {
try await client.waitForTask(with: taskID, in: indexName)
try await client.waitForTask(indexName: indexName, taskID: taskID)
}

let searchParams = SearchSearchParamsObject(query: "Jimmy")
Expand Down
2 changes: 2 additions & 0 deletions scripts/cts/runCts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { startTestServer } from './testServer';
import { assertChunkWrapperValid } from './testServer/chunkWrapper.js';
import { assertValidReplaceAllObjects } from './testServer/replaceAllObjects.js';
import { assertValidTimeouts } from './testServer/timeout.js';
import { assertValidWaitForApiKey } from './testServer/waitForApiKey.js';

async function runCtsOne(
language: Language,
Expand Down Expand Up @@ -152,5 +153,6 @@ export async function runCts(
assertValidTimeouts(languages.length);
assertChunkWrapperValid(languages.length - skip('dart') - skip('scala'));
assertValidReplaceAllObjects(languages.length - skip('dart') - skip('scala'));
assertValidWaitForApiKey(languages.length - skip('dart') - skip('scala'));
}
}
2 changes: 2 additions & 0 deletions scripts/cts/testServer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { gzipServer } from './gzip';
import { replaceAllObjectsServer } from './replaceAllObjects';
import { timeoutServer } from './timeout';
import { timeoutServerBis } from './timeoutBis';
import { waitForApiKeyServer } from './waitForApiKey';

export async function startTestServer(): Promise<() => Promise<void>> {
const servers = await Promise.all([
Expand All @@ -18,6 +19,7 @@ export async function startTestServer(): Promise<() => Promise<void>> {
timeoutServerBis(),
replaceAllObjectsServer(),
chunkWrapperServer(),
waitForApiKeyServer(),
]);

return async () => {
Expand Down
120 changes: 120 additions & 0 deletions scripts/cts/testServer/waitForApiKey.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import type { Server } from 'http';

import { expect } from 'chai';
import type { Express } from 'express';

import { setupServer } from '.';

const retryCount: Record<
string,
{
add: number;
update: number;
delete: number;
}
> = {};

export function assertValidWaitForApiKey(expectedCount: number): void {
expect(Object.keys(retryCount).length).to.be.equal(expectedCount);
for (const retry of Object.values(retryCount)) {
expect(retry).to.deep.equal({
add: 0,
update: 0,
delete: 0,
});
}
}

function addRoutes(app: Express): void {
app.get('/1/keys/:key', (req, res) => {
const lang = req.params.key.split('-').at(-1) as string;
if (!retryCount[lang]) {
retryCount[lang] = {
add: 0,
update: 0,
delete: 0,
};
}
const retry = retryCount[lang];
if (req.params.key === `api-key-add-operation-test-${lang}`) {
if (retry.add < 3) {
res.status(404).json({ message: `API key doesn't exist` });
} else if (retry.add === 3) {
res.status(200).json({
value: req.params.key,
description: 'my new api key',
acl: ['search', 'addObject'],
validity: 300,
maxQueriesPerIPPerHour: 100,
maxHitsPerQuery: 20,
createdAt: 1720094400,
});

retry.add = -1;
} else {
expect(retry.add).to.be.lessThan(3);
return;
}

retry.add += 1;
} else if (req.params.key === `api-key-update-operation-test-${lang}`) {
if (retry.update < 3) {
res.status(200).json({
value: req.params.key,
description: 'my new api key',
acl: ['search', 'addObject'],
validity: 300,
maxQueriesPerIPPerHour: 100,
maxHitsPerQuery: 20,
createdAt: 1720094400,
});
} else if (retry.update === 3) {
res.status(200).json({
value: req.params.key,
description: 'my updated api key',
acl: ['search', 'addObject', 'deleteObject'],
indexes: ['Movies', 'Books'],
referers: ['*google.com', '*algolia.com'],
validity: 305,
maxQueriesPerIPPerHour: 95,
maxHitsPerQuery: 20,
createdAt: 1720094400,
});

retry.update = -1;
} else {
expect(retry.update).to.be.lessThan(3);
return;
}

retry.update += 1;
} else if (req.params.key === `api-key-delete-operation-test-${lang}`) {
if (retry.delete < 3) {
res.status(200).json({
value: req.params.key,
description: 'my updated api key',
acl: ['search', 'addObject', 'deleteObject'],
validity: 305,
maxQueriesPerIPPerHour: 95,
maxHitsPerQuery: 20,
createdAt: 1720094400,
});
} else if (retry.delete === 3) {
res.status(404).json({ message: `API key doesn't exist` });

retry.delete = -1;
} else {
expect(retry.delete).to.be.lessThan(3);
return;
}

retry.delete += 1;
} else {
throw new Error(`Invalid API key ${req.params.key}`);
}
});
}

export function waitForApiKeyServer(): Promise<Server> {
return setupServer('waitForApiKey', 6681, addRoutes);
}
12 changes: 6 additions & 6 deletions specs/search/helpers/waitForApiKey.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,18 @@ method:
summary: Wait for an API key operation
description: Waits for an API key to be added, updated, or deleted.
parameters:
- in: query
name: operation
description: Whether the API key was created, updated, or deleted.
required: true
schema:
$ref: '#/apiKeyOperation'
- in: query
name: key
description: API key to wait for.
required: true
schema:
type: string
- in: query
name: operation
description: Whether the API key was created, updated, or deleted.
required: true
schema:
$ref: '#/apiKeyOperation'
- in: query
name: apiKey
description: Used to compare fields of the `getApiKey` response on an `update` operation, to check if the `key` has been updated.
Expand Down
Loading

0 comments on commit a68907d

Please sign in to comment.