Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,24 @@ export class AlgoliaError extends Error {
}
}

export class IndexNotFoundError extends AlgoliaError {
constructor(indexName: string) {
super(`${indexName} does not exist`, 'IndexNotFoundError');
}
}

export class IndicesInSameAppError extends AlgoliaError {
constructor() {
super('Indices are in the same application. Use operationIndex instead.', 'IndicesInSameAppError');
}
}

export class IndexAlreadyExistsError extends AlgoliaError {
constructor(indexName: string) {
super(`${indexName} index already exists.`, 'IndexAlreadyExistsError');
}
}

export class ErrorWithStackTrace extends AlgoliaError {
stackTrace: StackFrame[];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,13 @@ private Map<String, Snippet[]> loadSnippets(Map<String, CodegenOperation> operat
if (ope == null || !(boolean) ope.vendorExtensions.getOrDefault("x-helper", false)) {
continue;
}

List<String> availableLanguages = (List<String>) ope.vendorExtensions.getOrDefault("x-available-languages", new ArrayList<>());

if (availableLanguages.size() > 0 && !availableLanguages.contains(language)) {
continue;
}

Snippet newSnippet = new Snippet(step.method, test.testName, step.parameters, step.requestOptions);
Snippet[] existing = snippets.get(step.method);
if (existing == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,20 @@ public void run(Map<String, CodegenModel> models, Map<String, CodegenOperation>

throw new CTSException("Cannot find operation for method: " + step.method, test.testName);
}

boolean isHelper = (boolean) ope.vendorExtensions.getOrDefault("x-helper", false);

if (isHelper) {
List<String> availableLanguages = (List<String>) ope.vendorExtensions.getOrDefault(
"x-available-languages",
new ArrayList<>()
);

if (availableLanguages.size() > 0 && !availableLanguages.contains(language)) {
continue skipTest;
}
}

stepOut.put("stepTemplate", "tests/client/method.mustache");
stepOut.put("isMethod", true); // TODO: remove once kotlin is converted
stepOut.put("hasParams", ope.hasParams);
Expand All @@ -123,7 +137,6 @@ public void run(Map<String, CodegenModel> models, Map<String, CodegenOperation>
stepOut.put("returnsBoolean", ope.returnType.equals("Boolean")); // ruby requires a ? for boolean functions.
}

boolean isHelper = (boolean) ope.vendorExtensions.getOrDefault("x-helper", false);
stepOut.put("isHelper", isHelper);
// default to true because most api calls are asynchronous
stepOut.put("isAsyncMethod", (boolean) ope.vendorExtensions.getOrDefault("x-asynchronous-helper", true));
Expand Down
27 changes: 0 additions & 27 deletions playground/javascript/node/realtimePersonalization.ts

This file was deleted.

3 changes: 3 additions & 0 deletions scripts/cts/runCts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getTestOutputFolder } from '../config.ts';
import { createSpinner } from '../spinners.ts';
import type { Language } from '../types.ts';

import { assertValidAccountCopyIndex } from './testServer/accountCopyIndex.ts';
import { printBenchmarkReport } from './testServer/benchmark.ts';
import { assertChunkWrapperValid } from './testServer/chunkWrapper.ts';
import { startTestServer } from './testServer/index.ts';
Expand Down Expand Up @@ -147,10 +148,12 @@ export async function runCts(
if (withClientServer && (clients.includes('search') || clients.includes('all') || process.platform === 'darwin')) {
// the macos swift CI also runs the clients tests
const skip = (lang: Language): number => (languages.includes(lang) ? 1 : 0);
const only = skip;

assertValidTimeouts(languages.length);
assertChunkWrapperValid(languages.length - skip('dart'));
assertValidReplaceAllObjects(languages.length - skip('dart'));
assertValidAccountCopyIndex(only('javascript'));
assertValidReplaceAllObjectsFailed(languages.length - skip('dart'));
assertValidReplaceAllObjectsScopes(languages.length - skip('dart'));
assertValidWaitForApiKey(languages.length - skip('dart'));
Expand Down
207 changes: 207 additions & 0 deletions scripts/cts/testServer/accountCopyIndex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
import type { Server } from 'http';

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

import { setupServer } from './index.ts';

const aciState: Record<
string,
{
setSettingsCount: number;
getSettingsCount: number;
saveRulesCount: number;
browseRulesCount: number;
saveSynonymsCount: number;
browseSynonymsCount: number;
saveObjectsCount: number;
browseObjectsCount: number;
waitTaskCount: number;
successful: boolean;
}
> = {};

export function assertValidAccountCopyIndex(expectedCount: number): void {
expect(Object.keys(aciState)).to.have.length(expectedCount);
for (const lang in aciState) {
expect(aciState[lang].waitTaskCount).to.equal(4);
}
}

function addRoutes(app: Express): void {
app.use(express.urlencoded({ extended: true }));
app.use(
express.json({
type: ['application/json', 'text/plain'], // the js client sends the body as text/plain
}),
);

app.get('/1/indexes/:indexName/settings', (req, res) => {
const lang = req.params.indexName.match(/^cts_e2e_account_copy_index_(source|destination)_(.*)$/)?.[2] as string;

if (!aciState[lang] || aciState[lang].successful) {
aciState[lang] = {
setSettingsCount: 0,
getSettingsCount: 0,
saveRulesCount: 0,
browseRulesCount: 0,
saveSynonymsCount: 0,
browseSynonymsCount: 0,
saveObjectsCount: 0,
browseObjectsCount: 0,
waitTaskCount: 0,
successful: false,
};
} else {
expect(aciState).to.include.keys(lang);
}

aciState[lang].getSettingsCount++;

if (req.params.indexName.includes('destination')) {
res.status(404).json({ message: 'Index not found' });
} else {
res.status(200).json({
minWordSizefor1Typo: 4,
minWordSizefor2Typos: 8,
hitsPerPage: 100,
maxValuesPerFacet: 100,
paginationLimitedTo: 10,
exactOnSingleWordQuery: 'attribute',
ranking: ['typo', 'geo', 'words', 'filters', 'proximity', 'attribute', 'exact', 'custom'],
separatorsToIndex: '',
removeWordsIfNoResults: 'none',
queryType: 'prefixLast',
highlightPreTag: '<em>',
highlightPostTag: '</em>',
alternativesAsExact: ['ignorePlurals', 'singleWordSynonym'],
});
}
});

app.put('/1/indexes/:indexName/settings', (req, res) => {
const lang = req.params.indexName.match(/^cts_e2e_account_copy_index_destination_(.*)$/)?.[1] as string;
expect(aciState).to.include.keys(lang);

aciState[lang].setSettingsCount++;

res.json({ taskID: 123 + aciState[lang].setSettingsCount, updatedAt: '2021-01-01T00:00:00.000Z' });
});

app.post('/1/indexes/:indexName/rules/search', (req, res) => {
const lang = req.params.indexName.match(/^cts_e2e_account_copy_index_source_(.*)$/)?.[1] as string;
expect(aciState).to.include.keys(lang);

aciState[lang].browseRulesCount++;

res.json({
hits: [
{
conditions: [
{
alternatives: true,
anchoring: 'contains',
pattern: 'zorro',
},
],
consequence: {
params: {
ignorePlurals: 'true',
},
filterPromotes: true,
promote: [
{
objectIDs: ['\u00C6on Flux'],
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

position: 0,
},
],
},
description: 'test_rule',
enabled: true,
objectID: 'qr-1725004648916',
},
],
nbHits: 1,
nbPages: 1,
page: 0,
});
});

app.post('/1/indexes/:indexName/rules/batch', (req, res) => {
const lang = req.params.indexName.match(/^cts_e2e_account_copy_index_destination_(.*)$/)?.[1] as string;
expect(aciState).to.include.keys(lang);

aciState[lang].saveRulesCount++;

res.json({ taskID: 456 + aciState[lang].saveRulesCount, updatedAt: '2021-01-01T00:00:00.000Z' });
});

app.post('/1/indexes/:indexName/synonyms/search', (req, res) => {
const lang = req.params.indexName.match(/^cts_e2e_account_copy_index_source_(.*)$/)?.[1] as string;
expect(aciState).to.include.keys(lang);

aciState[lang].browseSynonymsCount++;

res.json({
hits: [
{
objectID: 'foo',
},
],
nbHits: 1,
nbPages: 1,
page: 0,
});
});

app.post('/1/indexes/:indexName/synonyms/batch', (req, res) => {
const lang = req.params.indexName.match(/^cts_e2e_account_copy_index_destination_(.*)$/)?.[1] as string;
expect(aciState).to.include.keys(lang);

aciState[lang].saveSynonymsCount++;

res.json({ taskID: 789 + aciState[lang].saveSynonymsCount, updatedAt: '2021-01-01T00:00:00.000Z' });
});

app.post('/1/indexes/:indexName/browse', (req, res) => {
const lang = req.params.indexName.match(/^cts_e2e_account_copy_index_source_(.*)$/)?.[1] as string;
expect(aciState).to.include.keys(lang);

aciState[lang].browseObjectsCount++;

res.json({
page: 0,
nbHits: 1,
nbPages: 1,
hitsPerPage: 1000,
query: '',
params: '',
hits: [{ objectID: 'bar' }],
});
});

app.post('/1/indexes/:indexName/batch', (req, res) => {
const lang = req.params.indexName.match(/^cts_e2e_account_copy_index_destination_(.*)$/)?.[1] as string;
expect(aciState).to.include.keys(lang);

aciState[lang].saveObjectsCount++;

res.json({ taskID: 10 + aciState[lang].saveObjectsCount, updatedAt: '2021-01-01T00:00:00.000Z' });
});

app.get('/1/indexes/:indexName/task/:taskID', (req, res) => {
const lang = req.params.indexName.match(/^cts_e2e_account_copy_index_destination_(.*)$/)?.[1] as string;
expect(aciState).to.include.keys(lang);

aciState[lang].waitTaskCount++;

res.json({ status: 'published', updatedAt: '2021-01-01T00:00:00.000Z' });
});
}

export function accountCopyIndexServer(): Promise<Server> {
// this server is used to simulate the responses for the accountCopyIndex method,
// and uses a state machine to determine if the logic is correct.
return setupServer('accountCopyIndex', 6687, addRoutes);
}
2 changes: 2 additions & 0 deletions scripts/cts/testServer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { createSpinner } from '../../spinners.ts';
import type { CTSType } from '../runCts.ts';

import { expect } from 'chai';
import { accountCopyIndexServer } from './accountCopyIndex.ts';
import { algoliaMockServer } from './algoliaMock.ts';
import { apiKeyServer } from './apiKey.ts';
import { benchmarkServer } from './benchmark.ts';
Expand All @@ -26,6 +27,7 @@ export async function startTestServer(suites: Record<CTSType, boolean>): Promise
timeoutServer(),
gzipServer(),
timeoutServerBis(),
accountCopyIndexServer(),
replaceAllObjectsServer(),
replaceAllObjectsServerFailed(),
replaceAllObjectsScopesServer(),
Expand Down
5 changes: 5 additions & 0 deletions specs/common/responses/IndexAlreadyExists.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
description: Destination index already exists.
content:
application/json:
schema:
$ref: '../schemas/ErrorBase.yml'
5 changes: 5 additions & 0 deletions specs/common/responses/IndexInSameApp.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
description: Indices are in the same application. Use operationIndex instead.
content:
application/json:
schema:
$ref: '../schemas/ErrorBase.yml'
Loading