diff --git a/README.md b/README.md index 28e9142..132f0e6 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,67 @@ Jest preset for running tests with local ElasticSearch, OpenSearch and ZincSearch. +## Usage +**Prerequisite:** +ElasticSearch and OpenSearch relies on Java, please make sure you have Java installed and `JAVA_HOME` is set. -install +**1. install library** ```bash npm install --save-dev @geek-fun/jest-search ``` + +**2. create config file `jest-search-config.js`** +```javascript +module.exports = () => { + return { + engine: 'elasticearch', // or 'opensearch' or 'zincsearch' + version: '8.8.2', + port: 9200, + binaryLocation: '', // optional + clusterName: 'jest-search-local', + nodeName: 'jest-search-local', + indexes: [ + { + name: 'index-name', + body: { + settings: { + number_of_shards: '1', + number_of_replicas: '1' + }, + aliases: { + 'your-alias': {} + }, + mappings: { + dynamic: false, + properties: { + id: { + type: 'keyword' + } + } + } + } + } + ] + }; +}; +``` + +**3. modify the `jest-config.js`** +```javascript +module.exports = { + preset: '@geek-fun/jest-search', +}; +``` + +**3. play with your test** +```typescript +// tests/utils/helper.ts sample utils to add item for test +export const saveBook = async (bookDoc: { name: string; author: string }) => { + await esClient.index({ index, body: bookDoc, refresh: true }); +}; + +// tests/book.test.ts sample test +beforeAll(async () => { + await saveBook(mockBook); +}); +``` diff --git a/package-lock.json b/package-lock.json index f9f75c3..3ec7192 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "jest-search", - "version": "0.0.1", + "name": "@geek-fun/jest-search", + "version": "0.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "jest-search", - "version": "0.0.1", + "name": "@geek-fun/jest-search", + "version": "0.0.3", "license": "MIT", "dependencies": { "debug": "^4.3.4", diff --git a/package.json b/package.json index b44cf3c..2d06eac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@geek-fun/jest-search", - "version": "0.0.2", + "version": "0.0.3", "main": "dist/index.js", "types": "dist/index.d.ts", "description": "Jest preset for running tests with local search platform", diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..e05add0 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,8 @@ +export const DISABLE_PROXY = { + env: { http_proxy: undefined, https_proxy: undefined, all_proxy: undefined }, +}; +export const ARTIFACTS = { + ES: 'https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch', + OS: 'https://artifacts.opensearch.org/releases/bundle/opensearch', + ZINC: 'https://github.com/zinclabs/zinc/releases/download', +}; diff --git a/src/engine.ts b/src/engine.ts index 89fa0c0..08efa7a 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -6,6 +6,7 @@ import download from 'download-tarball'; import { execSync } from 'child_process'; import path from 'path'; import { debug } from './debug'; +import { ARTIFACTS, DISABLE_PROXY } from './constants'; export enum EngineType { ZINC = 'zinc', @@ -14,23 +15,18 @@ export enum EngineType { } export type EngineOptions = { - engine?: EngineType; - version?: string; - binaryLocation?: string; - clusterName?: string; - nodeName?: string; - port?: number; - indexes?: Array; -}; - -const artifacts = { - ES: 'https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch', - OS: 'https://artifacts.opensearch.org/releases/bundle/opensearch', - ZINC: 'https://github.com/zinclabs/zinc/releases/download', + engine: EngineType; + version: string; + binaryLocation: string; + clusterName: string; + nodeName: string; + port: number; + indexes: Array<{ name: string; body: unknown }>; }; +type ConfiguredOptions = Omit & { binaryFilepath: string }; let server: execa.ExecaChildProcess; -let engineOptions: EngineOptions; +let engineOptions: ConfiguredOptions; const getEngineResourceURL = async (engine: EngineType, version: string) => { const { sysName, arch } = await platform(); const engines: { @@ -38,13 +34,13 @@ const getEngineResourceURL = async (engine: EngineType, version: string) => { } = { [EngineType.ELASTICSEARCH]: () => parseInt(version.charAt(0)) >= 7 - ? `${artifacts.ES}-${version}-${sysName}-${arch}.tar.gz` - : `${artifacts.ES}-${version}.tar.gz`, + ? `${ARTIFACTS.ES}-${version}-${sysName}-${arch}.tar.gz` + : `${ARTIFACTS.ES}-${version}.tar.gz`, [EngineType.OPENSEARCH]: () => - `${artifacts.OS}/${version}/opensearch-${version}-${sysName}-${arch.slice(1, 4)}.tar.gz`, + `${ARTIFACTS.OS}/${version}/opensearch-${version}-${sysName}-${arch.slice(1, 4)}.tar.gz`, [EngineType.ZINC]: () => - `${artifacts.ZINC}/v${version}/zinc_${version}_${sysName}_${arch}.tar.gz`, + `${ARTIFACTS.ZINC}/v${version}/zinc_${version}_${sysName}_${arch}.tar.gz`, }; return engines[engine](); @@ -64,12 +60,29 @@ const prepareEngine = async (engine: EngineType, version: string, binaryLocation return binaryFilepath; }; +const createIndexes = async () => { + const { indexes = [], port, engine } = engineOptions; + + const curlCommands: { + [engineType: string]: (indexItem: { name: string; body: unknown }) => string; + } = { + [EngineType.ELASTICSEARCH]: ({ name, body }: { name: string; body: unknown }) => + `curl -XPUT "http://localhost:${port}/${name}" -H "Content-Type: application/json" -d'${JSON.stringify( + body + )}'`, + [EngineType.OPENSEARCH]: ({ name, body }: { name: string; body: unknown }) => + `curl -XPUT "http://localhost:${port}/${name}" -H "Content-Type: application/json" -d'${JSON.stringify( + body + )}'`, + }; + debug('creating indexes'); + await Promise.all( + indexes.map(async (index) => await execSync(curlCommands[engine](index), DISABLE_PROXY)) + ); +}; -const start = async ( - options: Omit & { binaryFilepath: string } -) => { - engineOptions = options; - const { engine, version, binaryFilepath, clusterName, nodeName, port = 9200 } = options; +const start = async () => { + const { engine, version, binaryFilepath, clusterName, nodeName, port } = engineOptions; debug(`Starting ${engine} ${version}, ${binaryFilepath}`); const esExecArgs = [ '-p', @@ -84,24 +97,27 @@ const start = async ( await waitForLocalhost(port); debug(`${engine} is running on port: ${port}, pid: ${server.pid}`); -}; - -const cleanupIndices = (options: { indexes?: Array }): void => { - const indexes = options.indexes?.join(','); + await createIndexes(); - if (indexes) { - const result = execSync(`curl -s -X DELETE http://localhost/${indexes}`, { - env: { http_proxy: undefined, https_proxy: undefined, all_proxy: undefined }, - }); + debug(`indexes created`); +}; - const error = getError(result); +const cleanupIndices = async (): Promise => { + const { port, indexes } = engineOptions; + if (indexes.length <= 0) return; + debug(' deleting indexes'); + const result = execSync( + `curl -s -X DELETE http://localhost:${port}/${indexes.map(({ name }) => name).join(',')}`, + DISABLE_PROXY + ); - if (error) { - throw new Error(`Failed to remove index: ${error.reason}`); - } + const error = getError(result); - debug('Removed all indexes'); + if (error) { + throw new Error(`Failed to remove index: ${error.reason}`); } + + debug('Removed all indexes'); }; const killProcess = async (): Promise => { @@ -125,14 +141,16 @@ export const startEngine = async ({ binaryLocation = path.resolve(__dirname + '/../') + '/node_modules/.cache/jest-search', clusterName = 'jest-search-local', nodeName = 'jest-search-local', -}: EngineOptions = {}) => { + indexes = [], +}: Partial = {}) => { const binaryFilepath = await prepareEngine(engine, version, binaryLocation); + engineOptions = { engine, version, port, clusterName, nodeName, binaryFilepath, indexes }; // start engine - await start({ engine, version, port, clusterName, nodeName, binaryFilepath }); + await start(); }; export const stopEngine = async (): Promise => { - cleanupIndices({ indexes: engineOptions.indexes }); + await cleanupIndices(); await killProcess(); debug('ES has been stopped'); diff --git a/src/index.ts b/src/index.ts index b658d09..624de06 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,12 @@ import { startEngine, stopEngine, EngineType, EngineOptions } from './engine'; +import path from 'path'; -const globalSetup = startEngine; +const globalSetup = async () => { + const configPath = + process.env.JEST_ELASTICSEARCH_CONFIG || path.resolve(__dirname + '/../jest-search-config.js'); + const config = (await import(configPath))(); + await startEngine(config); +}; const globalTeardown = stopEngine; export { globalSetup, globalTeardown, EngineType, EngineOptions }; diff --git a/src/utils.ts b/src/utils.ts index c859b3b..23488e8 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,6 +3,7 @@ import { promisify } from 'util'; import execa from 'execa'; import { execSync } from 'child_process'; import { debug } from './debug'; +import { DISABLE_PROXY } from './constants'; export const waitForLocalhost = async (port: number, retries = 60) => { debug(`checking the local engine startup: ${retries}`); @@ -13,7 +14,7 @@ export const waitForLocalhost = async (port: number, retries = 60) => { const response = execSync( 'curl -s -o /dev/null -i -w "%{http_code}" "http://localhost:9200" || true', - { env: { http_proxy: undefined, https_proxy: undefined, all_proxy: undefined } } + DISABLE_PROXY ); const statusCode = parseInt(response.toString('utf-8'), 10); diff --git a/tests/engine.test.ts b/tests/engine.test.ts index b3a402f..4a9f19c 100644 --- a/tests/engine.test.ts +++ b/tests/engine.test.ts @@ -1,28 +1,66 @@ import { startEngine, stopEngine } from '../src/engine'; import { execSync } from 'child_process'; +import { DISABLE_PROXY } from '../src/constants'; +const indexes = [ + { + name: 'books', + body: { + settings: { + number_of_shards: '1', + number_of_replicas: '1', + }, + mappings: { + properties: { + name: { + type: 'text', + }, + author: { + type: 'keyword', + }, + }, + }, + }, + }, +]; jest.setTimeout(10 * 60 * 1000); +describe('unit test for elasticearch', () => { + it('should start engine with default config', async () => { + await startEngine(); + const response = await execSync('curl -s "http://localhost:9200/?pretty"', { + env: { http_proxy: undefined, https_proxy: undefined, all_proxy: undefined }, + }); + + await stopEngine(); -it('should start engine with default config', async () => { - await startEngine(); - const response = await execSync('curl -s "http://localhost:9200/?pretty"', { - env: { http_proxy: undefined, https_proxy: undefined, all_proxy: undefined }, + expect(JSON.parse(response.toString())).toMatchObject({ + name: 'jest-search-local', + cluster_name: 'jest-search-local', + version: { + number: '8.8.2', + build_flavor: 'default', + build_type: 'tar', + build_snapshot: false, + lucene_version: '9.6.0', + minimum_wire_compatibility_version: '7.17.0', + minimum_index_compatibility_version: '7.0.0', + }, + tagline: 'You Know, for Search', + }); }); - await stopEngine(); + it('should start engine and create index when passing indexes', async () => { + await startEngine({ indexes: [indexes[0]] }); - expect(JSON.parse(response.toString())).toMatchObject({ - name: 'jest-search-local', - cluster_name: 'jest-search-local', - version: { - number: '8.8.2', - build_flavor: 'default', - build_type: 'tar', - build_snapshot: false, - lucene_version: '9.6.0', - minimum_wire_compatibility_version: '7.17.0', - minimum_index_compatibility_version: '7.0.0', - }, - tagline: 'You Know, for Search', + const response = await execSync( + 'curl -s "http://localhost:9200/books/_mapping?pretty"', + DISABLE_PROXY + ); + + await stopEngine(); + + expect(JSON.parse(response.toString())).toEqual({ + books: { mappings: { properties: { author: { type: 'keyword' }, name: { type: 'text' } } } }, + }); }); });