diff --git a/tests/CTS/client/search/basic.json b/tests/CTS/client/search/basic.json new file mode 100644 index 0000000000..70b8318e4a --- /dev/null +++ b/tests/CTS/client/search/basic.json @@ -0,0 +1,143 @@ +[ + { + "testName": "client throws with invalid parameters", + "autoCreateClient": false, + "steps": [ + { + "type": "createClient", + "parameters": { + "apiKey": "blah" + }, + "expected": { + "error": "appId is missing!" + } + } + ] + }, + { + "testName": "client has instance variables for appId and apiKey", + "autoCreateClient": false, + "steps": [ + { + "type": "createClient", + "parameters": { + "appId": "my-app-id", + "apiKey": "my-api-key" + } + }, + { + "type": "variable", + "object": "$client", + "path": ["appId"], + "expected": { "match": "my-app-id" } + }, + { + "type": "variable", + "object": "$client", + "path": ["apiKey"], + "expected": { "match": "my-api-key" } + } + ] + }, + { + "testName": "sets user agent", + "steps": [ + { + "type": "method", + "object": "$client", + "path": ["setUserAgent"], + "parameters": ["hello"] + }, + { + "type": "method", + "object": "$client", + "path": ["getUserAgent"], + "expected": { "match": "hello" } + } + ] + }, + { + "testName": "save objects and perform a basic search", + "autoCreateIndex": true, + "steps": [ + { + "type": "method", + "object": "$index", + "path": ["saveObjects"], + "parameters": [ + [ + { + "objectID": "julien-lemoine", + "company": "Algolia", + "name": "Julien Lemoine" + }, + { + "objectID": "nicolas-dessaigne", + "company": "Algolia", + "name": "Nicolas Dessaigne" + }, + { "company": "Amazon", "name": "Jeff Bezos" }, + { "company": "Apple", "name": "Steve Jobs" }, + { "company": "Apple", "name": "Steve Wozniak" }, + { "company": "Arista Networks", "name": "Jayshree Ullal" }, + { "company": "Google", "name": "Larry Page" }, + { "company": "Google", "name": "Rob Pike" }, + { "company": "Google", "name": "Serguey Brin" }, + { "company": "Microsoft", "name": "Bill Gates" }, + { "company": "SpaceX", "name": "Elon Musk" }, + { "company": "Tesla", "name": "Elon Musk" }, + { "company": "Yahoo", "name": "Marissa Mayer" } + ], + { + "autoGenerateObjectIDIfNotExist": true + } + ] + }, + { + "type": "method", + "object": "$index", + "path": ["setSettings"], + "parameters": [{ "attributesForFaceting": ["searchable(company)"] }] + }, + { + "type": "method", + "object": "$index", + "path": ["search"], + "parameters": ["algolia"], + "expected": { + "match": [ + { + "objectContaining": { + "name": "Julien Lemoine" + } + }, + { + "objectContaining": { + "name": "Nicolas Dessaigne" + } + } + ] + } + }, + { + "type": "method", + "object": "$index", + "path": ["search"], + "parameters": [ + "elon", + { + "facets": ["*"], + "facetFilters": [["company:tesla", "company:spacex"]] + } + ], + "expected": { + "length": 2, + "match": [ + { "objectContaining": { "company": "SpaceX" } }, + { "objectContaining": { "company": "Tesla" } } + ] + } + } + ] + } +] diff --git a/tests/CTS/client/templates/javascript/createClient.mustache b/tests/CTS/client/templates/javascript/createClient.mustache new file mode 100644 index 0000000000..f5e85aa596 --- /dev/null +++ b/tests/CTS/client/templates/javascript/createClient.mustache @@ -0,0 +1 @@ +new {{client}}('{{parameters.appId}}', '{{parameters.apiKey}}'); \ No newline at end of file diff --git a/tests/CTS/client/templates/javascript/expected.mustache b/tests/CTS/client/templates/javascript/expected.mustache new file mode 100644 index 0000000000..7a56bc0d4e --- /dev/null +++ b/tests/CTS/client/templates/javascript/expected.mustache @@ -0,0 +1,5 @@ +{{#error}} +{{/error}} +{{#length}} + expect(result).toHaveLength({{length}}); +{{/length}} \ No newline at end of file diff --git a/tests/CTS/client/templates/javascript/method.mustache b/tests/CTS/client/templates/javascript/method.mustache new file mode 100644 index 0000000000..f77603d3eb --- /dev/null +++ b/tests/CTS/client/templates/javascript/method.mustache @@ -0,0 +1 @@ +await {{object}}{{#path}}.{{.}}{{/path}}({{{parameters}}}); \ No newline at end of file diff --git a/tests/CTS/client/templates/javascript/step.mustache b/tests/CTS/client/templates/javascript/step.mustache new file mode 100644 index 0000000000..0d1b1d5244 --- /dev/null +++ b/tests/CTS/client/templates/javascript/step.mustache @@ -0,0 +1,16 @@ +{{#isCreateClient}} + const $client = {{> createClient}} + result = $client; +{{/isCreateClient}} + +{{#isVariable}} + result = {{> variable}} +{{/isVariable}} + +{{#isMethod}} + result = {{> method}} +{{/isMethod}} + +{{#expected}} + {{> expected}} +{{/expected}} \ No newline at end of file diff --git a/tests/CTS/client/templates/javascript/suite.mustache b/tests/CTS/client/templates/javascript/suite.mustache new file mode 100644 index 0000000000..cd08bd729a --- /dev/null +++ b/tests/CTS/client/templates/javascript/suite.mustache @@ -0,0 +1,42 @@ +import { {{client}} } from '{{{import}}}'; + +const appId = process.env.ALGOLIA_APPLICATION_ID || 'test_app_id'; +const apiKey = process.env.ALGOLIA_SEARCH_KEY || 'test_api_key'; + +function createClient() { + return new {{client}}(appId, apiKey{{#hasRegionalHost}}, 'us'{{/hasRegionalHost}}); +} + +async function createIndex() { + // TODO +} + +{{#blocks}} +describe('{{operationId}}', () => { + {{#tests}} + test('{{testName}}', async () => { + {{#autoCreateClient}} + const $client = createClient(); + {{/autoCreateClient}} + {{#autoCreateIndex}} + const $index = await createIndex(); + {{/autoCreateIndex}} + + let result; + {{#steps}} + {{#expectedError}} + expect(() => { + {{> step}} + }).toThrowError("{{expectedError}}") + {{/expectedError}} + + {{^expectedError}} + {{> step}} + {{/expectedError}} + {{/steps}} + }); + + {{/tests}} +}) + +{{/blocks}} diff --git a/tests/CTS/client/templates/javascript/variable.mustache b/tests/CTS/client/templates/javascript/variable.mustache new file mode 100644 index 0000000000..7b079937d0 --- /dev/null +++ b/tests/CTS/client/templates/javascript/variable.mustache @@ -0,0 +1 @@ +{{object}}{{#path}}.{{.}}{{/path}}; \ No newline at end of file diff --git a/tests/CTS/integration/.gitkeep b/tests/CTS/integrations/.gitkeep similarity index 100% rename from tests/CTS/integration/.gitkeep rename to tests/CTS/integrations/.gitkeep diff --git a/tests/output/javascript/tests/client/search.test.ts b/tests/output/javascript/tests/client/search.test.ts new file mode 100644 index 0000000000..cc7850c363 --- /dev/null +++ b/tests/output/javascript/tests/client/search.test.ts @@ -0,0 +1,90 @@ +import { SearchApi } from '@algolia/client-search'; + +const appId = process.env.ALGOLIA_APPLICATION_ID || 'test_app_id'; +const apiKey = process.env.ALGOLIA_SEARCH_KEY || 'test_api_key'; + +function createClient() { + return new SearchApi(appId, apiKey); +} + +async function createIndex() { + // TODO +} + +describe('basic', () => { + test('client throws with invalid parameters', async () => { + let result; + expect(() => { + const $client = new SearchApi('', 'blah'); + result = $client; + }).toThrowError('appId is missing!'); + }); + + test('client has instance variables for appId and apiKey', async () => { + let result; + + const $client = new SearchApi('my-app-id', 'my-api-key'); + result = $client; + + result = $client.appId; + + result = $client.apiKey; + }); + + test('sets user agent', async () => { + const $client = createClient(); + + let result; + + result = await $client.setUserAgent('hello'); + + result = await $client.getUserAgent(); + }); + + test('save objects and perform a basic search', async () => { + const $client = createClient(); + const $index = await createIndex(); + + let result; + + result = await $index.saveObjects( + [ + { + objectID: 'julien-lemoine', + company: 'Algolia', + name: 'Julien Lemoine', + }, + { + objectID: 'nicolas-dessaigne', + company: 'Algolia', + name: 'Nicolas Dessaigne', + }, + { company: 'Amazon', name: 'Jeff Bezos' }, + { company: 'Apple', name: 'Steve Jobs' }, + { company: 'Apple', name: 'Steve Wozniak' }, + { company: 'Arista Networks', name: 'Jayshree Ullal' }, + { company: 'Google', name: 'Larry Page' }, + { company: 'Google', name: 'Rob Pike' }, + { company: 'Google', name: 'Serguey Brin' }, + { company: 'Microsoft', name: 'Bill Gates' }, + { company: 'SpaceX', name: 'Elon Musk' }, + { company: 'Tesla', name: 'Elon Musk' }, + { company: 'Yahoo', name: 'Marissa Mayer' }, + ], + { autoGenerateObjectIDIfNotExist: true } + ); + + result = await $index.setSettings({ + attributesForFaceting: ['searchable(company)'], + }); + + result = await $index.search('algolia'); + + result = await $index.search('elon', { + facets: ['*'], + facetFilters: [['company:tesla', 'company:spacex']], + }); + + expect(result).toHaveLength(2); + }); +}); diff --git a/tests/package.json b/tests/package.json index 82fd1ec450..8afcd6791b 100644 --- a/tests/package.json +++ b/tests/package.json @@ -7,8 +7,9 @@ "scripts": { "build": "tsc", "lint:fix": "yarn workspace javascript-tests lint:fix", - "generate": "yarn generate:methods:requets ${0:-javascript} ${1:-search}", + "generate": "yarn generate:methods:requets ${0:-javascript} ${1:-search} && yarn generate:client ${0:-javascript} ${1:-search}", "generate:methods:requets": "node dist/tests/src/methods/requests/main.js ${0:-javascript} ${1:-search}", + "generate:client": "node dist/tests/src/client/main.js ${0:-javascript} ${1:-search}", "start": "yarn build && yarn generate ${0:-javascript} ${1:-search} && yarn lint:fix" }, "devDependencies": { diff --git a/tests/src/client/.gitkeep b/tests/src/client/.gitkeep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/tests/src/client/generate.ts b/tests/src/client/generate.ts new file mode 100644 index 0000000000..dbb673f463 --- /dev/null +++ b/tests/src/client/generate.ts @@ -0,0 +1,114 @@ +import fsp from 'fs/promises'; +import Mustache from 'mustache'; + +import { + walk, + extensionForLanguage, + packageNames, + createClientName, +} from '../utils'; +import { TestsBlock, Test } from './types'; + +async function loadTests(client: string) { + const testBlocks: TestsBlock[] = []; + for await (const file of walk(`./CTS/client/${client}`)) { + if (!file.name.endsWith('.json')) { + continue; + } + const fileName = file.name.replace('.json', ''); + const fileContent = (await fsp.readFile(file.path)).toString(); + + if (!fileContent) { + throw new Error(`cannot read empty file ${fileName} - ${client} client`); + } + + const tests: Test[] = JSON.parse(fileContent).map((testCase) => { + if (!testCase.testName) { + throw new Error( + `Cannot have a test with no name ${fileName} - ${client} client` + ); + } + return { + autoCreateClient: true, + autoCreateIndex: false, + ...testCase, + }; + }); + + testBlocks.push({ + operationId: fileName, + tests, + }); + } + + return testBlocks; +} + +async function loadTemplates(language: string) { + const templates: Record = {}; + for await (const file of walk(`./CTS/client/templates/${language}`)) { + if (!file.name.endsWith('.mustache')) { + continue; + } + const type = file.name.replace('.mustache', ''); + const fileContent = (await fsp.readFile(file.path)).toString(); + templates[type] = fileContent; + } + return templates; +} + +export async function generateTests(language: string, client: string) { + const testsBlocks = await loadTests(client); + + const outputPath = `output/${language}/tests/client/`; + await fsp.mkdir(outputPath, { recursive: true }); + const { suite: template, ...partialTemplates } = await loadTemplates( + language + ); + const code = Mustache.render( + template, + { + import: packageNames[language][client], + client: createClientName(client), + blocks: modifyForMustache(testsBlocks), + }, + partialTemplates + ); + await fsp.writeFile( + `${outputPath}/${client}.${extensionForLanguage[language]}`, + code + ); +} + +function modifyForMustache(blocks: TestsBlock[]) { + return blocks.map(({ tests, ...rest }) => ({ + ...rest, + tests: tests.map(({ steps, ...rest }) => ({ + ...rest, + steps: steps.map((step) => { + const modified = { + ...step, + isCreateClient: step.type === 'createClient', + isVariable: step.type === 'variable', + isMethod: step.type === 'method', + }; + + if (step.type === 'method') { + if (step.parameters) { + let serialized = JSON.stringify(step.parameters); + serialized = serialized.slice(1, serialized.length - 1); + // @ts-expect-error + modified.parameters = serialized; + } + } + + if (step.expected && step.expected.error) { + // @ts-expect-error + modified.expectedError = step.expected.error; + } + + return modified; + }), + })), + })); +} diff --git a/tests/src/client/main.ts b/tests/src/client/main.ts new file mode 100644 index 0000000000..b7aaff200b --- /dev/null +++ b/tests/src/client/main.ts @@ -0,0 +1,18 @@ +import { generateTests } from './generate'; +import { parseCLI } from '../utils'; + +async function main() { + const { lang, client } = parseCLI(process.argv, 'generate:client'); + /* eslint-disable-next-line no-console */ + console.log(`Generating CTS > generate:client for ${lang}-${client}`); + + try { + await generateTests(lang, client); + } catch (e) { + if (e instanceof Error) { + console.error(e); + } + } +} + +main(); diff --git a/tests/src/client/types.ts b/tests/src/client/types.ts new file mode 100644 index 0000000000..1b9c0c1c1a --- /dev/null +++ b/tests/src/client/types.ts @@ -0,0 +1,47 @@ +export type Test = { + testName: string; + autoCreateClient?: boolean; // `true` by default + autoCreateIndex?: boolean; // `false` by default + steps: Step[]; +}; + +type Step = CreateClientStep | VariableStep | MethodStep; + +type CreateClientStep = { + type: 'createClient'; + parameters: { + appId: string; + apiKey: string; + }; + expected?: Expected; +}; + +type VariableStep = { + type: 'variable'; + object: string; + path: string[]; + expected?: Expected; +}; + +type MethodStep = { + type: 'method'; + object: string; + path: string[]; + parameters?: any; + expected?: Expected; +}; + +type Expected = { + length?: number; + error?: string; + match?: { objectContaining: object } | any; +}; + +export type TestsBlock = { + operationId: string; + tests: Test[]; +}; + +type AllTests = { + [client: string]: TestsBlock[]; +}; diff --git a/tests/src/methods/requests/main.ts b/tests/src/methods/requests/main.ts index b8b5ba95bf..0d39752c24 100644 --- a/tests/src/methods/requests/main.ts +++ b/tests/src/methods/requests/main.ts @@ -1,38 +1,15 @@ -/* eslint-disable no-console */ - import { generateTests } from './generate'; -import { packageNames } from '../../utils'; - -function printUsage(): void { - console.log(`usage: generateCTS language client`); - // eslint-disable-next-line no-process-exit - process.exit(1); -} - -async function parseCLI(args: string[]): Promise { - if (args.length < 3) { - console.log('not enough arguments'); - printUsage(); - } - - const lang = args[2]; - const client = args[3]; - - if (!(lang in packageNames)) { - console.log('Unknown language', lang); - // eslint-disable-next-line no-process-exit - process.exit(1); - } - if (!(client in packageNames[lang])) { - console.log('Unknown client', client); - // eslint-disable-next-line no-process-exit - process.exit(1); - } +import { parseCLI } from '../../utils'; - console.log(`Generating CTS for ${lang}-${client}`); +async function main() { + const { lang, client } = parseCLI(process.argv, 'generate:methods:requests'); + /* eslint-disable-next-line no-console */ + console.log( + `Generating CTS > generate:methods:requests for ${lang}-${client}` + ); try { - await generateTests(args[2], args[3]); + await generateTests(lang, client); } catch (e) { if (e instanceof Error) { console.error(e); @@ -40,4 +17,4 @@ async function parseCLI(args: string[]): Promise { } } -parseCLI(process.argv); +main(); diff --git a/tests/src/utils.ts b/tests/src/utils.ts index 3377e72b24..b6e7d58807 100644 --- a/tests/src/utils.ts +++ b/tests/src/utils.ts @@ -61,3 +61,38 @@ export const extensionForLanguage: Record = { javascript: 'test.ts', java: 'java', }; + +function printUsage(commandName: string): void { + console.log(`usage: ${commandName} language client`); + // eslint-disable-next-line no-process-exit + process.exit(1); +} + +export function parseCLI( + args: string[], + commandName: string +): { lang: string; client: string } { + if (args.length < 3) { + console.log('not enough arguments'); + printUsage(commandName); + } + + const lang = args[2]; + const client = args[3]; + + if (!(lang in packageNames)) { + console.log('Unknown language', lang); + // eslint-disable-next-line no-process-exit + process.exit(1); + } + if (!(client in packageNames[lang])) { + console.log('Unknown client', client); + // eslint-disable-next-line no-process-exit + process.exit(1); + } + + return { + lang, + client, + }; +}