/* * Copyright (C) 2019-2022 Open StApps * This program is free software: you can redistribute it and/or modify it * under the terms of the GNU General Public License as published by the Free * Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ /* eslint-disable unicorn/prevent-abbreviations */ import {SCSearchRequest, SCThings, SCThingType} from '@openstapps/core'; import {Logger} from '@openstapps/logger'; import {deepStrictEqual} from 'assert'; import {readdir, readFile} from 'fs'; import path from 'path'; import {promisify} from 'util'; import {ConnectorClient, HttpClientInterface} from '@openstapps/api'; import junit from 'junit-report-builder'; const localItemMap: Map = new Map(); const remoteItemMap: Map = new Map(); /** * */ async function runTest(name: string, scope: () => Promise, suite: junit.TestSuite, errors: string[]) { const testCase = suite.testCase().name(name); process.stdout.addListener('data', testCase.standardOutput); process.stderr.addListener('data', testCase.standardError); const start = performance.now(); await scope().catch(async error => { await Logger.error(error); testCase.failure(error.message); errors.push(error.message); }); process.stdout.removeListener('data', testCase.standardOutput); process.stderr.removeListener('data', testCase.standardError); const end = performance.now(); testCase.time((end - start) / 1000); } /** * Options to set up indexing core test files to backend */ export interface E2EOptions { /** * File path of the directory containing core test files */ samplesLocation: string; /** * URL of the backend to index to */ to: string; /** * Location of the report */ reportLocation?: string; } /** * Function that can be used for integration tests. * Adds all the SCThings that getItemsFromSamples() returns to the backend. * Afterward, retrieves the items from backend and checks for differences with original ones. */ export async function e2eRun(client: HttpClientInterface, options: E2EOptions): Promise { localItemMap.clear(); remoteItemMap.clear(); const builder = junit.newBuilder(); const errors: string[] = []; const api = new ConnectorClient(client, options.to); try { const indexSuite = builder.testSuite().name('e2e index'); await indexSamples(api, options, indexSuite, errors); Logger.info(`All samples have been indexed via the backend`); const retrieveSuite = builder.testSuite().name('e2e retrieve'); await retrieveItems(api, retrieveSuite, errors); Logger.info(`All samples have been retrieved from the backend`); const compareSuite = builder.testSuite().name('e2e compare'); await compareItems(compareSuite, errors); } catch (error) { throw error; } if (options.reportLocation) { builder.writeTo(options.reportLocation); } await (errors.length > 0 ? Logger.error(`\n${errors.length} failed test cases`) : Logger.ok('All tests passed.')); return errors; } /** * Retrieves all samples previously index using the api */ async function retrieveItems(api: ConnectorClient, suite: junit.TestSuite, errors: string[]): Promise { const singleItemSearchRequest: SCSearchRequest = { filter: { arguments: { field: 'uid', value: 'replace-me', }, type: 'value', }, }; for (const {uid, type} of localItemMap.values()) { await runTest( `Should find ${type} (${uid})`, async () => { singleItemSearchRequest.filter!.arguments.value = uid; const searchResponse = await api.search(singleItemSearchRequest); if (searchResponse.data.length !== 1) { throw new Error( `Search for single SCThing with uid: ${uid} returned ${searchResponse.data.length} results`, ); } remoteItemMap.set(uid, searchResponse.data[0]); }, suite, errors, ); } } /** * Compares all samples (local and remote) with the same uid and throws if they're not deep equal */ async function compareItems(suite: junit.TestSuite, errors: string[]) { for (const localThing of localItemMap.values()) { await runTest( `Should be the same for ${localThing.type} (${localThing.uid})`, async () => { /* istanbul ignore next retrieveItems will throw before*/ if (!remoteItemMap.has(localThing.uid)) { throw new Error(`Did not retrieve expected SCThing with uid: ${localThing.uid}`); } const remoteThing = remoteItemMap.get(localThing.uid); deepStrictEqual( remoteThing, localThing, `Unexpected difference between original and retrieved sample`, ); }, suite, errors, ); } Logger.info(`All samples retrieved from the backend have been compared`); } /** * Function to add all the SCThings that getItemsFromSamples() returns to the backend */ async function indexSamples( api: ConnectorClient, options: E2EOptions, suite: junit.TestSuite, errors: string[], ): Promise { const items = await getItemsFromSamples(options.samplesLocation); if (items.length === 0) { throw new Error('Could not index samples. None were retrieved from the file system.'); } // sort items by type const itemMap: Map = new Map(); for (const item of items) { if (!itemMap.has(item.type)) { itemMap.set(item.type, []); } const itemsOfSameType = itemMap.get(item.type) as SCThings[]; itemsOfSameType.push(item); itemMap.set(item.type, itemsOfSameType); localItemMap.set(item.uid, item); } // add items depending on their type property with one type per bulk for (const type of itemMap.keys()) { await runTest( `Should index ${type}`, async () => api.index(itemMap.get(type) as SCThings[], 'stapps-core-sample-data'), suite, errors, ); } } /** * Get all SCThings from the predefined core test json files * @param samplesDirectory Filepath to the directory containing to the core test json files * @returns an Array of all the SCThings specified for test usage */ export async function getItemsFromSamples(samplesDirectory: string): Promise { const readDirPromised = promisify(readdir); const readFilePromised = promisify(readFile); const things: T[] = []; try { const fileNames = await readDirPromised(samplesDirectory); for (const fileName of fileNames) { const filePath = path.join(samplesDirectory, fileName); if (filePath.endsWith('.json')) { const fileContent = await readFilePromised(filePath, {encoding: 'utf8'}); const schemaObject = JSON.parse(fileContent); if (schemaObject.errorNames.length === 0 && typeof schemaObject.instance.type === 'string') { things.push(schemaObject.instance); } } } } catch (error) { throw error; } return things; }