Skip to content

Commit d7e2075

Browse files
authored
feat(cts): generate tests for helpers (#2798)
1 parent 6127fab commit d7e2075

File tree

51 files changed

+322
-217
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+322
-217
lines changed

clients/algoliasearch-client-csharp/algoliasearch/Models/Common/SecuredApiKeyRestrictionHelper.cs

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
using System.Text.Json.Serialization;
44
using Algolia.Search.Http;
55
using Algolia.Search.Utils;
6+
using System.Collections.Generic;
67

78
namespace Algolia.Search.Models.Search;
89

910
/// <summary>
1011
/// Secured Api Key restrictions
1112
/// </summary>
12-
public partial class SecuredAPIKeyRestrictions
13+
public partial class SecuredApiKeyRestrictions
1314
{
1415

1516
/// <summary>
@@ -18,28 +19,26 @@ public partial class SecuredAPIKeyRestrictions
1819
/// <returns></returns>
1920
public string ToQueryString()
2021
{
21-
string restrictionQuery = null;
22+
var restrictions = ToQueryMap(this, nameof(SearchParams));
2223
if (SearchParams != null)
2324
{
24-
restrictionQuery = ToQueryString(SearchParams);
25+
// merge SearchParams into restrictions
26+
restrictions = restrictions.Concat(ToQueryMap(SearchParams)).ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
2527
}
2628

27-
var restrictions = ToQueryString(this, nameof(SearchParams));
28-
var array = new[] { restrictionQuery, restrictions };
29-
30-
return string.Join("&", array.Where(s => !string.IsNullOrEmpty(s)));
29+
return QueryStringHelper.ToQueryString(restrictions.OrderBy(x => x.Key).ToDictionary(kvp => kvp.Key, kvp => kvp.Value));
3130
}
3231

3332
/// <summary>
34-
/// Transform a poco to a query string
33+
/// Transform a poco to a map of query parameters
3534
/// </summary>
3635
/// <param name="value"></param>
3736
/// <param name="ignoreList"></param>
3837
/// <typeparam name="T"></typeparam>
3938
/// <returns></returns>
40-
private static string ToQueryString<T>(T value, params string[] ignoreList)
39+
private static Dictionary<string, string> ToQueryMap<T>(T value, params string[] ignoreList)
4140
{
42-
var properties = typeof(T).GetTypeInfo()
41+
return typeof(T).GetTypeInfo()
4342
.DeclaredProperties.Where(p =>
4443
p.GetValue(value, null) != null && !ignoreList.Contains(p.Name) &&
4544
p.GetCustomAttribute<JsonPropertyNameAttribute>() != null)
@@ -48,8 +47,6 @@ private static string ToQueryString<T>(T value, params string[] ignoreList)
4847
propsName = p.GetCustomAttribute<JsonPropertyNameAttribute>().Name,
4948
value = QueryStringHelper.ParameterToString(p.GetValue(value, null))
5049
}).ToDictionary(p => p.propsName, p => p.value);
51-
52-
return properties.ToQueryString();
5350
}
5451

5552
}

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

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ public IEnumerable<SynonymHit> BrowseSynonyms(string indexName, SearchSynonymsPa
264264
/// <param name="parentApiKey">Parent API Key</param>
265265
/// <param name="restriction">Restriction to add the key</param>
266266
/// <returns></returns>
267-
public string GenerateSecuredApiKey(string parentApiKey, SecuredAPIKeyRestrictions restriction)
267+
public string GenerateSecuredApiKey(string parentApiKey, SecuredApiKeyRestrictions restriction)
268268
{
269269
var queryParams = restriction.ToQueryString();
270270
var hash = HmacShaHelper.GetHash(parentApiKey, queryParams);
@@ -273,20 +273,20 @@ public string GenerateSecuredApiKey(string parentApiKey, SecuredAPIKeyRestrictio
273273

274274

275275
/// <summary>
276-
/// Get the remaining validity of a key generated by `GenerateSecuredApiKeys`.
276+
/// Get the remaining validity of a key generated by `GenerateSecuredApiKey`.
277277
/// </summary>
278-
/// <param name="securedAPIKey">The secured API Key</param>
278+
/// <param name="securedApiKey">The secured API Key</param>
279279
/// <returns></returns>
280280
/// <exception cref="ArgumentNullException"></exception>
281281
/// <exception cref="AlgoliaException"></exception>
282-
public TimeSpan GetSecuredApiKeyRemainingValidity(string securedAPIKey)
282+
public TimeSpan GetSecuredApiKeyRemainingValidity(string securedApiKey)
283283
{
284-
if (string.IsNullOrWhiteSpace(securedAPIKey))
284+
if (string.IsNullOrWhiteSpace(securedApiKey))
285285
{
286-
throw new ArgumentNullException(nameof(securedAPIKey));
286+
throw new ArgumentNullException(nameof(securedApiKey));
287287
}
288288

289-
var decodedKey = Encoding.UTF8.GetString(Convert.FromBase64String(securedAPIKey));
289+
var decodedKey = Encoding.UTF8.GetString(Convert.FromBase64String(securedApiKey));
290290

291291
var regex = new Regex(@"validUntil=\d+");
292292
var matches = regex.Matches(decodedKey);

clients/algoliasearch-client-javascript/packages/client-common/src/transporter/helpers.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,14 @@ export function serializeUrl(
4242
}
4343

4444
export function serializeQueryParameters(parameters: QueryParameters): string {
45-
const isObjectOrArray = (value: any): boolean =>
46-
Object.prototype.toString.call(value) === '[object Object]' ||
47-
Object.prototype.toString.call(value) === '[object Array]';
48-
4945
return Object.keys(parameters)
46+
.filter((key) => parameters[key] !== undefined)
47+
.sort()
5048
.map(
5149
(key) =>
5250
`${key}=${encodeURIComponent(
53-
isObjectOrArray(parameters[key])
54-
? JSON.stringify(parameters[key])
51+
Object.prototype.toString.call(parameters[key]) === '[object Array]'
52+
? parameters[key].join(',')
5553
: parameters[key]
5654
).replaceAll('+', '%20')}`
5755
)

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -344,13 +344,13 @@ public suspend fun <T> SearchClient.replaceAllObjects(
344344
/**
345345
* Generate a virtual API Key without any call to the server.
346346
*
347-
* @param parentAPIKey API key to generate from.
347+
* @param parentApiKey API key to generate from.
348348
* @param restriction Restriction to add the key
349349
* @throws Exception if an error occurs during the encoding
350350
*/
351-
public fun SearchClient.generateSecuredApiKey(parentAPIKey: String, restriction: SecuredAPIKeyRestrictions): String {
351+
public fun SearchClient.generateSecuredApiKey(parentApiKey: String, restriction: SecuredApiKeyRestrictions): String {
352352
val restrictionString = buildRestrictionString(restriction)
353-
val hash = encodeKeySHA256(parentAPIKey, restrictionString)
353+
val hash = encodeKeySHA256(parentApiKey, restrictionString)
354354
return "$hash$restrictionString".encodeBase64()
355355
}
356356

@@ -359,7 +359,7 @@ public fun SearchClient.generateSecuredApiKey(parentAPIKey: String, restriction:
359359
*
360360
* @param apiKey The secured API Key to check.
361361
* @return Duration left before the secured API key expires.
362-
* @throws IllegalArgumentException if [apiKey] doesn't have a [SecuredAPIKeyRestrictions.validUntil].
362+
* @throws IllegalArgumentException if [apiKey] doesn't have a [SecuredApiKeyRestrictions.validUntil].
363363
*/
364364
public fun securedApiKeyRemainingValidity(apiKey: String): Duration {
365365
val decoded = apiKey.decodeBase64String()

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@ package com.algolia.client.extensions.internal
22

33
import com.algolia.client.api.SearchClient
44
import com.algolia.client.model.search.SearchParamsObject
5-
import com.algolia.client.model.search.SecuredAPIKeyRestrictions
5+
import com.algolia.client.model.search.SecuredApiKeyRestrictions
66
import io.ktor.http.*
77
import kotlinx.serialization.json.JsonArray
88
import kotlinx.serialization.json.jsonObject
99
import kotlinx.serialization.json.jsonPrimitive
1010

1111
/**
12-
* Builds a restriction string based on provided [SecuredAPIKeyRestrictions].
12+
* Builds a restriction string based on provided [SecuredApiKeyRestrictions].
1313
*/
14-
internal fun SearchClient.buildRestrictionString(restriction: SecuredAPIKeyRestrictions): String {
14+
internal fun SearchClient.buildRestrictionString(restriction: SecuredApiKeyRestrictions): String {
1515
return Parameters.build {
1616
restriction.searchParams?.let { searchParams ->
1717
val json = options.json.encodeToJsonElement(SearchParamsObject.serializer(), searchParams).jsonObject

clients/algoliasearch-client-kotlin/client/src/commonTest/kotlin/com/algolia/client/TestSecureApiKey.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package com.algolia.client
22

33
import com.algolia.client.api.SearchClient
4-
import com.algolia.client.extensions.SecuredAPIKeyRestrictions
4+
import com.algolia.client.extensions.SecuredApiKeyRestrictions
55
import com.algolia.client.extensions.generateSecuredApiKey
66
import com.algolia.client.extensions.securedApiKeyRemainingValidity
77
import com.algolia.client.model.search.SearchParamsObject
@@ -14,14 +14,14 @@ class TestSecureApiKey {
1414

1515
@Test
1616
fun securedApiKey() {
17-
val parentAPIKey = "SearchOnlyApiKeyKeptPrivate"
18-
val restriction = SecuredAPIKeyRestrictions(
17+
val parentApiKey = "SearchOnlyApiKeyKeptPrivate"
18+
val restriction = SecuredApiKeyRestrictions(
1919
query = SearchParamsObject(filters = "_tags:user_42"),
2020
validUntil = Clock.System.now() + 2.days,
2121
)
2222

2323
val client = SearchClient("appId", "apiKey")
24-
val securedApiKey = client.generateSecuredApiKey(parentAPIKey, restriction)
24+
val securedApiKey = client.generateSecuredApiKey(parentApiKey, restriction)
2525
val validity = securedApiKeyRemainingValidity(securedApiKey)
2626
assertTrue { validity > 1.days }
2727
}

clients/algoliasearch-client-python/algoliasearch/http/serializer.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from json import dumps
22
from typing import Any, Dict
3+
from urllib.parse import urlencode
34

45
PRIMITIVE_TYPES = (float, bool, bytes, str, int)
56

@@ -12,21 +13,30 @@ class QueryParametersSerializer:
1213
query_parameters: Dict[str, Any] = {}
1314

1415
def parse(self, value) -> Any:
15-
if isinstance(value, dict):
16-
return dumps(value)
17-
elif isinstance(value, list):
16+
if isinstance(value, list):
1817
return ",".join([self.parse(item) for item in value])
18+
elif isinstance(value, dict):
19+
return dumps(value)
1920
elif isinstance(value, bool):
2021
return "true" if value else "false"
2122
else:
2223
return str(value)
2324

25+
def encoded(self) -> str:
26+
return urlencode(
27+
dict(sorted(self.query_parameters.items(), key=lambda val: val[0]))
28+
).replace("+", "%20")
29+
2430
def __init__(self, query_parameters: Dict[str, Any]) -> None:
2531
self.query_parameters = {}
2632
if query_parameters is None:
2733
return
2834
for key, value in query_parameters.items():
29-
self.query_parameters[key] = self.parse(value)
35+
if isinstance(value, dict):
36+
for dkey, dvalue in value.items():
37+
self.query_parameters[dkey] = self.parse(dvalue)
38+
else:
39+
self.query_parameters[key] = self.parse(value)
3040

3141

3242
def bodySerializer(obj: Any) -> dict:

clients/algoliasearch-client-swift/Sources/Search/Extra/SearchClientExtension.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -526,18 +526,18 @@ public extension SearchClient {
526526
/// - returns: String?
527527
func generateSecuredApiKey(
528528
parentApiKey: String,
529-
with restriction: SecuredAPIKeyRestrictions = SecuredAPIKeyRestrictions()
529+
with restriction: SecuredApiKeyRestrictions = SecuredApiKeyRestrictions()
530530
) throws -> String? {
531531
let queryParams = try restriction.toURLEncodedString()
532532
let hash = queryParams.hmac256(withKey: parentApiKey)
533533
return "\(hash)\(queryParams)".data(using: .utf8)?.base64EncodedString()
534534
}
535535

536536
/// Get the remaining validity of a secured API key
537-
/// - parameter securedAPIKey: The secured API key
537+
/// - parameter securedApiKey: The secured API key
538538
/// - returns: TimeInterval?
539-
func getSecuredApiKeyRemainingValidity(for securedAPIKey: String) -> TimeInterval? {
540-
guard let rawDecodedAPIKey = String(data: Data(base64Encoded: securedAPIKey) ?? Data(), encoding: .utf8),
539+
func getSecuredApiKeyRemainingValidity(for securedApiKey: String) -> TimeInterval? {
540+
guard let rawDecodedAPIKey = String(data: Data(base64Encoded: securedApiKey) ?? Data(), encoding: .utf8),
541541
!rawDecodedAPIKey.isEmpty else {
542542
return nil
543543
}

clients/algoliasearch-client-swift/Sources/Search/Extra/SecuredApiKeyRestrictionExtension.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#endif
44
import Foundation
55

6-
public extension SecuredAPIKeyRestrictions {
6+
public extension SecuredApiKeyRestrictions {
77
func toURLEncodedString() throws -> String {
88
var queryDictionary: [String: Any] = [:]
99

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,22 @@ public void run(Map<String, CodegenModel> models, Map<String, CodegenOperation>
9292
throw new CTSException("Cannot find operation for method: " + step.path, test.testName);
9393
}
9494
stepOut.put("stepTemplate", "tests/client/method.mustache");
95-
stepOut.put("isMethod", true); // TODO: remove once dart and kotlin are converted
95+
stepOut.put("isMethod", true); // TODO: remove once kotlin is converted
96+
stepOut.put("hasOperationParams", ope.hasParams);
97+
98+
// set on testOut because we need to wrap everything for java.
99+
testOut.put("isHelper", (boolean) ope.vendorExtensions.getOrDefault("x-helper", false));
100+
testOut.put("isAsync", (boolean) ope.vendorExtensions.getOrDefault("x-asynchronous-helper", true)); // default to true because most api calls are asynchronous
96101
}
97102

98103
stepOut.put("object", step.object);
99104
stepOut.put("path", step.path);
100105

101-
Map<String, Object> requestOptions = new HashMap<>();
102-
paramsType.enhanceParameters(step.requestOptions, requestOptions);
103-
stepOut.put("requestOptions", requestOptions);
106+
if (step.requestOptions != null) {
107+
Map<String, Object> requestOptions = new HashMap<>();
108+
paramsType.enhanceParameters(step.requestOptions, requestOptions);
109+
stepOut.put("requestOptions", requestOptions);
110+
}
104111

105112
if (step.path != null && CUSTOM_METHODS.contains(step.path)) {
106113
stepOut.put("isCustom", true);
@@ -148,10 +155,11 @@ public void run(Map<String, CodegenModel> models, Map<String, CodegenOperation>
148155
stepOut.put("expectedError", step.expected.error.replace(step.path, Helpers.toPascalCase(step.path)));
149156
}
150157
} else if (step.expected.match != null) {
151-
if (step.expected.match instanceof Map) {
152-
Map<String, Object> match = new HashMap<>();
153-
paramsType.enhanceParameters((Map<String, Object>) step.expected.match, match);
154-
stepOut.put("match", match);
158+
Map<String, Object> matchMap = new HashMap<>();
159+
if (step.expected.match instanceof Map match) {
160+
paramsType.enhanceParameters(match, matchMap);
161+
stepOut.put("match", matchMap);
162+
stepOut.put("matchIsObject", true);
155163
} else {
156164
stepOut.put("match", step.expected.match);
157165
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public void run(Map<String, CodegenModel> models, Map<String, CodegenOperation>
6969
boolean isHelper = (boolean) ope.vendorExtensions.getOrDefault("x-helper", false);
7070
if (!cts.containsKey(operationId)) {
7171
if (isHelper) {
72-
continue;
72+
continue; // some helpers don't have tests
7373
}
7474

7575
throw new CTSException(

playground/csharp/Playground/Playgrounds/Search.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ await PlaygroundHelper.Start("Deleting API Key", async () =>
201201

202202
Console.WriteLine("--- Generate Secured API Keys `GenerateSecuredApiKeys` ---");
203203
var generateSecuredApiKeys = _client.GenerateSecuredApiKey(_configuration.SearchApiKey,
204-
new SecuredAPIKeyRestrictions
204+
new SecuredApiKeyRestrictions
205205
{
206206
RestrictIndices = [DefaultIndex],
207207
});

playground/swift/playground/playground/main.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ guard let apiKey = Bundle.main.infoDictionary?["ALGOLIA_ADMIN_KEY"] as? String e
1818
}
1919

2020
guard applicationID != "" && apiKey != "" else {
21-
fatalError("AppID and APIKey must be filled in your Info.plist file")
21+
fatalError("AppID and ApiKey must be filled in your Info.plist file")
2222
}
2323

2424
struct Contact: Codable {

scripts/cts/runCts.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as fsp from 'fs/promises';
22

3-
import { run, runComposerInstall, toAbsolutePath } from '../common.js';
3+
import { isVerbose, run, runComposerInstall, toAbsolutePath } from '../common.js';
44
import { createSpinner } from '../spinners.js';
55
import type { Language } from '../types.js';
66

@@ -17,7 +17,7 @@ async function runCtsOne(language: string): Promise<void> {
1717
await run('dart test', { cwd, language });
1818
break;
1919
case 'go':
20-
await run('go test -race -count 1 ./...', {
20+
await run(`go test -race -count 1 ${isVerbose() ? '-v' : ''} ./...`, {
2121
cwd,
2222
language,
2323
});

specs/search/helpers/generateSecuredApiKey.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
method:
22
get:
33
x-helper: true
4+
x-asynchronous-helper: false
45
tags:
56
- Api Keys
67
operationId: generateSecuredApiKey
@@ -21,7 +22,7 @@ method:
2122
The generated API key can have the same restrictions as the parent API key, or be more restrictive.
2223
parameters:
2324
- in: query
24-
name: apiKey
25+
name: parentApiKey
2526
description: API key from which the secured API key will inherit its restrictions.
2627
required: true
2728
schema:
@@ -31,7 +32,7 @@ method:
3132
description: Restrictions to add to the API key.
3233
required: true
3334
schema:
34-
$ref: '#/securedAPIKeyRestrictions'
35+
$ref: '#/securedApiKeyRestrictions'
3536
responses:
3637
'200':
3738
description: OK
@@ -42,7 +43,7 @@ method:
4243
'400':
4344
$ref: '../../common/responses/IndexNotFound.yml'
4445

45-
securedAPIKeyRestrictions:
46+
securedApiKeyRestrictions:
4647
type: object
4748
additionalProperties: false
4849
properties:

templates/csharp/tests/client/method.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{{^useEchoRequester}}var res = {{/useEchoRequester}}await client.{{#lambda.pascalcase}}{{#path}}.{{.}}{{/path}}{{/lambda.pascalcase}}Async{{#isGeneric}}<Object>{{/isGeneric}}({{#parametersWithDataType}}{{> tests/generateParams}}{{^-last}},{{/-last}}{{/parametersWithDataType}}{{#hasRequestOptions}}, new RequestOptions(){
1+
{{^useEchoRequester}}var res = {{/useEchoRequester}}{{#isAsync}}await {{/isAsync}}client.{{#lambda.pascalcase}}{{#path}}.{{.}}{{/path}}{{/lambda.pascalcase}}{{#isAsync}}Async{{/isAsync}}{{#isGeneric}}<Object>{{/isGeneric}}({{#parametersWithDataType}}{{> tests/generateParams}}{{^-last}},{{/-last}}{{/parametersWithDataType}}{{#hasRequestOptions}}, new RequestOptions(){
22
{{#requestOptions.queryParameters}}
33
QueryParameters = new Dictionary<string, object>(){ {{#parametersWithDataType}} {"{{{key}}}", {{> tests/requests/requestOptionsParams}} } {{^-last}},{{/-last}}{{/parametersWithDataType}} },
44
{{/requestOptions.queryParameters}}

0 commit comments

Comments
 (0)