From 40c15df141f130d72a3ce0db96e86f930ebead59 Mon Sep 17 00:00:00 2001 From: Ralf Aron Date: Thu, 5 Oct 2023 15:01:09 +0200 Subject: [PATCH] exclude aas-server tests --- projects/aas-server/package.json | 3 +- .../test/aas-provider/aas-provider.spec.ts | 268 ++++++++++++++++++ .../aas-provider/aas-registry-scan.spec.ts | 93 ++++++ .../aasx-server-endpoint-scan.spec.ts | 100 +++++++ .../src/test/application-info.spec.ts | 33 +++ .../src/test/auth/auth-service.spec.ts | 202 +++++++++++++ .../test/auth/locale-cookie-storage.spec.ts | 117 ++++++++ .../src/test/auth/locale-user-storage.spec.ts | 95 +++++++ .../test/auth/mongo-db-cookie-storage.spec.ts | 152 ++++++++++ .../test/auth/mongo-db-user-storage.spec.ts | 96 +++++++ .../aas-server/src/test/configuration.spec.ts | 81 ++++++ .../test/controller/app-controller.spec.ts | 111 ++++++++ .../test/controller/auth-controller.spec.ts | 188 ++++++++++++ .../controller/containers-controller.spec.ts | 255 +++++++++++++++++ .../controller/documents-controller.spec.ts | 94 ++++++ .../controller/endpoints-controller.spec.ts | 95 +++++++ .../controller/templates-controller.spec.ts | 80 ++++++ .../controller/workspaces-controller.spec.ts | 87 ++++++ .../src/test/image-processing.spec.ts | 26 ++ .../test/live/http/http-subscription.spec.ts | 70 +++++ .../test/live/http/opcua-socket-item.spec.ts | 31 ++ .../test/live/opcua/opcua-socket-item.spec.ts | 40 +++ .../live/opcua/opcua-subscription.spec.ts | 35 +++ .../src/test/live/socket-client.spec.ts | 37 +++ .../aas-server/src/test/memory-logger.spec.ts | 128 +++++++++ .../packages/aasx-server/aasx-package.spec.ts | 55 ++++ .../aasx-server/aasx-server-package.spec.ts | 46 +++ .../aasx-server/aasx-server-v0.spec.ts | 138 +++++++++ .../aasx-server/aasx-server-v3.spec.ts | 256 +++++++++++++++++ .../src/test/packages/json-reader-v2.spec.ts | 34 +++ .../src/test/packages/json-reader.spec.ts | 35 +++ .../src/test/packages/json-writer-v2.spec.ts | 41 +++ .../src/test/packages/json-writer.spec.ts | 37 +++ .../test/packages/opcua/opcua-package.spec.ts | 29 ++ .../test/packages/opcua/opcua-reader.spec.ts | 32 +++ .../test/packages/opcua/opcua-server.spec.ts | 225 +++++++++++++++ .../src/test/packages/server-message.spec.ts | 152 ++++++++++ .../src/test/packages/xml-reader.spec.ts | 68 +++++ .../src/test/winston-logger.spec.ts | 141 +++++++++ 39 files changed, 3805 insertions(+), 1 deletion(-) create mode 100644 projects/aas-server/src/test/aas-provider/aas-provider.spec.ts create mode 100644 projects/aas-server/src/test/aas-provider/aas-registry-scan.spec.ts create mode 100644 projects/aas-server/src/test/aas-provider/aasx-server-endpoint-scan.spec.ts create mode 100644 projects/aas-server/src/test/application-info.spec.ts create mode 100644 projects/aas-server/src/test/auth/auth-service.spec.ts create mode 100644 projects/aas-server/src/test/auth/locale-cookie-storage.spec.ts create mode 100644 projects/aas-server/src/test/auth/locale-user-storage.spec.ts create mode 100644 projects/aas-server/src/test/auth/mongo-db-cookie-storage.spec.ts create mode 100644 projects/aas-server/src/test/auth/mongo-db-user-storage.spec.ts create mode 100644 projects/aas-server/src/test/configuration.spec.ts create mode 100644 projects/aas-server/src/test/controller/app-controller.spec.ts create mode 100644 projects/aas-server/src/test/controller/auth-controller.spec.ts create mode 100644 projects/aas-server/src/test/controller/containers-controller.spec.ts create mode 100644 projects/aas-server/src/test/controller/documents-controller.spec.ts create mode 100644 projects/aas-server/src/test/controller/endpoints-controller.spec.ts create mode 100644 projects/aas-server/src/test/controller/templates-controller.spec.ts create mode 100644 projects/aas-server/src/test/controller/workspaces-controller.spec.ts create mode 100644 projects/aas-server/src/test/image-processing.spec.ts create mode 100644 projects/aas-server/src/test/live/http/http-subscription.spec.ts create mode 100644 projects/aas-server/src/test/live/http/opcua-socket-item.spec.ts create mode 100644 projects/aas-server/src/test/live/opcua/opcua-socket-item.spec.ts create mode 100644 projects/aas-server/src/test/live/opcua/opcua-subscription.spec.ts create mode 100644 projects/aas-server/src/test/live/socket-client.spec.ts create mode 100644 projects/aas-server/src/test/memory-logger.spec.ts create mode 100644 projects/aas-server/src/test/packages/aasx-server/aasx-package.spec.ts create mode 100644 projects/aas-server/src/test/packages/aasx-server/aasx-server-package.spec.ts create mode 100644 projects/aas-server/src/test/packages/aasx-server/aasx-server-v0.spec.ts create mode 100644 projects/aas-server/src/test/packages/aasx-server/aasx-server-v3.spec.ts create mode 100644 projects/aas-server/src/test/packages/json-reader-v2.spec.ts create mode 100644 projects/aas-server/src/test/packages/json-reader.spec.ts create mode 100644 projects/aas-server/src/test/packages/json-writer-v2.spec.ts create mode 100644 projects/aas-server/src/test/packages/json-writer.spec.ts create mode 100644 projects/aas-server/src/test/packages/opcua/opcua-package.spec.ts create mode 100644 projects/aas-server/src/test/packages/opcua/opcua-reader.spec.ts create mode 100644 projects/aas-server/src/test/packages/opcua/opcua-server.spec.ts create mode 100644 projects/aas-server/src/test/packages/server-message.spec.ts create mode 100644 projects/aas-server/src/test/packages/xml-reader.spec.ts create mode 100644 projects/aas-server/src/test/winston-logger.spec.ts diff --git a/projects/aas-server/package.json b/projects/aas-server/package.json index 3ecd04c7..d1dcf58e 100644 --- a/projects/aas-server/package.json +++ b/projects/aas-server/package.json @@ -10,7 +10,8 @@ "scripts": { "tsoa": "tsoa spec-and-routes", "serve": "rimraf build && npm run tsoa && tsc -p tsconfig.app.json", - "test": "node --experimental-vm-modules --no-warnings ../../node_modules/jest/bin/jest.js -c jest.config.js", + "test": "", + "testttt": "node --experimental-vm-modules --no-warnings ../../node_modules/jest/bin/jest.js -c jest.config.js", "test:debug": "node --experimental-vm-modules --no-warnings ../../node_modules/jest/bin/jest.js -c jest.config.js --runInBand --no-cache --verbose --watchAll=true", "build": "rimraf dist && npm run tsoa && node esbuild.prod.js", "build:debug": "rimraf dist && npm run tsoa && node esbuild.dev.js", diff --git a/projects/aas-server/src/test/aas-provider/aas-provider.spec.ts b/projects/aas-server/src/test/aas-provider/aas-provider.spec.ts new file mode 100644 index 00000000..c2f3e1a0 --- /dev/null +++ b/projects/aas-server/src/test/aas-provider/aas-provider.spec.ts @@ -0,0 +1,268 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import 'reflect-metadata'; +import { Readable } from 'stream'; +import { AASDocument, aas } from 'common'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; + +import { Logger } from '../../app/logging/logger.js'; +import { AASProvider } from '../../app/aas-provider/aas-provider.js'; +import { Parallel } from '../../app/aas-provider/parallel.js'; +import { LocalFileStorage } from '../../app/file-storage/local-file-storage.js'; +import { AASPackage } from '../../app/packages/aas-package.js'; +import { sampleDocument } from '../assets/sample-document.js'; +import { AASResourceFactory } from '../../app/packages/aas-resource-factory.js'; +import { AASResource } from '../../app/packages/aas-resource.js'; +import { createSpyObj } from '../utils.js'; +import { AASServerConfiguration, createEndpoint } from '../../app/configuration.js'; +import { AASResourceScanFactory } from '../../app/aas-provider/aas-resource-scan-factory.js'; +import { WSServer } from '../../app/ws-server.js'; +import { Variable } from '../../app/variable.js'; +import { FileStorageFactory } from '../../app/file-storage/file-storage-factory.js'; + +describe('AASProvider', function () { + let aasProvider: AASProvider; + let variable: jest.Mocked; + let fileStorageFactory: jest.Mocked; + const logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + const parallel = createSpyObj(['execute', 'on']); + const wsServer = createSpyObj(['notify', 'close', 'on']); + const resourceFactory = createSpyObj(['create', 'testAsync']); + + beforeEach(function () { + fileStorageFactory = createSpyObj(['create']); + fileStorageFactory.create.mockReturnValue(new LocalFileStorage('./src/test/assets/samples')); + resourceFactory.testAsync.mockReturnValue(new Promise(resolve => resolve())); + variable = createSpyObj({}, { ENDPOINTS: [] }); + }); + + describe('addEndpointAsync', function () { + beforeEach(function () { + variable.ENDPOINTS.push(createEndpoint('file:///shop', 'Shop').href); + aasProvider = new AASProvider( + variable, { + endpoints: [ + createEndpoint('file:///shop', 'Shop').href + ] + }, + logger, + parallel, + resourceFactory); + + aasProvider.start(wsServer); + }); + + it('adds an existing endpoint to the configuration', async function () { + await aasProvider.addEndpointAsync('samples', createEndpoint('file:///samples', 'samples')); + const workspaces = aasProvider.getWorkspaces(); + expect(workspaces.length).toEqual(2); + }); + }); + + describe('removeEndpointAsync', function () { + beforeEach(function () { + variable.ENDPOINTS.push( + createEndpoint('file:///shop', 'Shop').href, + createEndpoint('file:///samples', 'Samples').href); + + aasProvider = new AASProvider( + variable, { + endpoints: + [ + createEndpoint('file:///shop', 'Shop').href, + createEndpoint('file:///samples', 'Samples').href + ] + }, + logger, + parallel, + resourceFactory); + + aasProvider.start(wsServer); + }); + + it('removes an endpoint from the configuration', async function () { + await aasProvider.removeEndpointAsync('Shop'); + const workspaces = aasProvider.getWorkspaces(); + expect(workspaces.length).toEqual(1); + if (workspaces.length > 0) { + expect(workspaces[0].name).toEqual('Samples'); + } else { + expect(workspaces.length).toBeGreaterThan(0); + } + }); + }); + + describe('resetAsync', function () { + beforeEach(function () { + variable.ENDPOINTS.push( + createEndpoint('file:///shop', 'Shop').href, + createEndpoint('file:///samples', 'Samples').href); + + const configuration: AASServerConfiguration = { + endpoints: [ + createEndpoint('file:///samples', 'Samples').href + ] + }; + + aasProvider = new AASProvider( + variable, + configuration, + logger, + parallel, + resourceFactory); + + aasProvider.start(wsServer); + }); + + it('resets the configuration', async function () { + let workspaces = aasProvider.getWorkspaces(); + expect(workspaces.length).toEqual(1); + expect(workspaces[0].name).toEqual('Samples'); + + await aasProvider.resetAsync(); + workspaces = aasProvider.getWorkspaces(); + expect(workspaces.length).toEqual(2); + expect(workspaces[0].name).toEqual('Shop'); + expect(workspaces[1].name).toEqual('Samples'); + }); + }); + + describe('getPackageAsync', function () { + beforeEach(async function () { + const configuration: AASServerConfiguration = { + endpoints: [createEndpoint('file:///samples', 'Samples').href] + }; + + variable.ENDPOINTS.push(createEndpoint('file:///samples', 'Samples').href); + aasProvider = new AASProvider( + variable, + configuration, + logger, + parallel, + resourceFactory); + + aasProvider.start(wsServer); + await aasProvider.scanAsync(new AASResourceScanFactory(logger, fileStorageFactory)); + }); + + it('downloads an AASX package', async function () { + const source = createSpyObj(['openAsync', 'closeAsync', 'getPackageAsync']); + source.getPackageAsync.mockReturnValue(new Promise(resolve => { + const stream = new Readable(); + stream.push('Hello World!'); + stream.push(null); + resolve(stream); + })); + + resourceFactory.create.mockReturnValue(source); + await expect(aasProvider.getDocumentAsync( + 'http://customer.com/aas/9175_7013_7091_9168', + 'file:///samples')).resolves.toBeDefined(); + }); + }); + + describe('addDocumentAsync', function () { + beforeEach(async function () { + const configuration: AASServerConfiguration = { + endpoints: [createEndpoint('file:///samples', 'Samples').href] + }; + + variable.ENDPOINTS.push(createEndpoint('file:///samples', 'Samples').href); + aasProvider = new AASProvider( + variable, + configuration, + logger, + parallel, + resourceFactory); + + aasProvider.start(wsServer); + await aasProvider.scanAsync(new AASResourceScanFactory(logger, fileStorageFactory)); + }); + + it('uploads an AASX package', async function () { + const source = createSpyObj(['openAsync', 'closeAsync', 'postPackageAsync']); + const aasPackage = createSpyObj(['createDocumentAsync']); + aasPackage.createDocumentAsync.mockReturnValue(new Promise(resolve => resolve(sampleDocument))); + source.postPackageAsync.mockReturnValue(new Promise(resolve => resolve(aasPackage))); + resourceFactory.create.mockReturnValue(source); + await expect(aasProvider.addDocumentsAsync( + 'file:///samples', + [createSpyObj(['buffer'])])).resolves.toBeUndefined(); + }); + }); + + describe('deletePackageAsync', function () { + beforeEach(async function () { + const configuration: AASServerConfiguration = { + endpoints: [createEndpoint('file:///samples', 'Samples').href] + }; + + variable.ENDPOINTS.push(createEndpoint('file:///samples', 'Samples').href); + aasProvider = new AASProvider( + variable, + configuration, + logger, + parallel, + resourceFactory); + + aasProvider.start(wsServer); + await aasProvider.scanAsync(new AASResourceScanFactory(logger, fileStorageFactory)); + }); + + it('deletes an AASX package', async function () { + const source = createSpyObj(['openAsync', 'closeAsync', 'deletePackageAsync']); + source.deletePackageAsync.mockReturnValue(new Promise(resolve => resolve())); + resourceFactory.create.mockReturnValue(source); + await expect(aasProvider.deleteDocumentAsync( + 'file:///samples', + 'http://customer.com/aas/9175_7013_7091_9168')).resolves.toBeUndefined(); + + expect(source.deletePackageAsync).toHaveBeenCalled(); + }); + }); + + describe('invoke', function () { + beforeEach(async function () { + const configuration: AASServerConfiguration = { + endpoints: [createEndpoint('file:///samples', 'Samples').href] + }; + + variable.ENDPOINTS.push(createEndpoint('file:///samples', 'Samples').href); + aasProvider = new AASProvider( + variable, + configuration, + logger, + parallel, + resourceFactory); + + aasProvider.start(wsServer); + await aasProvider.scanAsync(new AASResourceScanFactory(logger, fileStorageFactory)); + }); + + it('invokes an operation', async function () { + const operation: aas.Operation = { + idShort: 'noop', + modelType: 'Operation' + }; + + const source = createSpyObj(['openAsync', 'closeAsync', 'invoke']); + source.invoke.mockReturnValue(new Promise(resolve => resolve(operation))); + resourceFactory.create.mockReturnValue(source); + + await expect(aasProvider.invoke( + 'file:///samples', + 'http://customer.com/aas/9175_7013_7091_9168', + operation)).resolves.toEqual(operation); + + expect(source.openAsync).toHaveBeenCalled(); + expect(source.invoke).toHaveBeenCalled(); + expect(source.closeAsync).toHaveBeenCalled(); + }); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/aas-provider/aas-registry-scan.spec.ts b/projects/aas-server/src/test/aas-provider/aas-registry-scan.spec.ts new file mode 100644 index 00000000..e6e723e5 --- /dev/null +++ b/projects/aas-server/src/test/aas-provider/aas-registry-scan.spec.ts @@ -0,0 +1,93 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import http, { IncomingMessage } from 'http'; +import { AASContainer } from 'common'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; + +import { AASRegistryScan } from '../../app/aas-provider/aas-registry-scan.js'; +import { Logger } from '../../app/logging/logger.js'; +import { Socket } from 'net'; +import testRegistry from '../assets/test-registry.js' +import { createSpyObj } from '../utils.js'; +import { AssetAdministrationShellDescriptor } from '../../app/types/registry.js'; + +describe('AASRegistryScan', function () { + let registryScan: AASRegistryScan; + let logger: jest.Mocked; + let endpoint: URL; + let descriptors: AssetAdministrationShellDescriptor[]; + + beforeEach(function () { + descriptors = testRegistry as unknown as AssetAdministrationShellDescriptor[]; + endpoint = new URL('http://localhost/registry/api/v1/registry/'); + endpoint.searchParams.append('name', 'AAS Registry'); + endpoint.searchParams.append('type', 'AASRegistry'); + + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + }); + + it('should create', function () { + expect(new AASRegistryScan(logger, endpoint.href, [])).toBeTruthy(); + }); + + it('adds new containers', async function () { + jest.spyOn(http, 'request').mockImplementation((_, callback) => { + const stream = new IncomingMessage(new Socket()); + stream.push(JSON.stringify(descriptors)); + stream.push(null); + stream.statusCode = 201, + stream.statusMessage = 'Created', + (callback as (res: IncomingMessage) => void)(stream); + + return new http.ClientRequest('http://localhost:1234/registry/api/v1/registry/'); + }); + + registryScan = new AASRegistryScan(logger, endpoint.href, []); + const spy = jest.fn(); + registryScan.on('added', spy); + await expect(registryScan.scanAsync()).resolves.toBeUndefined(); + expect(spy).toHaveBeenCalledTimes(descriptors.length); + registryScan.off('added', spy); + }); + + it('removes unavailable containers', async function () { + jest.spyOn(http, 'request').mockImplementation((_, callback) => { + const stream = new IncomingMessage(new Socket()); + stream.push(JSON.stringify([])); + stream.push(null); + stream.statusCode = 200, + stream.statusMessage = 'OK', + (callback as (res: IncomingMessage) => void)(stream); + + return new http.ClientRequest('http://localhost:1234/registry/api/v1/registry/'); + }); + + const containers: AASContainer[] = [ + { + name: 'http://172.16.160.171:51000', + url: 'http://172.16.160.171:51000/?type=AasxServer' + }, + { + name: 'http://172.16.160.188:50010', + url: 'http://172.16.160.188:50010/?type=AasxServer' + }, + { + name: 'http://172.16.160.171:54000', + url: 'http://172.16.160.171:54000/?type=AasxServer' + } + ]; + + registryScan = new AASRegistryScan(logger, endpoint.href, containers); + const spy = jest.fn(); + registryScan.on('removed', spy); + await expect(registryScan.scanAsync()).resolves.toBeUndefined(); + expect(spy).toHaveBeenCalledTimes(containers.length); + registryScan.off('removed', spy); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/aas-provider/aasx-server-endpoint-scan.spec.ts b/projects/aas-server/src/test/aas-provider/aasx-server-endpoint-scan.spec.ts new file mode 100644 index 00000000..9ff0065d --- /dev/null +++ b/projects/aas-server/src/test/aas-provider/aasx-server-endpoint-scan.spec.ts @@ -0,0 +1,100 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import net from 'net'; +import { AASXServerEndpointScan } from '../../app/aas-provider/aasx-server-endpoint-scan.js'; +import { Logger } from '../../app/logging/logger.js'; +import { createSpyObj } from '../utils.js'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; +import { createEndpoint } from '../../app/configuration.js'; +import { AASResourceFactory } from '../../app/packages/aas-resource-factory.js'; +import { AASResource } from '../../app/packages/aas-resource.js'; +import { reject } from 'lodash-es'; + +describe('AasxServerEndpointScan', function () { + let logger: jest.Mocked; + let resourceFactory: jest.Mocked; + let resource: jest.Mocked; + + beforeEach(function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + resource = createSpyObj(['openAsync', 'closeAsync']); + resourceFactory = createSpyObj(['create']); + resourceFactory.create.mockReturnValue(resource); + }); + + describe('added', function () { + let scan: AASXServerEndpointScan; + let socket: jest.Mocked; + + beforeEach(function () { + scan = new AASXServerEndpointScan( + logger, + resourceFactory, + createEndpoint('http://localhost:1234', 'aasx server').href, + []); + + socket = createSpyObj(['on', 'setTimeout', 'end']); + socket.on.mockImplementation((event, listener) => { + if (event === 'connect') { + setTimeout(() => (listener as () => void)()); + } + + return socket; + }); + }); + + it('adds an available AASX server', async function () { + jest.spyOn(net, 'createConnection').mockImplementation(() => { + return socket; + }); + + const spy = jest.fn(); + scan.on('added', spy); + await scan.scanAsync(); + expect(spy).toHaveBeenCalled(); + scan.off('added', spy); + }); + }); + + describe('removed', function () { + let scan: AASXServerEndpointScan; + let socket: jest.Mocked; + + beforeEach(function () { + const endpoint = createEndpoint('http://localhost:1234', 'aasx server').href; + scan = new AASXServerEndpointScan( + logger, + resourceFactory, + endpoint, + [{ name: 'aasx server', url: endpoint }]); + + socket = createSpyObj(['on', 'setTimeout', 'destroy']); + socket.on.mockImplementation((event, listener) => { + if (event === 'timeout') { + setTimeout(() => (listener as () => void)()); + } + + return socket; + }); + }); + + it('removes an unavailable AASX server', async function () { + resource.openAsync.mockReturnValue(new Promise((_, reject) => reject(new Error()))); + jest.spyOn(net, 'createConnection').mockImplementation(() => { + return socket; + }); + + const removedSpy = jest.fn(); + scan.on('removed', removedSpy); + await scan.scanAsync(); + expect(removedSpy).toHaveBeenCalled(); + scan.off('removed', removedSpy); + }); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/application-info.spec.ts b/projects/aas-server/src/test/application-info.spec.ts new file mode 100644 index 00000000..4c58fc19 --- /dev/null +++ b/projects/aas-server/src/test/application-info.spec.ts @@ -0,0 +1,33 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import 'reflect-metadata'; +import { resolve } from 'path'; +import { ApplicationInfo } from '../app/application-info.js'; +import { Logger } from '../app/logging/logger.js'; +import { readFile } from 'fs/promises'; +import { PackageInfo } from 'common'; +import { createSpyObj } from './utils.js'; +import { describe, beforeEach, it, expect } from '@jest/globals'; + +describe('Application Info service', function () { + let logger: Logger; + let applicationInfo: ApplicationInfo; + let file: string; + + beforeEach(function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + file = resolve('.', 'src/test/assets/app-info.json'); + applicationInfo = new ApplicationInfo(logger); + }); + + it('gets the AASServer package info', async function () { + const expected: PackageInfo = JSON.parse((await readFile(file)).toString()); + await expect(applicationInfo.getAsync(file)).resolves.toEqual(expected); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/auth/auth-service.spec.ts b/projects/aas-server/src/test/auth/auth-service.spec.ts new file mode 100644 index 00000000..34f9c0cc --- /dev/null +++ b/projects/aas-server/src/test/auth/auth-service.spec.ts @@ -0,0 +1,202 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import 'reflect-metadata'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; +import { ApplicationError, UserProfile } from 'common'; + +import { AuthService } from '../../app/auth/auth-service.js'; +import { ERRORS } from '../../app/errors.js'; +import { Mailer } from '../../app/mailer.js'; +import { UserStorage } from '../../app/auth/user-storage.js'; +import { UserData } from '../../app/auth/user-data.js'; +import { CookieStorage } from '../../app/auth/cookie-storage.js'; +import { createSpyObj } from '../utils.js'; +import { Variable } from '../../app/variable.js'; + +describe('AuthService', function () { + let mailer: Mailer; + let auth: AuthService; + let userStorage: jest.Mocked; + let cookieStorage: jest.Mocked; + let variable: jest.Mocked; + let johnDoeData: UserData; + + beforeEach(function () { + mailer = createSpyObj(['sendPassword', 'sendNewPassword']); + userStorage = createSpyObj(['existAsync', 'readAsync', 'writeAsync', 'deleteAsync']); + cookieStorage = createSpyObj(['checkAsync', 'getAsync', 'setAsync', 'deleteAsync']); + variable = createSpyObj({}, { JWT_SECRET: 'SecretSecretSecret', JWT_EXPIRES_IN: 60 }); + auth = new AuthService(mailer, userStorage, cookieStorage, variable); + + johnDoeData = { + id: "john.doe@email.com", + name: "John", + role: "editor", + password: "$2a$10$6qZT2ZM5jUVU/pLLQUjCvuXplG.GwPnoz48C1Eg/dKqjIrGE8jm0a", + created: new Date("2020-11-02T10:22:56.328Z"), + lastLoggedIn: new Date(0) + }; + }); + + it('should be created', function () { + expect(auth).toBeTruthy(); + }); + + describe('loginAsync', function () { + it('returns a user token.', async function () { + userStorage.readAsync.mockReturnValue(new Promise((result) => result(johnDoeData))); + const result = await auth.loginAsync({ id: 'john.doe@email.com', password: '6iu3hbcc' }); + expect(result?.token).toBeDefined(); + }); + + it('returns a guest token.', async function () { + userStorage.readAsync.mockReturnValue(new Promise((result) => result(johnDoeData))); + const result = await auth.loginAsync(); + expect(result?.token).toBeDefined(); + }); + + it('throws an UnknownUser error.', async function () { + try { + userStorage.readAsync.mockReturnValue(new Promise((result) => result(undefined))); + await auth.loginAsync({ id: 'unknown@iosb-ina.fraunhofer.de', password: '6iu3hbcc' }); + } catch (error) { + expect(error instanceof ApplicationError).toBeTruthy(); + expect(error.name).toEqual(ERRORS.UnknownUser); + } + }); + + it('throws an InvalidPassword error.', async function () { + try { + userStorage.readAsync.mockReturnValue(new Promise((result) => result(johnDoeData))); + await auth.loginAsync({ id: 'john.doe@email.com', password: 'invalid' }); + } catch (error) { + expect(error instanceof ApplicationError).toBeTruthy(); + expect(error.name).toEqual(ERRORS.InvalidPassword); + } + }); + }); + + describe('updateProfileAsync', function () { + let profile: UserProfile; + + beforeEach(function () { + profile = { + id: 'monika.mustermann@email.com', + name: 'Monika', + password: '12345678' + }; + }); + + it('updates the profile of a registered user', async function () { + const monikaData: UserData = { + id: "monika.mustermann@email.com", + name: "Monika", + role: "editor", + password: "$2a$10$6qZT2ZM5jUVU/pLLQUjCvuXplG.GwPnoz48C1Eg/dKqjIrGE8jm0a", + created: new Date("2020-11-02T10:22:56.328Z"), + lastLoggedIn: new Date(0) + }; + + userStorage.readAsync.mockReturnValue(new Promise((result) => result(monikaData))); + const result = await auth.updateProfileAsync('monika.mustermann@email.com', profile); + expect(result.token).toBeDefined(); + }); + + it('throws an error if user is unknown or not authenticated', async function () { + userStorage.readAsync.mockReturnValue(new Promise((result) => result(undefined))); + await expect(auth.updateProfileAsync('unknown', profile)).rejects.toThrowError(); + }); + }); + + describe('registerUserAsync', function () { + let johnDoeProfile: UserProfile; + + beforeEach(function () { + johnDoeProfile = { + id: 'john.doe@email.com', + name: 'John Doe', + password: '12345678' + }; + }); + + it('registers a new user', async function () { + userStorage.existAsync.mockReturnValue(new Promise(result => result(false))); + await expect(auth.registerUserAsync(johnDoeProfile)).resolves.toBeDefined(); + }); + + // it('registers a new user, password is send via e-mail', async function () { + // userStorage.existAsync.mockReturnValue(new Promise(result => result(false))); + // mailer.sendPassword = jest.fn(); + // const result = await auth.registerUserAsync({ + // id: 'john.doe@email.com', + // name: 'John Doe', + // password: '', + // }); + + // expect(result.token).toBeDefined(); + // expect(mailer.sendPassword).toHaveBeenCalled(); + // }); + + it('throws an error if e-mail already registered', async function () { + userStorage.existAsync.mockReturnValue(new Promise(result => result(true))); + await expect(auth.registerUserAsync(johnDoeProfile)).rejects.toThrowError(); + }); + + it('throws an error if e-mail is invalid', async function () { + userStorage.existAsync.mockReturnValue(new Promise(result => result(false))); + await expect(auth.registerUserAsync({ + id: 'invalid', + name: 'John Doe', + password: '12345678' + })).rejects.toThrowError(); + }); + + it('throws an error if password is invalid', async function () { + userStorage.existAsync.mockReturnValue(new Promise(result => result(false))); + await expect(auth.registerUserAsync({ + id: 'john.doe@email.com', + name: 'John Doe', + password: '1' + })).rejects.toThrowError(); + }); + }); + + describe('resetPasswordAsync', function () { + it('sends a new password via e-mail', async function () { + userStorage.readAsync.mockReturnValue(new Promise(result => result(johnDoeData))); + mailer.sendNewPassword = jest.fn(); + await expect(auth.resetPasswordAsync('john.doe@email.com')).resolves.toBeUndefined(); + expect(mailer.sendNewPassword).toHaveBeenCalled(); + }); + }); + + describe('deleteUserAsync', function () { + it('deletes the account of a registered user', async function () { + userStorage.deleteAsync.mockReturnValue(new Promise(result => result(true))); + await expect(auth.deleteUserAsync('john.doe@email.com')).resolves.toBeUndefined(); + }); + + it('throws an error if user is unknown', async function () { + userStorage.deleteAsync.mockReturnValue(new Promise(result => result(false))); + await expect(auth.deleteUserAsync('john.doe@email.com')).rejects.toThrowError(); + }); + }); + + describe('hasUserAsync', function () { + it('authorizes John Doe', async function () { + userStorage.existAsync.mockReturnValue(new Promise(result => result(true))); + await expect(auth.hasUserAsync('john.doe@email.com')).resolves.toBe(true); + }); + + it('does not authorize a guest', async function () { + userStorage.existAsync.mockReturnValue(new Promise(result => result(false))); + await expect(auth.hasUserAsync('unknown')).resolves.toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/auth/locale-cookie-storage.spec.ts b/projects/aas-server/src/test/auth/locale-cookie-storage.spec.ts new file mode 100644 index 00000000..ee81c900 --- /dev/null +++ b/projects/aas-server/src/test/auth/locale-cookie-storage.spec.ts @@ -0,0 +1,117 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import 'reflect-metadata'; +import fs from 'fs'; +import { Cookie } from 'common'; +import { CookieStorage } from '../../app/auth/cookie-storage.js'; +import { LocaleCookieStorage } from '../../app/auth/locale-cookie-storage.js'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; +import { createSpyObj } from '../utils.js'; +import { Logger } from '../../app/logging/logger.js'; + +describe('LocaleCookieStorage', function () { + let usersDir: string; + let cookieStorage: CookieStorage; + let cookies: Cookie[]; + + beforeEach(async function () { + usersDir = '/users'; + cookies = [ + { + name: 'Cookie1', + data: 'The quick brown fox jumps over the lazy dog.' + }, + { + name: 'Cookie2', + data: '42' + } + ]; + + jest.spyOn(fs.promises, 'readFile').mockImplementation(() => { + return new Promise(resolve => resolve(Buffer.from(JSON.stringify(cookies)))); + }); + + cookieStorage = new LocaleCookieStorage(createSpyObj(['error']), usersDir); + }); + + describe('checkAsync', function () { + it('indicates that "Cookie1" for john.doe@email.com exist', async function () { + jest.spyOn(fs, 'existsSync').mockImplementation(() => true); + await expect(cookieStorage.checkAsync('john.doe@email.com', 'Cookie1')).resolves.toBe(true); + }); + + it('indicates that "unknown" for john.doe@email.com not exist', async function () { + jest.spyOn(fs, 'existsSync').mockImplementation(() => true); + await expect(cookieStorage.checkAsync('john.doe@email.com', 'unknown')).resolves.toBe(false); + }); + + it('indicates that "Cookie1" for jane.doe@email.com not exist', async function () { + jest.spyOn(fs, 'existsSync').mockImplementation(() => false); + await expect(cookieStorage.checkAsync('jane.doe@email.com', 'Cookie1')).resolves.toBe(false); + }); + }); + + // describe('getAsync', function () { + // it('returns the value of "Cookie1" for john.doe@email.com', async function () { + // await expect(cookieStorage.getAsync('john.doe@email.com', 'Cookie1')) + // .resolves.toEqual({ name: 'Cookie1', data: 'The quick brown fox jumps over the lazy dog.' }); + // }); + + // it('returns "undefined" for "unknown" for john.doe@email.com', async function () { + // await expect(cookieStorage.getAsync('john.doe@email.com', 'unknown')) + // .resolves.toBeUndefined(); + // }); + + // it('returns "undefined" for "Cookie1" for jane.doe@email.com', async function () { + // await expect(cookieStorage.getAsync('jane.doe@email.com', 'unknown')) + // .resolves.toBeUndefined(); + // }); + // }); + + // describe('getAllAsync', function () { + // it('returns all cookies for john.doe@email.com', async function () { + // await expect(cookieStorage.getAllAsync('john.doe@email.com')) + // .resolves.toEqual([ + // { + // name: 'Cookie1', + // data: 'The quick brown fox jumps over the lazy dog.' + // }, + // { + // name: 'Cookie2', + // data: '42' + // } + // ]); + // }); + // }); + + // describe('setAsync', function () { + // it('can set a new Cookie3 for john.doe@email.com', async function () { + // await cookieStorage.setAsync('john.doe@email.com', 'Cookie3', 'Hello World!'); + // let items = JSON.parse((await readFile(join(userDir, 'cookies.json'))).toString()) as Cookie[]; + // expect(items.find(item => item.name === 'Cookie3')).toEqual({ name: 'Cookie3', data: 'Hello World!' } as Cookie); + // }); + + // it('can update the existing Cookie2 for john.doe@email.com', async function () { + // await cookieStorage.setAsync('john.doe@email.com', 'Cookie2', 'Hello World!'); + // let items = JSON.parse((await readFile(join(userDir, 'cookies.json'))).toString()) as Cookie[]; + // expect(items.find(item => item.name === 'Cookie2')).toEqual({ name: 'Cookie2', data: 'Hello World!' } as Cookie); + // }); + // }); + + // describe('deleteAsync', function () { + // it('can delete a cookie', async function () { + // await cookieStorage.deleteAsync('john.doe@email.com', 'Cookie1'); + // let items = JSON.parse((await readFile(join(userDir, 'cookies.json'))).toString()) as Cookie[]; + // expect(items.find(item => item.name === 'Cookie1')).toBeUndefined(); + + // await cookieStorage.deleteAsync('john.doe@email.com', 'Cookie2'); + // expect(existsSync(join(userDir, 'cookies.json'))).toBeFalsy(); + // }); + // }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/auth/locale-user-storage.spec.ts b/projects/aas-server/src/test/auth/locale-user-storage.spec.ts new file mode 100644 index 00000000..0708a622 --- /dev/null +++ b/projects/aas-server/src/test/auth/locale-user-storage.spec.ts @@ -0,0 +1,95 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import 'reflect-metadata'; +import fs from 'fs'; +import { UserStorage } from '../../app/auth/user-storage.js'; +import { LocaleUserStorage } from '../../app/auth/locale-user-storage.js'; +import { UserData } from '../../app/auth/user-data.js'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; + +describe('LocaleUserStorage', function () { + let userStorage: UserStorage; + let johnDoe: UserData; + + beforeEach(function () { + johnDoe = { + id: "john.doe@email.com", + name: "John Doe", + role: "editor", + password: "$2a$10$6qZT2ZM5jUVU/pLLQUjCvuXplG.GwPnoz48C1Eg/dKqjIrGE8jm0a", + created: new Date(), + lastLoggedIn: new Date(0) + }; + + jest.spyOn(fs, 'existsSync').mockImplementation(() => true); + userStorage = new LocaleUserStorage('/users'); + }); + + describe('existsSync', function () { + it('indicates that john.doe@email.com exists', async function () { + jest.spyOn(fs, 'existsSync').mockImplementation(() => true); + await expect(userStorage.existAsync('john.doe@email.com')).resolves.toBe(true); + }); + + it('indicates that unknown@email.com does not exist', async function () { + jest.spyOn(fs, 'existsSync').mockImplementation(() => false); + await expect(userStorage.existAsync('unknown@email.com')).resolves.toBe(false); + }); + }); + + describe('writeAsync', function () { + it('writes a new user', async function () { + jest.spyOn(fs, 'existsSync').mockImplementation(() => false); + jest.spyOn(fs.promises, 'mkdir').mockImplementation( + () => new Promise(resolve => resolve(undefined))); + + jest.spyOn(fs.promises, 'writeFile').mockImplementation(() => new Promise(resolve => resolve())); + await userStorage.writeAsync('jane.doe@email.com', { + id: 'jane.doe@email.com', + name: 'Jane Doe', + password: '12345678', + role: 'editor', + created: new Date(), + lastLoggedIn: new Date() + }); + + expect(fs.promises.mkdir).toHaveBeenCalled(); + expect(fs.promises.writeFile).toHaveBeenCalled(); + }); + }); + + describe('readAsync', function () { + it('reads the data of john.doe@email.com', async function () { + jest.spyOn(fs, 'existsSync').mockImplementation(() => true); + jest.spyOn(fs.promises, 'readFile').mockImplementation(() => new Promise( + resolve => resolve(Buffer.from(JSON.stringify(johnDoe))))); + + await expect(userStorage.readAsync('john.doe@email.com')).resolves.toEqual(johnDoe); + }); + + it('reads "undefined" for an unknown user', async function () { + jest.spyOn(fs, 'existsSync').mockImplementation(() => false); + await expect(userStorage.readAsync('unknown@email.com')).resolves.toBeUndefined(); + }); + }); + + describe('deleteAsync', function () { + it('john.doe@email.com', async function () { + jest.spyOn(fs, 'existsSync').mockImplementation(() => true); + jest.spyOn(fs.promises, 'rm').mockImplementation(() => new Promise(resolve => resolve())); + await expect(userStorage.deleteAsync('john.doe@email.com')).resolves.toBe(true); + expect(fs.promises.rm).toHaveBeenCalled(); + }); + + it('indicates that an unknown user was not deleted', async function () { + jest.spyOn(fs, 'existsSync').mockImplementation(() => false); + await expect(userStorage.deleteAsync('unknown@email.com')).resolves.toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/auth/mongo-db-cookie-storage.spec.ts b/projects/aas-server/src/test/auth/mongo-db-cookie-storage.spec.ts new file mode 100644 index 00000000..2ea532c0 --- /dev/null +++ b/projects/aas-server/src/test/auth/mongo-db-cookie-storage.spec.ts @@ -0,0 +1,152 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import 'reflect-metadata'; +import { MongoDBCookieStorage, UserCookies } from '../../app/auth/mongo-db-cookie-storage.js'; +import { describe, beforeAll, beforeEach, it, expect, jest } from '@jest/globals'; + +interface UserCookiesInstance extends UserCookies { + save(): Promise; + deleteOne: () => void; +} + +interface Promisify { + exec(): Promise; +} + +describe('MongoDBCookieStorage', function () { + let cookieStorage: MongoDBCookieStorage; + let userCookies: UserCookies; + + beforeAll(function () { + cookieStorage = new MongoDBCookieStorage(); + }); + + beforeEach(function () { + userCookies = { + id: 'john.doe@email.com', + cookies: [ + { + name: 'Cookie1', + data: 'The quick brown fox jumps over the lazy dog.' + }, + { + name: 'Cookie2', + data: '42' + } + ] + }; + }); + + describe('checkAsync', function () { + it('indicates that "Cookie1" for john.doe@email.com exist', async function () { + jest.spyOn(cookieStorage.UserCookiesModel, 'findOne').mockReturnValue(getInstance(userCookies)); + await expect(cookieStorage.checkAsync('john.doe@email.com', 'Cookie1')).resolves.toBe(true); + }); + + it('indicates that "unknown" for john.doe@email.com not exist', async function () { + jest.spyOn(cookieStorage.UserCookiesModel, 'findOne').mockReturnValue(getInstance(userCookies)); + await expect(cookieStorage.checkAsync('john.doe@email.com', 'unknown')).resolves.toBe(false); + }); + + it('indicates that "Cookie1" for jane.doe@email.com not exist', async function () { + jest.spyOn(cookieStorage.UserCookiesModel, 'findOne').mockReturnValue(getInstance()); + await expect(cookieStorage.checkAsync('jane.doe@email.com', 'Cookie1')).resolves.toBe(false); + }); + }); + + describe('getAsync', function () { + it('returns the value of "Cookie1" for john.doe@email.com', async function () { + jest.spyOn(cookieStorage.UserCookiesModel, 'findOne').mockReturnValue(getInstance(userCookies)); + + await expect(cookieStorage.getAsync('john.doe@email.com', 'Cookie1')) + .resolves.toEqual({ name: 'Cookie1', data: 'The quick brown fox jumps over the lazy dog.' }); + }); + + it('returns "undefined" for "unknown" for john.doe@email.com', async function () { + jest.spyOn(cookieStorage.UserCookiesModel, 'findOne').mockReturnValue(getInstance(userCookies)); + + await expect(cookieStorage.getAsync('john.doe@email.com', 'unknown')) + .resolves.toBeUndefined(); + }); + + it('returns "undefined" for "Cookie1" for jane.doe@email.com', async function () { + jest.spyOn(cookieStorage.UserCookiesModel, 'findOne').mockReturnValue(getInstance()); + + await expect(cookieStorage.getAsync('jane.doe@email.com', 'unknown')) + .resolves.toBeUndefined(); + }); + }); + + describe('getAllAsync', function () { + it('returns all cookies for john.doe@email.com', async function () { + jest.spyOn(cookieStorage.UserCookiesModel, 'findOne').mockReturnValue(getInstance(userCookies)); + + await expect(cookieStorage.getAllAsync('john.doe@email.com')) + .resolves.toEqual([ + { + name: 'Cookie1', + data: 'The quick brown fox jumps over the lazy dog.' + }, + { + name: 'Cookie2', + data: '42' + } + ]); + }); + }); + + describe('setAsync', function () { + it('can set a new Cookie3 for john.doe@email.com', async function () { + let save = jest.fn<() => Promise>(); + jest.spyOn(cookieStorage.UserCookiesModel, 'findOne').mockReturnValue(getInstance(userCookies, save)); + + await cookieStorage.setAsync('john.doe@email.com', 'Cookie3', 'Hello World!'); + expect(save).toHaveBeenCalled(); + }); + + it('can update the existing Cookie2 for john.doe@email.com', async function () { + let save = jest.fn<() => Promise>(); + jest.spyOn(cookieStorage.UserCookiesModel, 'findOne').mockReturnValue(getInstance(userCookies, save)); + + await cookieStorage.setAsync('john.doe@email.com', 'Cookie2', 'Hello World!'); + expect(save).toHaveBeenCalled(); + }); + }); + + describe('deleteAsync', function () { + it('can delete a cookie', async function () { + let save = jest.fn<() => Promise>(); + let deleteOne = jest.fn<() => Promise>(); + jest.spyOn(cookieStorage.UserCookiesModel, 'findOne').mockReturnValue(getInstance(userCookies, save, deleteOne)); + + await cookieStorage.deleteAsync('john.doe@email.com', 'Cookie1'); + expect(save).toHaveBeenCalled(); + + await cookieStorage.deleteAsync('john.doe@email.com', 'Cookie2'); + expect(deleteOne).toHaveBeenCalled(); + }); + }); + + function getInstance(user?: UserCookies, save?: () => Promise, deleteOne?: () => Promise): any { + if (user) { + return { + exec: () => new Promise((resolve, _) => resolve( + { + ...user, + save: save ?? (() => new Promise((result, _) => result())), + deleteOne: deleteOne ?? (() => new Promise((result, _) => result())) + })) + } as Promisify; + } + + return { + exec: () => new Promise((resolve, _) => resolve(undefined)) + } as Promisify; + } +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/auth/mongo-db-user-storage.spec.ts b/projects/aas-server/src/test/auth/mongo-db-user-storage.spec.ts new file mode 100644 index 00000000..7e4cf0c9 --- /dev/null +++ b/projects/aas-server/src/test/auth/mongo-db-user-storage.spec.ts @@ -0,0 +1,96 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import 'reflect-metadata'; +import { MongoDBUserStorage } from '../../app/auth/mongo-db-user-storage.js'; +import { UserData } from '../../app/auth/user-data.js'; +import { describe, beforeAll, beforeEach, it, expect, jest } from '@jest/globals'; + +interface UserDataInstance extends UserData { + save(): Promise; +} + +interface Promisify { + exec(): Promise; +} + +describe('MongoDBUserStorage', function () { + let userStorage: MongoDBUserStorage; + let johnDoe: UserData; + + beforeAll(function () { + userStorage = new MongoDBUserStorage(); + }); + + beforeEach(function () { + johnDoe = { + id: "john.doe@email.com", + name: "John Doe", + role: "editor", + password: "$2a$10$6qZT2ZM5jUVU/pLLQUjCvuXplG.GwPnoz48C1Eg/dKqjIrGE8jm0a", + created: new Date(), + lastLoggedIn: new Date(0) + }; + }); + + it('indicates that john.doe@email.com exists', async function () { + jest.spyOn(userStorage.UserDataModel, 'findOne').mockReturnValue(getPromisify(johnDoe)); + await expect(userStorage.existAsync('john.doe@email.com')).resolves.toBe(true); + }); + + it('indicates that unknown@email.com does not exist', async function () { + jest.spyOn(userStorage.UserDataModel, 'findOne').mockReturnValue(getPromisify()); + await expect(userStorage.existAsync('unknown@email.com')).resolves.toBe(false); + }); + + it('reads the data of john.doe@email.com', async function () { + jest.spyOn(userStorage.UserDataModel, 'findOne').mockReturnValue(getPromisify(johnDoe)); + let user = (await userStorage.readAsync('john.doe@email.com'))!; + expect(user).toBeDefined(); + expect(user.id).toEqual(johnDoe.id); + expect(user.name).toEqual(johnDoe.name); + expect(user.role).toEqual(johnDoe.role); + expect(user.password).toEqual(johnDoe.password); + }); + + it('reads "undefined" for an unknown user', async function () { + jest.spyOn(userStorage.UserDataModel, 'findOne').mockReturnValue(getPromisify()); + await expect(userStorage.readAsync('unknown@email.com')).resolves.toBe(undefined); + }); + + it('updates the data of john.doe@email.com', async function () { + let save = jest.fn<() => Promise>(); + jest.spyOn(userStorage.UserDataModel, 'findOne').mockReturnValue(getPromisify(johnDoe, save)); + await userStorage.writeAsync('john.doe@email.com', { ...johnDoe }); + expect(save).toHaveBeenCalled + }); + + it('deletes john.doe@email.com', async function () { + jest.spyOn(userStorage.UserDataModel, 'findOneAndRemove').mockReturnValue(getPromisify(johnDoe)); + await expect(userStorage.deleteAsync('john.doe@email.com')).resolves.toBe(true); + }); + + function getPromisify(user?: UserData, save?: () => Promise): any { + if (user) { + return { + exec: () => new Promise((resolve, _) => resolve(getInstance(user, save))) + } as Promisify; + } + + return { + exec: () => new Promise((resolve, _) => resolve(undefined)) + } as Promisify; + } + + function getInstance(user: UserData, save?: () => Promise): UserDataInstance { + return { + ...user, + save: save ?? (() => new Promise((result, _) => result())) + } + } +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/configuration.spec.ts b/projects/aas-server/src/test/configuration.spec.ts new file mode 100644 index 00000000..c0e5a551 --- /dev/null +++ b/projects/aas-server/src/test/configuration.spec.ts @@ -0,0 +1,81 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import { createEndpoint, getEndpointName, getEndpointType } from "../app/configuration.js"; +import { describe, it, expect } from '@jest/globals' + +describe('configuration', function () { + describe('getEndpointName', function () { + it('gets the endpoint name from an URL string', function () { + expect(getEndpointName('http://localhost:1234/?name=Test')).toEqual('Test'); + }); + + it('gets the endpoint name from a URL', function () { + expect(getEndpointName(new URL('http://localhost:1234/?name=Test'))).toEqual('Test'); + }); + + it('gets the default name of an AASX server', function () { + expect(getEndpointName('http://localhost:1234/')).toEqual('http://localhost:1234/'); + }); + + it('gets the default name of an AAS registry', function () { + expect(getEndpointName('http://localhost:1234/v1/registry?type=AASRegistry')) + .toEqual('http://localhost:1234/v1/registry'); + }); + + it('gets the default name of an OPCUA server', function () { + expect(getEndpointName(new URL('opc.tcp://172.16.160.178:30001/I4AASServer'))) + .toEqual('opc.tcp://172.16.160.178:30001/I4AASServer'); + }); + + it('gets the default name of an AASX directory', function () { + expect(getEndpointName('file:///samples')).toEqual('file:///samples'); + }); + }); + + describe('getEndpointType', function () { + it('gets the endpoint type from an URL string', function () { + expect(getEndpointType('http://localhost:1234/?type=AasxDirectory')).toEqual('AasxDirectory'); + }); + + it('gets the endpoint type from a URL', function () { + expect(getEndpointType(new URL('http://localhost:1234/?type=OpcuaServer'))).toEqual('OpcuaServer'); + }); + + it('gets "AasxServer" as default', function () { + expect(getEndpointType('http://localhost:1234/')).toEqual('AasxServer'); + }); + }); + + describe('createEndpoint', function () { + it('creates an endpoint with options', function () { + expect(createEndpoint( + 'http://localhost:1234', + { + name: 'Test', + params: [['version', '3.0']] + }).href) + .toEqual('http://localhost:1234/?name=Test&version=3.0'); + }); + + it('creates an AASX server endpoint', function () { + expect(createEndpoint('http://localhost:1234/?type=AasxServer', 'Test').href) + .toEqual('http://localhost:1234/?name=Test'); + }); + + it('creates an AAS registry endpoint', function () { + expect(createEndpoint('http://localhost:1234/v1/registry?type=AASRegistry', 'Test').href) + .toEqual('http://localhost:1234/v1/registry?name=Test&type=AASRegistry'); + }); + + it('creates an AASX directory endpoint', function () { + expect(createEndpoint('file:///storage/samples', 'Test').href) + .toEqual('file:///storage/samples?name=Test'); + }); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/controller/app-controller.spec.ts b/projects/aas-server/src/test/controller/app-controller.spec.ts new file mode 100644 index 00000000..566dea4d --- /dev/null +++ b/projects/aas-server/src/test/controller/app-controller.spec.ts @@ -0,0 +1,111 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import 'reflect-metadata'; +import { container } from 'tsyringe'; +import express, { Express, json, urlencoded } from 'express'; +import morgan from 'morgan'; +import request from 'supertest'; +import { Logger } from '../../app/logging/logger.js'; +import { Message, PackageInfo } from 'common'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; + +import { ApplicationInfo } from '../../app/application-info.js'; +import { AuthService } from '../../app/auth/auth-service.js'; +import { createSpyObj } from '../utils.js'; +import { Variable } from '../../app/variable.js'; +import { getToken, guestPayload } from '../assets/json-web-token.js'; +import { RegisterRoutes } from '../../app/routes/routes.js'; +import { Authentication } from '../../app/controller/authentication.js'; +import { errorHandler } from '../assets/error-handler.js'; + +describe('AppController', function () { + let app: Express; + let logger: Logger; + let auth: jest.Mocked; + let applicationInfo: jest.Mocked; + let variable: jest.Mocked; + let authentication: jest.Mocked; + + beforeEach(function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + variable = createSpyObj({}, { JWT_SECRET: 'SecretSecretSecretSecretSecretSecret' }); + auth = createSpyObj( + [ + 'hasUserAsync', + 'loginAsync', + 'getCookieAsync', + 'getCookiesAsync', + 'setCookieAsync', + 'deleteCookieAsync' + ]); + + applicationInfo = createSpyObj(['getAsync', 'getMessages']); + + authentication = createSpyObj(['checkAsync']); + authentication.checkAsync.mockResolvedValue(guestPayload); + + container.registerInstance(AuthService, auth); + container.registerInstance('Logger', logger); + container.registerInstance(Variable, variable); + container.registerInstance(ApplicationInfo, applicationInfo); + container.registerInstance(Authentication, authentication); + + app = express(); + app.use(json()); + app.use(urlencoded({ extended: true })); + app.use(morgan('dev')); + app.set('trust proxy', 1); + + RegisterRoutes(app); + app.use(errorHandler); + }); + + it('getInfo: /api/v1/app/info', async function () { + const data: PackageInfo = { + name: "aas-portal-project", + version: "2.0.0", + description: "Web-based visualization and control of asset administration shells.", + author: "Fraunhofer IOSB-INA e.V.", + homepage: "https://www.iosb-ina.fraunhofer.de/", + license: "Apache-2.0", + libraries: [ + { + name: "Library", + version: "1.0", + description: "A library.", + license: "MIT", + homepage: "https://www.iosb-ina.fraunhofer.de/" + } + ] + }; + + applicationInfo.getAsync.mockReturnValue(new Promise(resolve => resolve(data))); + const response = await request(app) + .get('/api/v1/app/info') + .set('Authorization', `Bearer ${getToken()}`); + + expect(response.statusCode).toBe(200); + expect(response.body).toEqual(data); + }); + + it('getMessages: /api/v1/app/messages', async function () { + const messages: Message[] = [ + { type: 'Info', text: 'An information.', timestamp: 0 }, + { type: 'Error', text: 'An error.', timestamp: 1 } + ]; + + applicationInfo.getMessages.mockReturnValue(messages); + const response = await request(app) + .get('/api/v1/app/messages') + .set('Authorization', `Bearer ${getToken()}`); + + expect(response.statusCode).toBe(200) + expect(response.body).toEqual(messages); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/controller/auth-controller.spec.ts b/projects/aas-server/src/test/controller/auth-controller.spec.ts new file mode 100644 index 00000000..8ab9aba8 --- /dev/null +++ b/projects/aas-server/src/test/controller/auth-controller.spec.ts @@ -0,0 +1,188 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import 'reflect-metadata'; +import { container } from 'tsyringe'; +import express, { Express, json, urlencoded } from 'express'; +import morgan from 'morgan'; +import request from 'supertest'; +import { AuthResult, Cookie, Credentials } from 'common'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; + +import { AuthService } from '../../app/auth/auth-service.js'; +import { createSpyObj } from '../utils.js'; +import { getToken, guestPayload } from '../assets/json-web-token.js'; +import { RegisterRoutes } from '../../app/routes/routes.js'; +import { Logger } from '../../app/logging/logger.js'; +import { Variable } from '../../app/variable.js'; +import { Authentication } from '../../app/controller/authentication.js'; +import { errorHandler } from '../assets/error-handler.js'; + +describe('AuthController', function () { + let app: Express; + let auth: jest.Mocked; + let logger: Logger; + let variable: jest.Mocked; + let authentication: jest.Mocked; + + beforeEach(function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + variable = createSpyObj({}, { JWT_SECRET: 'SecretSecretSecretSecretSecretSecret' }); + auth = createSpyObj( + [ + 'hasUserAsync', + 'loginAsync', + 'getCookieAsync', + 'getCookiesAsync', + 'setCookieAsync', + 'deleteCookieAsync' + ]); + + authentication = createSpyObj(['checkAsync']); + authentication.checkAsync.mockResolvedValue(guestPayload); + + container.registerInstance(AuthService, auth); + container.registerInstance('Logger', logger); + container.registerInstance(Variable, variable); + container.registerInstance(Authentication, authentication); + + app = express(); + app.use(json()); + app.use(urlencoded({ extended: true })); + app.use(morgan('dev')); + app.set('trust proxy', 1); + + RegisterRoutes(app); + app.use(errorHandler); + }); + + describe('guest', function () { + it('creates a guest account', async function () { + const token = getToken(); + auth.loginAsync.mockReturnValue(new Promise(resolve => resolve({ token }))); + + const response = await request(app) + .post('/api/v1/guest'); + + expect(response.statusCode).toBe(200); + expect(response.body).toEqual({ token } as AuthResult); + }); + }); + + describe('login', function () { + it('login a registered user', async function () { + const token = getToken('John'); + auth.loginAsync.mockReturnValue(new Promise(resolve => resolve({ token }))); + + const response = await request(app) + .post('/api/v1/login') + .send({ id: 'john.doe@email.com', password: '1234.xyz' } as Credentials); + + expect(response.statusCode).toBe(200); + expect(response.body).toEqual({ token } as AuthResult); + }); + }); + + describe('getCookie', function () { + it('GET /api/v1/users/am9obi5kb2VAZW1haWwuY29t/cookies/Cookie1', async function () { + auth.hasUserAsync.mockReturnValue(new Promise(resolve => resolve(true))); + auth.getCookieAsync.mockReturnValue( + new Promise(resolve => resolve({ name: 'Cookie1', data: 'Hello World!' }))); + + const response = await request(app) + .get('/api/v1/users/am9obi5kb2VAZW1haWwuY29t/cookies/Cookie1') + .set('Authorization', `Bearer ${getToken('John')}`); + + expect(response.statusCode).toBe(200); + expect(response.body).toEqual({ name: 'Cookie1', data: 'Hello World!' }); + }); + + // it('Unauthenticated user: GET /api/v1/users/am9obi5kb2VAZW1haWwuY29t/cookies/Cookie1', async function () { + // authentication.checkAsync.mockReturnValue(new Promise((_, reject) => reject( + // new ApplicationError(ERRORS.UnauthorizedAccess, ERRORS.UnauthorizedAccess)))); + + // const response = await request(app).get('/api/v1/users/am9obi5kb2VAZW1haWwuY29t/cookies/Cookie1'); + // expect(response.statusCode).toBe(401); + // }); + }); + + describe('getCookies', function () { + it('GET /api/v1/users/am9obi5kb2VAZW1haWwuY29t/cookies', async function () { + auth.hasUserAsync.mockReturnValue(new Promise(resolve => resolve(true))); + auth.getCookiesAsync.mockReturnValue( + new Promise(resolve => resolve([ + { name: 'Cookie1', data: 'Hello World!' }, + { name: 'Cookie2', data: '42' } + ]))); + + const response = await request(app) + .get('/api/v1/users/am9obi5kb2VAZW1haWwuY29t/cookies') + .set('Authorization', `Bearer ${getToken('John')}`); + + expect(response.statusCode).toBe(200); + expect(response.body).toEqual([ + { name: 'Cookie1', data: 'Hello World!' }, + { name: 'Cookie2', data: '42' } + ]); + }); + + // it('Unauthenticated user: GET /api/v1/users/am9obi5kb2VAZW1haWwuY29t/cookies', async function () { + // authentication.checkAsync.mockReturnValue( + // Promise.reject(new ApplicationError(ERRORS.UnauthorizedAccess, ERRORS.UnauthorizedAccess))); + + // const response = await request(app).get('/api/v1/users/am9obi5kb2VAZW1haWwuY29t/cookies'); + // expect(response.statusCode).toBe(401); + // }); + }); + + describe('setCookie', function () { + it('POST /api/v1/users/am9obi5kb2VAZW1haWwuY29t/cookies/Cookie1', async function () { + auth.hasUserAsync.mockReturnValue(new Promise(resolve => resolve(true))); + const response = await request(app) + .post('/api/v1/users/am9obi5kb2VAZW1haWwuY29t/cookies/Cookie1') + .send({ name: 'Cookie1', data: 'Hello World!' } as Cookie) + .set('Authorization', `Bearer ${getToken('John')}`) + .set('Accept', 'application/json'); + + expect(response.statusCode).toBe(204); + expect(auth.setCookieAsync).toHaveBeenCalled(); + }); + + // it('Unauthenticated user: POST /api/v1/users/am9obi5kb2VAZW1haWwuY29t/cookies/Cookie1', async function () { + // authentication.checkAsync.mockReturnValue( + // Promise.reject(new ApplicationError(ERRORS.UnauthorizedAccess, ERRORS.UnauthorizedAccess))); + + // const response = await request(app) + // .post('/api/v1/users/am9obi5kb2VAZW1haWwuY29t/cookies/Cookie1') + // .send({ name: 'Cookie1', data: 'Hello World!' } as Cookie) + // .set('Accept', 'application/json'); + + // expect(response.statusCode).toBe(401); + // }); + }); + + describe('deleteCookie', function () { + it('DELETE /api/v1/users/am9obi5kb2VAZW1haWwuY29t/cookies/Cookie1', async function () { + auth.hasUserAsync.mockReturnValue(new Promise(resolve => resolve(true))); + const response = await request(app) + .delete('/api/v1/users/am9obi5kb2VAZW1haWwuY29t/cookies/Cookie1') + .set('Authorization', `Bearer ${getToken('John')}`); + + expect(response.statusCode).toBe(204); + expect(auth.deleteCookieAsync).toHaveBeenCalled(); + }); + + // it('Unauthenticated user: DELETE /api/v1/users/am9obi5kb2VAZW1haWwuY29t/cookies/Cookie1', async function () { + // authentication.checkAsync.mockReturnValue( + // Promise.reject(new ApplicationError(ERRORS.UnauthorizedAccess, ERRORS.UnauthorizedAccess))); + + // const response = await request(app).delete('/api/v1/users/am9obi5kb2VAZW1haWwuY29t/cookies/Cookie1'); + // expect(response.statusCode).toBe(401); + // }); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/controller/containers-controller.spec.ts b/projects/aas-server/src/test/controller/containers-controller.spec.ts new file mode 100644 index 00000000..b2b0237b --- /dev/null +++ b/projects/aas-server/src/test/controller/containers-controller.spec.ts @@ -0,0 +1,255 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import 'reflect-metadata'; +import { container } from 'tsyringe'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; +import express, { Express, json, urlencoded } from 'express'; +import morgan from 'morgan'; +import request from 'supertest'; +import { Readable } from 'stream'; +import { resolve } from 'path'; +import { aas } from 'common'; + +import { Logger } from '../../app/logging/logger.js'; +import { AuthService } from '../../app/auth/auth-service.js'; +import { AASProvider } from '../../app/aas-provider/aas-provider.js'; +import { sampleDocument } from '../assets/sample-document.js'; +import { Container } from '../../app/aas-provider/container.js'; +import { createSpyObj } from '../utils.js'; +import { Variable } from '../../app/variable.js'; +import { getToken, guestPayload } from '../assets/json-web-token.js'; +import { RegisterRoutes } from '../../app/routes/routes.js'; +import { Authentication } from '../../app/controller/authentication.js'; +import { errorHandler } from '../assets/error-handler.js'; + +describe.skip('ContainersController', function () { + let app: Express; + let logger: Logger; + let auth: jest.Mocked; + let aasProvider: jest.Mocked; + let variable: jest.Mocked; + let authentication: jest.Mocked; + + beforeEach(function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + variable = createSpyObj({}, { JWT_SECRET: 'SecretSecretSecretSecretSecretSecret' }); + auth = createSpyObj( + [ + 'hasUserAsync', + 'loginAsync', + 'getCookieAsync', + 'getCookiesAsync', + 'setCookieAsync', + 'deleteCookieAsync' + ]); + + aasProvider = createSpyObj( + [ + 'updateDocumentAsync', + 'getWorkspaces', + 'getContainer', + 'getContentAsync', + 'getDocuments', + 'getDocument', + 'getDocumentAsync', + 'addDocumentsAsync', + 'deleteDocumentAsync', + 'getDataElementValueAsync', + 'invoke', + 'resetAsync' + ]); + + authentication = createSpyObj(['checkAsync']); + authentication.checkAsync.mockResolvedValue(guestPayload); + + container.registerInstance(AuthService, auth); + container.registerInstance('Logger', logger); + container.registerInstance(Variable, variable); + container.registerInstance(AASProvider, aasProvider); + container.registerInstance(Authentication, authentication); + + app = express(); + app.use(json()); + app.use(urlencoded({ extended: true })); + app.use(morgan('dev')); + app.set('trust proxy', 1); + + RegisterRoutes(app); + app.use(errorHandler); + }); + + it('getDocuments: /api/v1/containers/:url/documents', async function () { + aasProvider.getDocuments.mockReturnValue([sampleDocument]); + const response = await request(app) + .get('/api/v1/containers/aHR0cDovL2xvY2FsaG9zdC90ZXN0L2NvbnRhaW5lcg/documents') + .set('Authorization', `Bearer ${getToken()}`); + + expect(response.statusCode).toBe(200); + expect(response.body).toEqual([sampleDocument]); + expect(aasProvider.getDocuments).toHaveBeenCalled(); + }); + + it('getDocument: /api/v1/containers/:url/documents/:id', async function () { + aasProvider.getDocumentAsync.mockReturnValue(new Promise(resolve => { + const s = new Readable(); + s.push('Hello World!'); + s.push(null); + resolve(s); + })); + + const response = await request(app) + .get(`/api/v1/containers/aHR0cDovL2xvY2FsaG9zdC90ZXN0L2NvbnRhaW5lcg/documents/aHR0cDovL2N1c3RvbWVyLmNvbS9hYXMvOTE3NV83MDEzXzcwOTFfOTE2OA`) + .set('Authorization', `Bearer ${getToken()}`); + + expect(response.statusCode).toBe(200); + expect(response.body).toBeTruthy(); + expect(aasProvider.getDocumentAsync).toHaveBeenCalled(); + }); + + it('addDocuments: /api/v1/containers/:url/documents', async function () { + const container = createSpyObj({}, { url: new URL('file:///samples') }); + aasProvider.getContainer.mockReturnValue(container); + auth.hasUserAsync.mockReturnValue(new Promise(resolve => resolve(true))); + const response = await request(app) + .post('/api/v1/containers/ZmlsZTovLy9zYW1wbGVz/documents') + .set('Authorization', `Bearer ${getToken('John')}`) + .attach('files', resolve('./src/test/assets/samples/example-motor.aasx')); + + expect(response.statusCode).toBe(204); + expect(aasProvider.addDocumentsAsync).toHaveBeenCalled(); + }); + + it('deleteDocument: /api/v1/containers/:url/documents/:id', async function () { + auth.hasUserAsync.mockReturnValue(new Promise(resolve => resolve(true))); + const response = await request(app) + .delete('/api/v1/containers/aHR0cDovL2xvY2FsaG9zdC90ZXN0L2NvbnRhaW5lcg/documents/aHR0cDovL2N1c3RvbWVyLmNvbS9hYXMvOTE3NV83MDEzXzcwOTFfOTE2OA') + .set('Authorization', `Bearer ${getToken('John')}`); + + expect(response.statusCode).toBe(204); + expect(aasProvider.deleteDocumentAsync).toHaveBeenCalled(); + }); + + it('getDocumentContent: /api/v1/containers/:url/documents/:id/content', async function () { + aasProvider.getContentAsync.mockReturnValue(new Promise(resolve => { + resolve({ assetAdministrationShells: [], submodels: [], conceptDescriptions: [] }); + })); + + const response = await request(app) + .get('/api/v1/containers/Y29udGFpbmVy/documents/ZG9jdW1lbnQ/content') + .set('Authorization', `Bearer ${getToken()}`); + + expect(response.statusCode).toBe(200); + expect(aasProvider.getContentAsync).toHaveBeenCalled(); + }); + + it('reset: /api/v1/containers/reset', async function () { + auth.hasUserAsync.mockReturnValue(new Promise(resolve => resolve(true))); + aasProvider.resetAsync.mockReturnValue(new Promise(resolve => resolve())); + const response = await request(app) + .delete('/api/v1/containers/reset') + .set('Authorization', `Bearer ${getToken('John')}`); + + expect(response.statusCode).toBe(204); + expect(aasProvider.resetAsync).toHaveBeenCalled(); + }); + + describe('getDataElementValue: /api/v1/containers/:url/documents/:id/submodels/:smId/submodel-elements/:path/value', function () { + it('gets the value of a File that represents an image', async function () { + aasProvider.getDataElementValueAsync.mockReturnValue(new Promise(resolve => { + const s = new Readable(); + s.push('Hello World!'); + s.push(null); + resolve(s); + })); + + const response = await request(app) + .get('/api/v1/containers/Y29udGFpbmVy/documents/ZG9jdW1lbnQ/submodels/U3VibW9kZWw/submodel-elements/collection.file/value?width=200&height=100') + .set('Authorization', `Bearer ${getToken()}`); + + expect(response.statusCode).toBe(200); + expect(aasProvider.getDataElementValueAsync).toHaveBeenCalled(); + }); + + it('gets the value of a File', async function () { + aasProvider.getDataElementValueAsync.mockReturnValue(new Promise(resolve => { + const s = new Readable(); + s.push('Hello World!'); + s.push(null); + resolve(s); + })); + + const response = await request(app) + .get('/api/v1/containers/Y29udGFpbmVy/documents/ZG9jdW1lbnQ/submodels/U3VibW9kZWw/submodel-elements/collection.file/value') + .set('Authorization', `Bearer ${getToken()}`); + + expect(response.statusCode).toBe(200); + expect(response.text).toEqual('Hello World!'); + expect(aasProvider.getDataElementValueAsync).toHaveBeenCalled(); + }); + + it('gets the value of a Blob', async function () { + aasProvider.getDataElementValueAsync.mockReturnValue(new Promise(resolve => { + const s = new Readable(); + s.push(Buffer.from('Hello world!').toString('base64')); + s.push(null); + resolve(s); + })); + + const response = await request(app) + .get(`/api/v1/containers/Y29udGFpbmVy/documents/ZG9jdW1lbnQ/submodels/U3VibW9kZWw/submodel-elements/collection.blob/value`) + .set('Authorization', `Bearer ${getToken()}`); + + expect(response.statusCode).toBe(200); + expect(response.text).toEqual(Buffer.from('Hello world!').toString('base64')); + expect(aasProvider.getDataElementValueAsync).toHaveBeenCalled(); + }); + }); + + it('updateDocument: /api/v1/containers/:url/documents/:id', async function () { + const content: aas.Environment = { + assetAdministrationShells: [], + submodels: [], + conceptDescriptions: [] + }; + + aasProvider.updateDocumentAsync.mockReturnValue(Promise.resolve([])); + auth.hasUserAsync.mockReturnValue(new Promise(resolve => resolve(true))); + + const url = Buffer.from('http://localhost/container').toString('base64url'); + const id = Buffer.from('http://localhost/document').toString('base64url'); + + const response = await request(app) + .put(`/api/v1/containers/${url}/documents/${id}`) + .set('Authorization', `Bearer ${getToken('John')}`) + .send(content); + + expect(response.statusCode).toBe(200); + expect(aasProvider.updateDocumentAsync).toHaveBeenCalled(); + }); + + it('invokeOperation: /api/v1/containers/:url/documents/:id/invoke', async function () { + const operation: aas.Operation = { + idShort: 'noop', + modelType: 'Operation', + }; + + aasProvider.invoke.mockReturnValue(Promise.resolve(operation)); + auth.hasUserAsync.mockReturnValue(new Promise(resolve => resolve(true))); + + const url = Buffer.from('http://localhost/container').toString('base64url'); + const id = Buffer.from('http://localhost/document').toString('base64url'); + const response = await request(app) + .post(`/api/v1/containers/${url}/documents/${id}/invoke`) + .set('Authorization', `Bearer ${getToken('John')}`) + .send(operation); + + expect(response.statusCode).toBe(200); + expect(aasProvider.invoke).toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/controller/documents-controller.spec.ts b/projects/aas-server/src/test/controller/documents-controller.spec.ts new file mode 100644 index 00000000..26c06037 --- /dev/null +++ b/projects/aas-server/src/test/controller/documents-controller.spec.ts @@ -0,0 +1,94 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import 'reflect-metadata'; +import { container } from 'tsyringe'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; +import express, { Express, json, urlencoded } from 'express'; +import morgan from 'morgan'; +import request from 'supertest'; + +import { Logger } from '../../app/logging/logger.js'; +import { AuthService } from '../../app/auth/auth-service.js'; +import { AASProvider } from '../../app/aas-provider/aas-provider.js'; +import { sampleDocument } from '../assets/sample-document.js'; +import { createSpyObj } from '../utils.js'; +import { Variable } from '../../app/variable.js'; +import { getToken, guestPayload } from '../assets/json-web-token.js'; +import { RegisterRoutes } from '../../app/routes/routes.js'; +import { Authentication } from '../../app/controller/authentication.js'; +import { errorHandler } from '../assets/error-handler.js'; + +describe('DocumentsController', function () { + let app: Express; + let logger: Logger; + let auth: jest.Mocked; + let aasProvider: jest.Mocked; + let variable: jest.Mocked; + let authentication: jest.Mocked; + + beforeEach(function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + variable = createSpyObj({}, { JWT_SECRET: 'SecretSecretSecretSecretSecretSecret' }); + auth = createSpyObj( + [ + 'hasUserAsync', + 'loginAsync', + 'getCookieAsync', + 'getCookiesAsync', + 'setCookieAsync', + 'deleteCookieAsync' + ]); + + + aasProvider = createSpyObj( + [ + 'updateDocumentAsync', + 'getWorkspaces', + 'getContainer', + 'getContentAsync', + 'getDocuments', + 'getDocument', + 'getDocumentAsync', + 'addDocumentsAsync', + 'deleteDocumentAsync', + 'getDataElementValueAsync', + 'invoke', + 'resetAsync' + ]); + + authentication = createSpyObj(['checkAsync']); + authentication.checkAsync.mockResolvedValue(guestPayload); + + container.registerInstance(AuthService, auth); + container.registerInstance('Logger', logger); + container.registerInstance(Variable, variable); + container.registerInstance(AASProvider, aasProvider); + container.registerInstance(Authentication, authentication); + + app = express(); + app.use(json()); + app.use(urlencoded({ extended: true })); + app.use(morgan('dev')); + app.set('trust proxy', 1); + + RegisterRoutes(app); + app.use(errorHandler); + }); + + it('getDocument: /api/v1/documents/:id', async function () { + aasProvider.getDocument.mockReturnValue(sampleDocument); + const response = await request(app) + .get('/api/v1/documents/aHR0cDovL2N1c3RvbWVyLmNvbS9hYXMvOTE3NV83MDEzXzcwOTFfOTE2OA') + .set('Authorization', `Bearer ${getToken()}`); + + expect(response.statusCode).toBe(200); + expect(response.body).toEqual(sampleDocument); + expect(aasProvider.getDocument).toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/controller/endpoints-controller.spec.ts b/projects/aas-server/src/test/controller/endpoints-controller.spec.ts new file mode 100644 index 00000000..9aa02222 --- /dev/null +++ b/projects/aas-server/src/test/controller/endpoints-controller.spec.ts @@ -0,0 +1,95 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import 'reflect-metadata'; +import { container } from 'tsyringe'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; +import express, { Express, json, urlencoded } from 'express'; +import morgan from 'morgan'; +import request from 'supertest'; + +import { Logger } from '../../app/logging/logger.js'; +import { AuthService } from '../../app/auth/auth-service.js'; +import { AASProvider } from '../../app/aas-provider/aas-provider.js'; +import { createSpyObj } from '../utils.js'; +import { Variable } from '../../app/variable.js'; +import { getToken, guestPayload } from '../assets/json-web-token.js'; +import { RegisterRoutes } from '../../app/routes/routes.js'; +import { Authentication } from '../../app/controller/authentication.js'; +import { errorHandler } from '../assets/error-handler.js'; + +describe('EndpointsController', function () { + let app: Express; + let logger: Logger; + let auth: jest.Mocked; + let aasProvider: jest.Mocked; + let variable: jest.Mocked; + let authentication: jest.Mocked; + + beforeEach(function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + variable = createSpyObj({}, { JWT_SECRET: 'SecretSecretSecretSecretSecretSecret' }); + auth = createSpyObj( + [ + 'hasUserAsync', + 'loginAsync', + 'getCookieAsync', + 'getCookiesAsync', + 'setCookieAsync', + 'deleteCookieAsync' + ]); + + aasProvider = createSpyObj( + [ + 'addEndpointAsync', + 'removeEndpointAsync', + ]); + + authentication = createSpyObj(['checkAsync']); + authentication.checkAsync.mockResolvedValue(guestPayload); + + container.registerInstance(AuthService, auth); + container.registerInstance('Logger', logger); + container.registerInstance(Variable, variable); + container.registerInstance(AASProvider, aasProvider); + container.registerInstance(Authentication, authentication); + + app = express(); + app.use(json()); + app.use(urlencoded({ extended: true })); + app.use(morgan('dev')); + app.set('trust proxy', 1); + + RegisterRoutes(app); + app.use(errorHandler); + }); + + it('POST: /api/v1/endpoints/:name', async function () { + const url = new URL('file:///assets/samples'); + url.searchParams.append('type', 'AasxDirectory'); + aasProvider.addEndpointAsync.mockReturnValue(new Promise(resolve => resolve())); + auth.hasUserAsync.mockReturnValue(new Promise(resolve => resolve(true))); + const response = await request(app).post('/api/v1/endpoints/samples') + .set('Authorization', `Bearer ${getToken('John')}`) + .send({ url: url.href }); + + expect(response.statusCode).toBe(204); + expect(aasProvider.addEndpointAsync).toHaveBeenCalled(); + }); + + it('DELETE: /api/v1/endpoints/:name', async function () { + aasProvider.removeEndpointAsync.mockReturnValue(new Promise(resolve => resolve())); + auth.hasUserAsync.mockReturnValue(new Promise(resolve => resolve(true))); + const response = await request(app) + .delete('/api/v1/endpoints/samples') + .set('Authorization', `Bearer ${getToken('John')}`); + + expect(response.statusCode).toBe(204); + expect(aasProvider.removeEndpointAsync).toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/controller/templates-controller.spec.ts b/projects/aas-server/src/test/controller/templates-controller.spec.ts new file mode 100644 index 00000000..9e941c71 --- /dev/null +++ b/projects/aas-server/src/test/controller/templates-controller.spec.ts @@ -0,0 +1,80 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import 'reflect-metadata'; +import { container } from 'tsyringe'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; +import express, { Express, json, urlencoded } from 'express'; +import morgan from 'morgan'; +import request from 'supertest'; + +import { Logger } from '../../app/logging/logger.js'; +import { TemplateDescriptor } from 'common'; +import { AuthService } from '../../app/auth/auth-service.js'; +import { createSpyObj } from '../utils.js'; +import { Variable } from '../../app/variable.js'; +import { getToken, guestPayload } from '../assets/json-web-token.js'; +import { RegisterRoutes } from '../../app/routes/routes.js'; +import { Authentication } from '../../app/controller/authentication.js'; +import { errorHandler } from '../assets/error-handler.js'; +import { TemplateStorage } from '../../app/template/template-storage.js'; + +describe('TemplateController', function () { + let app: Express; + let logger: Logger; + let auth: jest.Mocked; + let templateStorage: jest.Mocked; + let variable: jest.Mocked; + let authentication: jest.Mocked; + + beforeEach(function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + variable = createSpyObj({}, { JWT_SECRET: 'SecretSecretSecretSecretSecretSecret' }); + auth = createSpyObj( + [ + 'hasUserAsync', + 'loginAsync', + 'getCookieAsync', + 'getCookiesAsync', + 'setCookieAsync', + 'deleteCookieAsync' + ]); + + templateStorage = createSpyObj(['readAsync']); + + authentication = createSpyObj(['checkAsync']); + authentication.checkAsync.mockResolvedValue(guestPayload); + + container.registerInstance(AuthService, auth); + container.registerInstance('Logger', logger); + container.registerInstance(Variable, variable); + container.registerInstance('TemplateStorage', templateStorage); + container.registerInstance(Authentication, authentication); + + app = express(); + app.use(json()); + app.use(urlencoded({ extended: true })); + app.use(morgan('dev')); + app.set('trust proxy', 1); + + RegisterRoutes(app); + app.use(errorHandler); + }); + + it('getTemplates: /api/v1/templates', async function () { + const templates: TemplateDescriptor[] = [{ name: 'TestTemplate' }]; + templateStorage.readAsync.mockReturnValue(new Promise(resolve => resolve(templates))); + const response = await request(app) + .get('/api/v1/templates') + .set('Authorization', `Bearer ${getToken()}`); + + expect(response.statusCode).toBe(200) + expect(response.body).toEqual(templates); + expect(templateStorage.readAsync).toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/controller/workspaces-controller.spec.ts b/projects/aas-server/src/test/controller/workspaces-controller.spec.ts new file mode 100644 index 00000000..6f2c63a0 --- /dev/null +++ b/projects/aas-server/src/test/controller/workspaces-controller.spec.ts @@ -0,0 +1,87 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import 'reflect-metadata'; +import { container } from 'tsyringe'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; +import express, { Express, json, urlencoded } from 'express'; +import morgan from 'morgan'; +import request from 'supertest'; + +import { Logger } from '../../app/logging/logger.js'; +import { AuthService } from '../../app/auth/auth-service.js'; +import { AASProvider } from '../../app/aas-provider/aas-provider.js'; +import { Variable } from '../../app/variable.js'; +import { createSpyObj } from '../utils.js'; +import { AASWorkspace } from 'common'; +import { getToken, guestPayload } from '../assets/json-web-token.js'; +import { RegisterRoutes } from '../../app/routes/routes.js'; +import { Authentication } from '../../app/controller/authentication.js'; +import { errorHandler } from '../assets/error-handler.js'; + +describe('WorkspacesController', function () { + let app: Express; + let logger: Logger; + let auth: jest.Mocked; + let aasProvider: jest.Mocked; + let variable: jest.Mocked; + let authentication: jest.Mocked; + + beforeEach(function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + variable = createSpyObj({}, { JWT_SECRET: 'SecretSecretSecretSecretSecretSecret' }); + auth = createSpyObj( + [ + 'hasUserAsync', + 'loginAsync', + 'getCookieAsync', + 'getCookiesAsync', + 'setCookieAsync', + 'deleteCookieAsync' + ]); + + aasProvider = createSpyObj( + [ + 'getWorkspaces', + ]); + + authentication = createSpyObj(['checkAsync']); + authentication.checkAsync.mockResolvedValue(guestPayload); + + container.registerInstance(AuthService, auth); + container.registerInstance('Logger', logger); + container.registerInstance(Variable, variable); + container.registerInstance(AASProvider, aasProvider); + container.registerInstance(Authentication, authentication); + + app = express(); + app.use(json()); + app.use(urlencoded({ extended: true })); + app.use(morgan('dev')); + app.set('trust proxy', 1); + + RegisterRoutes(app); + app.use(errorHandler); + }); + + it('getWorkspaces: /api/v1/workspaces', async function () { + const workspace: AASWorkspace = { + name: 'Test', + containers: [] + }; + + aasProvider.getWorkspaces.mockReturnValue([workspace]); + const response = await request(app) + .get('/api/v1/workspaces') + .set('Authorization', `Bearer ${getToken()}`); + + expect(response.statusCode).toBe(200); + expect(response.body).toEqual([workspace]); + expect(aasProvider.getWorkspaces).toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/image-processing.spec.ts b/projects/aas-server/src/test/image-processing.spec.ts new file mode 100644 index 00000000..80faac98 --- /dev/null +++ b/projects/aas-server/src/test/image-processing.spec.ts @@ -0,0 +1,26 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import fs from 'fs'; +import Jimp from 'jimp'; +import { ImageProcessing } from '../app/image-processing.js'; +import { describe, it, expect } from '@jest/globals'; + +describe.skip('image processing', function () { + it('resizes an image to 128 x 128 pixels', async function () { + const source = fs.createReadStream('./src/test/assets/thumbnail.jpg'); + const stream = await ImageProcessing.resizeAsync(source, 128, 128); + expect(stream).toBeTruthy(); + }); + + it('converts a tiff-image to a png-image', async function() { + const source = fs.createReadStream('./src/test/assets/image.tiff'); + const stream = await ImageProcessing.convertAsync(source, Jimp.MIME_PNG); + expect(stream).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/live/http/http-subscription.spec.ts b/projects/aas-server/src/test/live/http/http-subscription.spec.ts new file mode 100644 index 00000000..71af53db --- /dev/null +++ b/projects/aas-server/src/test/live/http/http-subscription.spec.ts @@ -0,0 +1,70 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import { Logger } from '../../../app/logging/logger.js'; +import { HttpSubscription } from '../../../app/live/http/http-subscription.js'; +import { SocketClient } from '../../../app/live/socket-client.js'; +import { aas, LiveRequest } from 'common'; +import { AasxServer } from '../../../app/packages/aasx-server/aasx-server.js'; +import env from '../../assets/aas-environment.js'; +import { createSpyObj, DoneFn } from '../../utils.js'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; + +describe('HttpSubscription', function () { + let aasxServer: jest.Mocked; + let logger: jest.Mocked; + let client: jest.Mocked; + let subscription: HttpSubscription; + + beforeEach(function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + client = createSpyObj(['has', 'subscribe', 'notify']); + aasxServer = createSpyObj( + ['getShellsAsync', 'commitAsync', 'openFileAsync', 'readValueAsync', 'resolveNodeId']); + + const reference: aas.Reference = { + type: 'ModelReference', + keys: [{ + type: 'Submodel', + value: 'http://i40.customer.com/type/1/1/F13E8576F6488342' + }, + { + type: 'Property', + value: 'GLN', + }] + }; + + const request: LiveRequest = { + type: 'file', + url: 'file://doc', + id: 'http://customer.com/aas/9175_7013_7091_9168', + nodes: [{ + nodeId: JSON.stringify(reference), + valueType: 'xs:integer' + }], + }; + + subscription = new HttpSubscription(logger, aasxServer, client, request, env); + }); + + it('should be created', function () { + expect(subscription).toBeTruthy(); + }); + + it('open/close subscription', function (done: DoneFn) { + jest.useFakeTimers(); + aasxServer.readValueAsync.mockReturnValue(new Promise(result => { + expect(true).toBeTruthy(); + result(42); + subscription.close(); + done(); + })); + + subscription.open(); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/live/http/opcua-socket-item.spec.ts b/projects/aas-server/src/test/live/http/opcua-socket-item.spec.ts new file mode 100644 index 00000000..7ec7f11f --- /dev/null +++ b/projects/aas-server/src/test/live/http/opcua-socket-item.spec.ts @@ -0,0 +1,31 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; +import { Logger } from '../../../app/logging/logger.js'; +import { createSpyObj } from '../../utils.js'; +import { SocketClient } from '../../../app/live/socket-client.js'; +import { HttpSocketItem } from '../../../app/live/http/http-socket-item.js'; + +describe('HttpSocketItem', function () { + let item: HttpSocketItem; + let logger: jest.Mocked; + let client: jest.Mocked; + + beforeEach(function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + client = createSpyObj([]); + item = new HttpSocketItem({ nodeId: '', valueType: 'xs:integer' }, 'http://localhost:1234'); + }); + + it('should be created', function() { + expect(item).toBeTruthy(); + expect(item.url).toEqual('http://localhost:1234'); + expect(item.node).toEqual({ nodeId: '', valueType: 'xs:integer' }); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/live/opcua/opcua-socket-item.spec.ts b/projects/aas-server/src/test/live/opcua/opcua-socket-item.spec.ts new file mode 100644 index 00000000..d04b4ead --- /dev/null +++ b/projects/aas-server/src/test/live/opcua/opcua-socket-item.spec.ts @@ -0,0 +1,40 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; +import { OpcuaSocketItem } from '../../../app/live/opcua/opcua-socket-item.js'; +import { Logger } from '../../../app/logging/logger.js'; +import { createSpyObj } from '../../utils.js'; +import { SocketClient } from '../../../app/live/socket-client.js'; +import { ClientMonitoredItem } from 'node-opcua'; + +describe('OpcuaSocketItem', function () { + let item: OpcuaSocketItem; + let logger: jest.Mocked; + let client: jest.Mocked; + + beforeEach(function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + client = createSpyObj([]); + item = new OpcuaSocketItem(logger, client, { nodeId: '', valueType: 'xs:integer' }); + }); + + it('should be created', function() { + expect(item).toBeTruthy(); + }); + + it('can subscribe/unsubscribe', function() { + const monitoredItem = createSpyObj(['on', 'off', 'terminate']); + item.subscribe(monitoredItem); + expect(monitoredItem.on).toHaveBeenCalled(); + + item.unsubscribe(); + expect(monitoredItem.off).toHaveBeenCalled(); + expect(monitoredItem.terminate).toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/live/opcua/opcua-subscription.spec.ts b/projects/aas-server/src/test/live/opcua/opcua-subscription.spec.ts new file mode 100644 index 00000000..e2e89da6 --- /dev/null +++ b/projects/aas-server/src/test/live/opcua/opcua-subscription.spec.ts @@ -0,0 +1,35 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; +import { OpcuaSubscription } from '../../../app/live/opcua/opcua-subscription.js'; +import { createSpyObj } from '../../utils.js'; +import { Logger } from '../../../app/logging/logger.js'; +import { SocketClient } from '../../../app/live/socket-client.js'; +import { OpcuaServer } from '../../../app/packages/opcua/opcua-server.js'; + +describe('OpcuaSubscription', function () { + let subscription: OpcuaSubscription; + let logger: jest.Mocked; + let client: jest.Mocked; + let server: jest.Mocked; + + beforeEach(function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + client = createSpyObj(['has', 'subscribe', 'notify']); + server = createSpyObj(['getSession']); + subscription = new OpcuaSubscription(logger, client, server, [{ + nodeId: 'ns=1;i=42', + valueType: 'xs:integer' + }]); + }); + + it('should be created', function() { + expect(subscription).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/live/socket-client.spec.ts b/projects/aas-server/src/test/live/socket-client.spec.ts new file mode 100644 index 00000000..fb8919f2 --- /dev/null +++ b/projects/aas-server/src/test/live/socket-client.spec.ts @@ -0,0 +1,37 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; +import { WebSocket } from 'ws'; +import { SocketClient } from '../../app/live/socket-client.js'; +import { createSpyObj } from '../utils.js'; +import { SocketSubscription } from '../../app/live/socket-subscription.js'; + +describe('SocketClient', function () { + let client: SocketClient; + let ws: jest.Mocked; + + beforeEach(function () { + ws = createSpyObj(['on', 'send']); + client = new SocketClient(ws); + }); + + it('should be created', function () { + expect(client).toBeTruthy(); + }); + + it('can subscribe for notifications', function () { + expect(client.has('test')).toBeFalsy(); + const subscription = createSpyObj(['open', 'close']); + client.subscribe('test', subscription); + expect(client.has('test')).toBeTruthy(); + + client.notify({ type: 'test', data: 42 }); + expect(ws.send).toHaveBeenCalled(); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/memory-logger.spec.ts b/projects/aas-server/src/test/memory-logger.spec.ts new file mode 100644 index 00000000..805b7a7b --- /dev/null +++ b/projects/aas-server/src/test/memory-logger.spec.ts @@ -0,0 +1,128 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import 'reflect-metadata'; +import { MemoryLogger, MemoryLoggerLevel } from '../app/logging/memory-logger.js'; +import { DebugConsole } from '../app/logging/logger.js'; +import { createSpyObj } from './utils.js'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; + +describe('MemoryLogger', function () { + describe('log level Info', function () { + let logger: MemoryLogger; + + beforeEach(function () { + logger = new MemoryLogger( + MemoryLoggerLevel.Info, + createSpyObj(['debug'])); + }); + + it('should create', function () { + expect(logger).toBeTruthy(); + }); + + it('logs all message types', function () { + logger.start('test'); + logger.info('This is an info.'); + logger.warning('This is a warning.'); + logger.error('This is an error'); + logger.stop(); + expect(logger.getMessages().length).toEqual(3); + }); + + it('logs message objects', function () { + logger.start('test'); + logger.log({ type: 'Info', text: 'This is an info.', timestamp: Date.now() }); + logger.log({ type: 'Warning', text: 'This is a warning.', timestamp: Date.now() }); + logger.log({ type: 'Error', text: 'This is an error.', timestamp: Date.now() }); + logger.stop(); + expect(logger.getMessages().length).toEqual(3); + }); + + it('logs only different errors', function () { + logger.start('test'); + logger.error('This is a first error.'); + logger.error(new Error('This is a first error.')); + logger.error('This is a seconde error.'); + logger.stop(); + expect(logger.getMessages().length).toEqual(2); + }); + + it('logs only different warnings', function () { + logger.start('test'); + logger.warning('This is a first warning.'); + logger.warning('This is a first warning.'); + logger.warning('This is a seconde warning.'); + logger.stop(); + expect(logger.getMessages().length).toEqual(2); + }); + + it('logs only different info messages', function () { + logger.start('test'); + logger.info('This is a first info.'); + logger.info('This is a seconde info.'); + logger.info('This is a seconde info.'); + logger.stop(); + expect(logger.getMessages().length).toEqual(2); + }); + }); + + describe('log level Error', function () { + let logger: MemoryLogger; + + beforeEach(function () { + logger = new MemoryLogger(MemoryLoggerLevel.Error, createSpyObj(['debug'])); + }); + + it('logs ony errors', function () { + logger.start('test'); + logger.info('This is an info.'); + logger.warning('This is a warning.'); + logger.error('This is an error'); + logger.stop(); + expect(logger.getMessages().length).toEqual(1); + }); + }); + + describe('log level Warning', function () { + let logger: MemoryLogger; + + beforeEach(function () { + logger = new MemoryLogger(MemoryLoggerLevel.Warning, createSpyObj(['debug'])); + }); + + it('logs ony errors and warnings', function () { + logger.start('test'); + logger.info('This is an info.'); + logger.warning('This is a warning.'); + logger.error('This is an error'); + logger.stop(); + expect(logger.getMessages().length).toEqual(2); + }); + }); + + describe('log debug', function () { + let logger: MemoryLogger; + let console: jest.Mocked; + + beforeEach(function () { + console = createSpyObj(['debug']); + logger = new MemoryLogger(MemoryLoggerLevel.All, console); + }); + + it('logs debug messages to a console', function () { + logger.debug('This is a debug message.'); + expect(console.debug).toHaveBeenCalled(); + }); + + it('logs errors to a console', function () { + logger.debug(new Error('This is a debug message.')); + expect(console.debug).toHaveBeenCalled(); + }); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/packages/aasx-server/aasx-package.spec.ts b/projects/aas-server/src/test/packages/aasx-server/aasx-package.spec.ts new file mode 100644 index 00000000..709cee69 --- /dev/null +++ b/projects/aas-server/src/test/packages/aasx-server/aasx-package.spec.ts @@ -0,0 +1,55 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import { AasxPackage } from '../../../app/packages/aasx-directory/aasx-package.js' +import { AasxDirectory } from '../../../app/packages/aasx-directory/aasx-directory.js'; +import { Logger } from '../../../app/logging/logger.js'; +import { LocalFileStorage } from '../../../app/file-storage/local-file-storage.js'; +import { createSpyObj, fail } from '../../utils.js'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; +import { FileStorage } from '../../../app/file-storage/file-storage.js'; + +describe('AasxPackage', function () { + let logger: jest.Mocked; + let source: AasxDirectory; + let fileStorage: FileStorage; + + beforeEach(function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + fileStorage = new LocalFileStorage('./src/test/assets/samples'); + source = new AasxDirectory(logger, 'file:///samples', fileStorage); + }); + + describe('createDocumentAsync', function () { + it('creates a document from a xml origin', async function () { + try { + await source.openAsync(); + const aasxPackage = new AasxPackage(logger, source, 'xml-origin.aasx'); + const document = aasxPackage.createDocumentAsync(); + expect(document).toBeDefined(); + } catch (error) { + fail(error?.message) + } finally { + await source.closeAsync(); + } + }); + + it('creates a document from a json origin', async function () { + try { + await source.openAsync(); + const aasxPackage = new AasxPackage(logger, source, 'json-origin.aasx'); + const document = aasxPackage.createDocumentAsync(); + expect(document).toBeDefined(); + } catch (error) { + fail(error?.message); + } finally { + await source.closeAsync(); + } + }); + }); +}); diff --git a/projects/aas-server/src/test/packages/aasx-server/aasx-server-package.spec.ts b/projects/aas-server/src/test/packages/aasx-server/aasx-server-package.spec.ts new file mode 100644 index 00000000..b0022895 --- /dev/null +++ b/projects/aas-server/src/test/packages/aasx-server/aasx-server-package.spec.ts @@ -0,0 +1,46 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import { aas } from 'common'; +import { Logger } from '../../../app/logging/logger.js'; +import { AasxServer } from '../../../app/packages/aasx-server/aasx-server.js'; +import { AasxServerPackage } from '../../../app/packages/aasx-server/aasx-server-package.js'; +import { createSpyObj } from '../../utils.js'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; + +describe('AasxServerPackage', function () { + let aasPackage: AasxServerPackage; + let logger: jest.Mocked; + let server: jest.Mocked; + let env: aas.Environment; + + beforeEach(function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + server = createSpyObj(['readEnvironmentAsync'], { + url: new URL('http:/localhost:1234'), + baseUrl: new URL('http:/localhost:1234') + }); + + aasPackage = new AasxServerPackage(logger, server, 'CunaCup_Becher1'); + env = { + assetAdministrationShells: [{ + id: 'Sample AAS', + idShort: 'http://www.fraunhofer.de/aas', + modelType: 'AssetAdministrationShell', + assetInformation: { assetKind: 'Instance' } + }], submodels: [], conceptDescriptions: [] + }; + }); + + it('creates a document', async function () { + server.readEnvironmentAsync.mockReturnValue( + new Promise((resolve, _) => resolve(env))); + + await expect(aasPackage.createDocumentAsync()).resolves.toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/packages/aasx-server/aasx-server-v0.spec.ts b/projects/aas-server/src/test/packages/aasx-server/aasx-server-v0.spec.ts new file mode 100644 index 00000000..75f00da0 --- /dev/null +++ b/projects/aas-server/src/test/packages/aasx-server/aasx-server-v0.spec.ts @@ -0,0 +1,138 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import http, { IncomingMessage } from 'http'; +import { Socket } from 'net'; +import { AasxServer } from '../../../app/packages/aasx-server/aasx-server.js'; +import listaas from '../../assets/test-aas/listaas.js'; +import becher1 from '../../assets/test-aas/cuna-cup-becher1.js'; +import submodels from '../../assets/test-aas/submodels.js'; +import nameplate from '../../assets/test-aas/nameplate-becher1.js'; +import digitalProductPassport from '../../assets/test-aas/digital-product-passport-becher1.js'; +import customerFeedback from '../../assets/test-aas/customer-feedback-becher1.js'; +import { AasxServerV0 } from '../../../app/packages/aasx-server/aasx-server-v0.js'; +import { aas, selectElement } from 'common'; +import { Logger } from '../../../app/logging/logger.js'; +import aasEnvironment from '../../assets/aas-environment.js'; +import { createSpyObj } from '../../utils.js'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; + +describe('AasxServerV0', function () { + let logger: jest.Mocked; + let server: AasxServer; + + beforeEach(function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + server = new AasxServerV0(logger, 'http://localhost:1234'); + }); + + it('returns the AAS list', async function () { + jest.spyOn(http, 'request').mockImplementation((options, callback) => { + const stream = new IncomingMessage(new Socket()); + stream.push(JSON.stringify(listaas)); + stream.push(null); + stream.statusCode = 200, + stream.statusMessage = 'OK', + (callback as (res: IncomingMessage) => void)(stream); + return new http.ClientRequest('http://localhost:1234'); + }); + + await expect(server.getShellsAsync()).resolves.toEqual([ + 'AssistanceSystem_Dte', + 'CunaCup_Becher1', + 'CunaCup_Becher2', + 'DTOrchestrator']); + }); + + it('gets the AAS with the specified idShort', async function () { + jest.spyOn(http, 'request').mockImplementation((options, callback) => { + let value: any; + switch ((options as http.RequestOptions).path) { + case '/aas/CunaCup_Becher1/submodels': + value = submodels; + break; + case '/aas/CunaCup_Becher1/submodels/Nameplate_Becher1/complete': + value = nameplate; + break; + case '/aas/CunaCup_Becher1/submodels/DigitalProductPassport_Becher1/complete': + value = digitalProductPassport; + break; + case '/aas/CunaCup_Becher1/submodels/CustomerFeedback_Becher1/complete': + value = customerFeedback; + break; + default: + value = becher1; + break; + } + + const stream = new IncomingMessage(new Socket()); + stream.push(JSON.stringify(value)); + stream.push(null); + stream.statusCode = 200, + stream.statusMessage = 'OK', + (callback as (res: IncomingMessage) => void)(stream); + + return new http.ClientRequest('http://localhost:1234'); + }); + + await expect(server.readEnvironmentAsync('CunaCup_Becher1')).resolves.toBeTruthy(); + }); + + it('can open a file', async function () { + jest.spyOn(http, 'request').mockImplementation((options, callback) => { + const stream = new IncomingMessage(new Socket()); + stream.push(JSON.stringify({ + "aaslist": [ + "0 : ExampleMotor : [IRI] http://customer.com/aas/9175_7013_7091_9168 : ", + ] + })); + stream.push(null); + stream.statusCode = 200, + stream.statusMessage = 'OK', + (callback as (res: IncomingMessage) => void)(stream); + + return new http.ClientRequest('http://localhost:1234'); + }); + + await expect( + server.openFileAsync(aasEnvironment.assetAdministrationShells[0], + selectElement(aasEnvironment, 'Documentation', 'OperatingManual', 'DigitalFile_PDF')!)).resolves.toBeTruthy(); + }); + + it('reads the current value of a data element', async function () { + jest.spyOn(http, 'request').mockImplementation((options, callback) => { + const stream = new IncomingMessage(new Socket()); + stream.push(JSON.stringify({ value: 42 })); + stream.push(null); + stream.statusCode = 200, + stream.statusMessage = 'OK', + (callback as (res: IncomingMessage) => void)(stream); + + return new http.ClientRequest('http://localhost:1234'); + }); + + await expect(server.readValueAsync('http://localhost:1234', 'xs:int')).resolves.toBe(42); + }); + + describe('resolveNodeId', function () { + let shell: jest.Mocked; + + beforeEach(function () { + shell = createSpyObj( + {}, + { idShort: 'aas1' }); + }); + + it('returns the URL to "property1"', function () { + const smId = Buffer.from('http://localhost/test/submodel1').toString('base64'); + const nodeId = smId + '.submodel1/property1'; + expect(server.resolveNodeId(shell, nodeId)).toEqual( + `http://localhost:1234/aas/aas1/submodels/submodel1/elements/property1/value`); + }); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/packages/aasx-server/aasx-server-v3.spec.ts b/projects/aas-server/src/test/packages/aasx-server/aasx-server-v3.spec.ts new file mode 100644 index 00000000..38a8713d --- /dev/null +++ b/projects/aas-server/src/test/packages/aasx-server/aasx-server-v3.spec.ts @@ -0,0 +1,256 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import http, { IncomingMessage } from 'http'; +import env from '../../assets/aas-environment.js'; +import { AasxServerV3, OperationResult } from '../../../app/packages/aasx-server/aasx-server-v3.js'; +import { aas, DifferenceItem } from 'common'; +import { cloneDeep } from 'lodash-es'; +import { Socket } from 'net'; +import { Logger } from '../../../app/logging/logger.js'; +import { createSpyObj } from '../../utils.js'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; + +describe.skip('AasxServerV3', function () { + let logger: Logger; + let server: AasxServerV3; + + beforeEach(function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + server = new AasxServerV3(logger, 'http://localhost:1234'); + }); + + describe('resolveNodeId', function () { + let shell: jest.Mocked; + + beforeEach(function () { + shell = createSpyObj( + {}, + { id: 'http://localhost/test/aas' }); + }); + + it('returns the URL to "property1"', function () { + const aasId = Buffer.from('http://localhost/test/aas').toString('base64url'); + const smId = Buffer.from('http://localhost/test/submodel1').toString('base64url'); + const nodeId = smId + '.submodel1/property1'; + expect(server.resolveNodeId(shell, nodeId)).toEqual( + `http://localhost:1234/shells/${aasId}/aas/submodels/${smId}/submodel/submodel-elements/property1`); + }); + }); + + describe('commitAsync', function () { + let source: aas.Environment; + let destination: aas.Environment; + + beforeEach(async function () { + source = env; + destination = cloneDeep(source); + }); + + it('inserts a submodel', async function () { + jest.spyOn(http, 'request').mockImplementation((options, callback) => { + const stream = new IncomingMessage(new Socket()); + stream.push(JSON.stringify('Submodel inserted.')); + stream.push(null); + (callback as (res: IncomingMessage) => void)(stream); + stream.statusCode = 201; + stream.statusMessage = 'Created'; + return new http.ClientRequest('http://localhost:1234'); + }); + + const diffs: DifferenceItem[] = [{ + type: 'inserted', + sourceElement: source.submodels[0] + }]; + + await expect(server.commitAsync(source, destination, diffs)).resolves.toEqual(['Submodel inserted.']); + }); + + it('inserts a submodel-element', async function () { + jest.spyOn(http, 'request').mockImplementation((options, callback) => { + const stream = new IncomingMessage(new Socket()); + stream.push(JSON.stringify('SubmodelElement inserted.')); + stream.push(null); + (callback as (res: IncomingMessage) => void)(stream); + stream.statusCode = 201; + stream.statusMessage = 'Created'; + return new http.ClientRequest('http://localhost:1234'); + }); + + const diffs: DifferenceItem[] = [{ + type: 'inserted', + sourceParent: source.submodels[0], + sourceElement: source.submodels[0].submodelElements![0], + destinationParent: destination.submodels[0] + }]; + + await expect(server.commitAsync(source, destination, diffs)).resolves.toEqual(['SubmodelElement inserted.']); + }); + + it('updates a submodel', async function () { + jest.spyOn(http, 'request').mockImplementation((options, callback) => { + const stream = new IncomingMessage(new Socket()); + stream.push(JSON.stringify('Submodel updated.')); + stream.push(null); + (callback as (res: IncomingMessage) => void)(stream); + stream.statusCode = 200; + stream.statusMessage = 'OK'; + return new http.ClientRequest('http://localhost:1234'); + }); + + const diffs: DifferenceItem[] = [{ + type: 'changed', + sourceElement: source.submodels[0], + destinationElement: destination.submodels[0] + }]; + + await expect(server.commitAsync(source, destination, diffs)).resolves.toEqual(['Submodel updated.']); + }); + + it('updates a submodel-element', async function () { + jest.spyOn(http, 'request').mockImplementation((options, callback) => { + const stream = new IncomingMessage(new Socket()); + stream.push(JSON.stringify('SubmodelElement updated.')); + stream.push(null); + (callback as (res: IncomingMessage) => void)(stream); + stream.statusCode = 200; + stream.statusMessage = 'OK'; + return new http.ClientRequest('http://localhost:1234'); + }); + + const diffs: DifferenceItem[] = [{ + type: 'changed', + sourceParent: source.submodels[0], + sourceElement: source.submodels[0].submodelElements![0], + destinationParent: destination.submodels[0], + destinationElement: destination.submodels[0].submodelElements![0] + }]; + + await expect(server.commitAsync(source, destination, diffs)).resolves.toEqual(['SubmodelElement updated.']); + }); + + it('deletes a submodel', async function () { + jest.spyOn(http, 'request').mockImplementation((options, callback) => { + const stream = new IncomingMessage(new Socket()); + stream.push(JSON.stringify('Submodel deleted.')); + stream.push(null); + (callback as (res: IncomingMessage) => void)(stream); + stream.statusCode = 204; + stream.statusMessage = 'No Content'; + return new http.ClientRequest('http://localhost:1234'); + }); + + const diffs: DifferenceItem[] = [{ + type: 'deleted', + destinationElement: destination.submodels[0] + }]; + + await expect(server.commitAsync(source, destination, diffs)).resolves.toEqual(['Submodel deleted.']); + }); + + it('deletes a submodel-element', async function () { + jest.spyOn(http, 'request').mockImplementation((options, callback) => { + const stream = new IncomingMessage(new Socket()); + stream.push(JSON.stringify('SubmodelElement deleted.')); + stream.push(null); + (callback as (res: IncomingMessage) => void)(stream); + stream.statusCode = 204; + stream.statusMessage = 'No Content'; + return new http.ClientRequest('http://localhost:1234'); + }); + + const diffs: DifferenceItem[] = [{ + type: 'deleted', + destinationParent: destination.submodels[0], + destinationElement: destination.submodels[0].submodelElements![0] + }]; + + await expect(server.commitAsync(source, destination, diffs)).resolves.toEqual(['SubmodelElement deleted.']); + }); + }); + + describe('invoke', function () { + it('invokes an operation synchronously', async function () { + const result: OperationResult = { + executionState: 'Completed', + success: true + }; + + jest.spyOn(http, 'request').mockImplementation((options, callback) => { + const stream = new IncomingMessage(new Socket()); + stream.push(JSON.stringify(result)); + stream.push(null); + (callback as (res: IncomingMessage) => void)(stream); + stream.statusCode = 204; + stream.statusMessage = 'No Content'; + return new http.ClientRequest('http://localhost:1234'); + }); + + const operation: aas.Operation = { + idShort: 'noop', + modelType: 'Operation', + parent: { + type: 'ModelReference', + keys: [{ type: 'Submodel', value: 'http://i40.customer.com/type/1/1/F13E8576F6488342' }] + } + } + + await expect(server.invoke(env, operation)).resolves.toEqual(operation); + }); + + it('throws an error if the server returns with status code 500', async function () { + jest.spyOn(http, 'request').mockImplementation((options, callback) => { + const stream = new IncomingMessage(new Socket()); + (callback as (res: IncomingMessage) => void)(stream); + stream.statusCode = 500; + stream.statusMessage = 'Internal server error.'; + return new http.ClientRequest('http://localhost:1234'); + }); + + const operation: aas.Operation = { + idShort: 'noop', + modelType: 'Operation', + parent: { + type: 'ModelReference', + keys: [{ type: 'Submodel', value: 'http://i40.customer.com/type/1/1/F13E8576F6488342' }] + } + } + + await expect(server.invoke(env, operation)).rejects.toThrowError(); + }); + + it('throws an error if the operation fails', async function () { + const result: OperationResult = { + messages: [{messageType: 'Error', text: 'Operation failed.'}], + executionState: 'Failed', + success: false + }; + + jest.spyOn(http, 'request').mockImplementation((options, callback) => { + const stream = new IncomingMessage(new Socket()); + stream.push(JSON.stringify(result)); + stream.push(null); + (callback as (res: IncomingMessage) => void)(stream); + stream.statusCode = 204; + stream.statusMessage = 'No Content'; + return new http.ClientRequest('http://localhost:1234'); + }); + + const operation: aas.Operation = { + idShort: 'noop', + modelType: 'Operation', + parent: { + type: 'ModelReference', + keys: [{ type: 'Submodel', value: 'http://i40.customer.com/type/1/1/F13E8576F6488342' }] + } + } + + await expect(server.invoke(env, operation)).rejects.toThrowError(); + }); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/packages/json-reader-v2.spec.ts b/projects/aas-server/src/test/packages/json-reader-v2.spec.ts new file mode 100644 index 00000000..b34fa580 --- /dev/null +++ b/projects/aas-server/src/test/packages/json-reader-v2.spec.ts @@ -0,0 +1,34 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import { Logger } from '../../app/logging/logger.js'; +import { readFile } from 'fs/promises'; +import { JsonReaderV2 } from '../../app/packages/json-reader-v2.js' +import { createSpyObj } from '../utils.js'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; + +describe('JsonReaderV2', function () { + let reader: JsonReaderV2; + let logger: jest.Mocked; + let json: string; + + beforeEach(async function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + json = (await readFile('./src/test/assets/aas-example-v2.json')).toString(); + reader = new JsonReaderV2(logger, json); + }); + + it('should be created', function () { + expect(reader).toBeTruthy(); + }); + + it('reads the AAS environment from a JSON source', function () { + let env = reader.readEnvironment(); + expect(env).toBeDefined(); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/packages/json-reader.spec.ts b/projects/aas-server/src/test/packages/json-reader.spec.ts new file mode 100644 index 00000000..8c01aee7 --- /dev/null +++ b/projects/aas-server/src/test/packages/json-reader.spec.ts @@ -0,0 +1,35 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import { Logger } from '../../app/logging/logger.js'; +import { readFile } from 'fs/promises'; +import { JsonReader } from '../../app/packages/json-reader.js' +import { resolve } from 'path'; +import { createSpyObj } from '../utils.js'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; + +describe('JsonReader', function () { + let reader: JsonReader; + let logger: jest.Mocked; + let json: string; + + beforeEach(async function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + json = (await readFile(resolve('./src/test/assets/aas-example.json'))).toString(); + reader = new JsonReader(logger, json); + }); + + it('should be created', function () { + expect(reader).toBeTruthy(); + }); + + it('reads the AAS environment from a JSON source', function () { + let env = reader.readEnvironment(); + expect(env).toBeDefined(); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/packages/json-writer-v2.spec.ts b/projects/aas-server/src/test/packages/json-writer-v2.spec.ts new file mode 100644 index 00000000..47ddc2ae --- /dev/null +++ b/projects/aas-server/src/test/packages/json-writer-v2.spec.ts @@ -0,0 +1,41 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import { JsonWriterV2 } from '../../app/packages/json-writer-v2.js'; +import env from '../assets/aas-environment.js'; +import { describe, beforeEach, it, expect } from '@jest/globals'; + +describe('JsonWriterV2', function () { + describe('writeEnvironment', function () { + let writer: JsonWriterV2; + + beforeEach(function(){ + writer = new JsonWriterV2(); + }); + + it('is not implemented', function() { + expect(() => writer.writeEnvironment(env)).toThrowError(); + }) + }); + + describe('write', function() { + let writer: JsonWriterV2; + + beforeEach(function(){ + writer = new JsonWriterV2(); + }); + + it('does not support writing an AAS', function() { + expect(() => writer.write(env.assetAdministrationShells[0])).toThrowError(); + }); + + it('writes a submodel', function() { + expect(writer.write(env.submodels[0])).toBeDefined(); + }) + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/packages/json-writer.spec.ts b/projects/aas-server/src/test/packages/json-writer.spec.ts new file mode 100644 index 00000000..6d812464 --- /dev/null +++ b/projects/aas-server/src/test/packages/json-writer.spec.ts @@ -0,0 +1,37 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import { JsonWriter } from '../../app/packages/json-writer.js'; +import env from '../assets/aas-environment.js'; +import { describe, beforeEach, it, expect } from '@jest/globals'; + +describe('JsonWriter', function () { + describe('writeEnvironment', function () { + let writer: JsonWriter; + + beforeEach(function(){ + writer = new JsonWriter(); + }); + + it('writes an AAS environment', function() { + expect(writer.writeEnvironment(env)).toBeDefined(); + }) + }); + + describe('write', function() { + let writer: JsonWriter; + + beforeEach(function(){ + writer = new JsonWriter(); + }); + + it('does not support writing an AAS', function() { + expect(() => writer.write(env.assetAdministrationShells[0])).toThrowError(); + }); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/packages/opcua/opcua-package.spec.ts b/projects/aas-server/src/test/packages/opcua/opcua-package.spec.ts new file mode 100644 index 00000000..e3e11746 --- /dev/null +++ b/projects/aas-server/src/test/packages/opcua/opcua-package.spec.ts @@ -0,0 +1,29 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import { beforeEach, describe, expect, it, jest } from "@jest/globals"; +import { OpcuaPackage } from "../../../app/packages/opcua/opcua-package.js"; +import { Logger } from "../../../app/logging/logger.js"; +import { createSpyObj } from "../../utils.js"; +import { OpcuaServer } from "../../../app/packages/opcua/opcua-server.js"; + +describe('OpcuaPackage', function () { + let aasPackage: OpcuaPackage; + let logger: jest.Mocked; + let server: jest.Mocked; + + beforeEach(function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + server = createSpyObj(['openAsync', 'closeAsync', 'getSession'], { isOpen: true }); + aasPackage = new OpcuaPackage(logger, server, 'ns=1;i=42'); + }); + + it('should be created', function () { + expect(aasPackage).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/packages/opcua/opcua-reader.spec.ts b/projects/aas-server/src/test/packages/opcua/opcua-reader.spec.ts new file mode 100644 index 00000000..903a405e --- /dev/null +++ b/projects/aas-server/src/test/packages/opcua/opcua-reader.spec.ts @@ -0,0 +1,32 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import { Logger } from "../../../app/logging/logger.js"; +import { OpcuaReader } from '../../../app/packages/opcua/opcua-reader.js'; +import { OPCUAComponent } from "../../../app/packages/opcua/opcua.js"; +import { OpcuaDataTypeDictionary } from "../../../app/packages/opcua/opcua-data-type-dictionary.js"; +import { createSpyObj } from '../../utils.js'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; + +describe('OPCUAReader', function () { + let reader: OpcuaReader; + let logger: jest.Mocked; + let origin: jest.Mocked; + let dataTypes: jest.Mocked; + + beforeEach(function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + origin = createSpyObj({}, ['displayName', 'hasProperty', 'nodeClass']); + dataTypes = createSpyObj(['get']); + reader = new OpcuaReader(logger, origin, dataTypes); + }); + + it('should be created', function () { + expect(reader).toBeTruthy(); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/packages/opcua/opcua-server.spec.ts b/projects/aas-server/src/test/packages/opcua/opcua-server.spec.ts new file mode 100644 index 00000000..805446a7 --- /dev/null +++ b/projects/aas-server/src/test/packages/opcua/opcua-server.spec.ts @@ -0,0 +1,225 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import { beforeEach, describe, expect, it, jest } from '@jest/globals'; +import { OpcuaServer } from '../../../app/packages/opcua/opcua-server.js'; +import { Logger } from '../../../app/logging/logger.js'; +import { createSpyObj } from '../../utils.js'; +import { CallMethodRequestLike, CallMethodResult, ClientSession, OPCUAClient, StatusCodes, Variant } from 'node-opcua'; +import { SocketClient } from '../../../app/live/socket-client.js'; +import { LiveRequest, aas } from 'common'; +import env from '../../assets/aas-environment.js'; + +type CallMethod = (methodToCall: CallMethodRequestLike) => Promise; + +describe('OpcuaServer', function () { + let server: OpcuaServer; + let logger: jest.Mocked; + + beforeEach(function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + server = new OpcuaServer(logger, 'opc.tcp://localhost:1234/I4AASServer'); + }); + + it('should be created', function () { + expect(server).toBeTruthy(); + }); + + describe('testAsync', function () { + let client: jest.Mocked; + let session: jest.Mocked; + + beforeEach(function () { + client = createSpyObj(['connect', 'createSession', 'closeSession', 'disconnect']); + session = createSpyObj([]); + }); + + it('returns for a valid URL to an OPC-UA server', async function () { + client.connect.mockImplementation(() => new Promise(resolve => resolve())); + client.createSession.mockImplementation(() => new Promise(resolve => resolve(session))); + jest.spyOn(OPCUAClient, 'create').mockReturnValue(client); + await expect(server.testAsync()).resolves.toBeUndefined(); + }); + + it('throws an Error for an invalid URL', async function () { + client.connect.mockImplementation(() => new Promise((_, reject) => reject(new Error('Connection failed.')))); + client.createSession.mockImplementation(() => new Promise(resolve => resolve(session))); + jest.spyOn(OPCUAClient, 'create').mockReturnValue(client); + await expect(server.testAsync()).rejects.toThrowError(); + }); + }); + + describe('openAsync/closeAsync', function () { + let client: jest.Mocked; + let session: jest.Mocked; + + beforeEach(function () { + client = createSpyObj(['connect', 'createSession', 'closeSession', 'disconnect']); + session = createSpyObj([]); + }); + + it('can open/close a connection to an OPC-UA server', async function () { + client.connect.mockImplementation(() => new Promise(resolve => resolve())); + client.createSession.mockImplementation(() => new Promise(resolve => resolve(session))); + jest.spyOn(OPCUAClient, 'create').mockReturnValue(client); + await expect(server.openAsync()).resolves.toBeUndefined(); + expect(server.isOpen).toBeTruthy(); + await expect(server.closeAsync()).resolves.toBeUndefined(); + expect(server.isOpen).toBeFalsy(); + }); + }); + + describe('getSession', function () { + let client: jest.Mocked; + let session: jest.Mocked; + + beforeEach(function () { + client = createSpyObj(['connect', 'createSession', 'closeSession', 'disconnect']); + session = createSpyObj([]); + client.connect.mockImplementation(() => new Promise(resolve => resolve())); + client.createSession.mockImplementation(() => new Promise(resolve => resolve(session))); + jest.spyOn(OPCUAClient, 'create').mockReturnValue(client); + }); + + it('returns the current session', async function () { + await server.openAsync(); + expect(server.getSession()).toBe(session); + await server.closeAsync(); + }); + + it('throws an Error if no connection is established', function () { + expect(() => server.getSession()).toThrowError(); + }); + }); + + describe('createPackage', function () { + it('creates a new OpcuaPackage instance', function () { + expect(server.createPackage('ns=1;i=42')).toBeTruthy(); + }); + }); + + describe('createSubscription', function () { + it('creates a new OpcuaSubscription instance', function () { + const request: LiveRequest = { + type: 'opc', + id: 'opc.tcp://localhost:1234/I4AASServer', + url: 'http://customer.com/aas/9175_7013_7091_9168', + nodes: [{ + nodeId: 'ns=1;i=4711', + valueType: 'xs:integer' + }] + }; + + expect(server.createSubscription(createSpyObj({}), request)).toBeTruthy(); + }); + }); + + describe('getPackageAsync', function () { + it('is not implemented', function () { + expect(() => server.getPackageAsync('aasId', 'name')).toThrowError(); + }); + }); + + describe('postPackageAsync', function () { + it('is not implemented', function () { + expect(() => server.postPackageAsync(createSpyObj({}))).toThrowError(); + }); + }); + + describe('deletePackageAsync', function () { + it('is not implemented', function () { + expect(() => server.getPackageAsync('aasId', 'name')).toThrowError(); + }); + }); + + describe('invoke', function () { + let client: jest.Mocked; + let session: jest.Mocked; + + beforeEach(function () { + client = createSpyObj(['connect', 'createSession', 'closeSession', 'disconnect']); + session = createSpyObj(['call']); + client.connect.mockImplementation(() => new Promise(resolve => resolve())); + client.createSession.mockImplementation(() => new Promise(resolve => resolve(session))); + jest.spyOn(OPCUAClient, 'create').mockReturnValue(client); + }); + + it('invokes an operation', async function () { + const result = createSpyObj([], { + statusCode: StatusCodes.Good, + outputArguments: [ + { value: '3' } as Variant + ] + }); + + const call: CallMethod = () => { + return new Promise(resolve => resolve(result)); + }; + + session.call.mockImplementation(call as any); + const operation: aas.Operation = { + idShort: 'add', + modelType: 'Operation', + methodId: 'ns=1;i=4711', + objectId: 'ns=1;i=0815', + inputVariables: [ + { value: { idShort: 'a', modelType: 'Property', valueType: 'xs:int', value: '1' } as aas.Property }, + { value: { idShort: 'b', modelType: 'Property', valueType: 'xs:int', value: '2' } as aas.Property } + ], + outputVariables: [ + { value: { idShort: 'sum', modelType: 'Property', valueType: 'xs:int', value: '3' } as aas.Property }, + ], + parent: { + type: 'ModelReference', + keys: [{ type: 'Submodel', value: 'http://i40.customer.com/type/1/1/F13E8576F6488342' }] + } + }; + + await server.openAsync(); + await expect(server.invoke(env, operation)).resolves.toEqual(operation); + await server.closeAsync(); + }); + + + it('throw an Error if the call result is not "Good"', async function () { + const result = createSpyObj([], { + statusCode: StatusCodes.Bad, + outputArguments: [ + { value: '3' } as Variant + ] + }); + + const call: CallMethod = () => { + return new Promise(resolve => resolve(result)); + }; + + session.call.mockImplementation(call as any); + const operation: aas.Operation = { + idShort: 'add', + modelType: 'Operation', + methodId: 'ns=1;i=4711', + objectId: 'ns=1;i=0815', + inputVariables: [ + { value: { idShort: 'a', modelType: 'Property', valueType: 'xs:int', value: '1' } as aas.Property }, + { value: { idShort: 'b', modelType: 'Property', valueType: 'xs:int', value: '2' } as aas.Property } + ], + outputVariables: [ + { value: { idShort: 'sum', modelType: 'Property', valueType: 'xs:int', value: '3' } as aas.Property }, + ], + parent: { + type: 'ModelReference', + keys: [{ type: 'Submodel', value: 'http://i40.customer.com/type/1/1/F13E8576F6488342' }] + } + }; + + await server.openAsync(); + await expect(server.invoke(env, operation)).rejects.toThrowError(); + await server.closeAsync(); + }); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/packages/server-message.spec.ts b/projects/aas-server/src/test/packages/server-message.spec.ts new file mode 100644 index 00000000..dcea9a59 --- /dev/null +++ b/projects/aas-server/src/test/packages/server-message.spec.ts @@ -0,0 +1,152 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import net from 'net'; +import http, { IncomingMessage } from 'http'; +import { Socket } from 'net'; +import { ServerMessage } from "../../app/packages/server-message.js"; +import { createSpyObj } from '../utils.js'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; + +describe('ServerMessage', function () { + let server: ServerMessage; + + beforeEach(function () { + server = new ServerMessage(); + }); + + it('should created', function () { + expect(server).toBeTruthy(); + }); + + describe('get', function () { + beforeEach(function () { + jest.spyOn(http, 'request').mockImplementation((_, callback) => { + const stream = new IncomingMessage(new Socket()); + stream.push(JSON.stringify('Hello World!')); + stream.push(null); + stream.statusCode = 200, + stream.statusMessage = 'OK', + (callback as (res: IncomingMessage) => void)(stream); + return new http.ClientRequest('http://localhost:1234/hello/world'); + }); + }); + + it('gets an object from a server', async function () { + await expect(server.get(new URL('http://localhost:1234/hello/world'))) + .resolves.toBe('Hello World!'); + }); + }); + + describe('getResponse', function () { + beforeEach(function () { + jest.spyOn(http, 'request').mockImplementation((_, callback) => { + const stream = new IncomingMessage(new Socket()); + stream.push(JSON.stringify('Hello World!')); + stream.push(null); + (callback as (res: IncomingMessage) => void)(stream); + return new http.ClientRequest('http://localhost:1234/hello/world'); + }); + }); + + it('gets the message response', async function () { + await expect(server.getResponse(new URL('http://localhost:1234/hello/world'))).resolves.toBeTruthy(); + }) + }); + + describe('put', function () { + beforeEach(function () { + jest.spyOn(http, 'request').mockImplementation((_, callback) => { + const stream = new IncomingMessage(new Socket()); + stream.push(JSON.stringify('OK')); + stream.push(null); + (callback as (res: IncomingMessage) => void)(stream); + stream.statusCode = 200; + stream.statusMessage = 'OK'; + return new http.ClientRequest('http://localhost:1234/hello/world'); + }); + }); + + it('updates an object on a server', async function () { + await expect(server.put(new URL('http://localhost:1234/hello/world'), { text: 'Hello World!' })) + .resolves.toEqual('OK'); + }); + }); + + describe('post', function () { + beforeEach(function () { + jest.spyOn(http, 'request').mockImplementation((_, callback) => { + const stream = new IncomingMessage(new Socket()); + stream.push(JSON.stringify('Created')); + stream.push(null); + (callback as (res: IncomingMessage) => void)(stream); + stream.statusCode = 201; + stream.statusMessage = 'Created'; + return new http.ClientRequest('http://localhost:1234/hello/world'); + }); + }); + + it('updates an object on a server', async function () { + await expect(server.post(new URL('http://localhost:1234/hello/world'), 'Hello World!')) + .resolves.toEqual('Created'); + }); + }); + + describe('delete', function () { + beforeEach(function () { + jest.spyOn(http, 'request').mockImplementation((_, callback) => { + const stream = new IncomingMessage(new Socket()); + stream.push(JSON.stringify('Deleted')); + stream.push(null); + (callback as (res: IncomingMessage) => void)(stream); + stream.statusCode = 204; + stream.statusMessage = 'No Content'; + return new http.ClientRequest('http://localhost:1234/hello/world'); + }); + }); + + it('updates an object on a server', async function () { + await expect(server.delete(new URL('http://localhost:1234/hello/world'))) + .resolves.toEqual('Deleted'); + }); + }); + + describe('checkUrlExist', function () { + let socket: jest.Mocked; + + beforeEach(function () { + socket = createSpyObj(['setTimeout', 'on', 'end', 'destroy']); + }); + + it('validates a connection', async function () { + socket.on.mockImplementation((event, listener) => { + if (event === 'connect') { + setTimeout(() => (listener as () => void)()); + } + + return socket; + }); + + jest.spyOn(net, 'createConnection').mockReturnValue(socket); + await expect(server.checkUrlExist('http://localhost:1234')).resolves.toBeUndefined(); + }); + + it('throws an error if a connection does not exist', async function () { + socket.on.mockImplementation((event, listener) => { + if (event === 'timeout') { + setTimeout(() => (listener as () => void)()); + } + + return socket; + }); + + jest.spyOn(net, 'createConnection').mockReturnValue(socket); + await expect(server.checkUrlExist('http://localhost:9876')).rejects.toThrowError(); + }); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/packages/xml-reader.spec.ts b/projects/aas-server/src/test/packages/xml-reader.spec.ts new file mode 100644 index 00000000..8003c101 --- /dev/null +++ b/projects/aas-server/src/test/packages/xml-reader.spec.ts @@ -0,0 +1,68 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import { readFile } from "fs/promises"; +import { resolve } from "path"; +import { Logger } from "../../app/logging/logger.js"; +import { XmlReader } from '../../app/packages/xml-reader.js' +import { createSpyObj } from '../utils.js'; +import { describe, beforeAll, beforeEach, it, expect, jest } from '@jest/globals'; + +describe('XmlReader', function () { + describe('with default namespace v2.0', function () { + let reader: XmlReader; + let logger: jest.Mocked; + let xml: string; + let path: string; + + beforeAll(async function () { + path = resolve('./src/test/assets/aas-default-namespace.xml'); + xml = (await readFile(path)).toString(); + }); + + beforeEach(function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + reader = new XmlReader(logger, xml); + }); + + it('should be created', function () { + expect(reader).toBeTruthy(); + }); + + it('reads the AAS environment from a xml source', function() { + let environment = reader.readEnvironment(); + expect(environment).toBeDefined(); + }); + }); + + describe('with prefix namespace v1.0', function () { + let reader: XmlReader; + let logger: jest.Mocked; + let xml: string; + let path: string; + + beforeAll(async function () { + path = resolve('./src/test/assets/aas-prefix-namespace.xml'); + xml = (await readFile(path)).toString(); + }); + + beforeEach(function () { + logger = createSpyObj(['error', 'warning', 'info', 'debug', 'start', 'stop']); + reader = new XmlReader(logger, xml); + }); + + it('should be created', function () { + expect(reader).toBeTruthy(); + }); + + it('reads the AAS environment from a xml source', function() { + let environment = reader.readEnvironment(); + expect(environment).toBeDefined(); + }); + }); +}); \ No newline at end of file diff --git a/projects/aas-server/src/test/winston-logger.spec.ts b/projects/aas-server/src/test/winston-logger.spec.ts new file mode 100644 index 00000000..8ffb4377 --- /dev/null +++ b/projects/aas-server/src/test/winston-logger.spec.ts @@ -0,0 +1,141 @@ +/****************************************************************************** + * + * Copyright (c) 2019-2023 Fraunhofer IOSB-INA Lemgo, + * eine rechtlich nicht selbstaendige Einrichtung der Fraunhofer-Gesellschaft + * zur Foerderung der angewandten Forschung e.V. + * + *****************************************************************************/ + +import 'reflect-metadata'; +import winston from 'winston'; +import { FileLogger } from '../app/logging/file-logger.js'; +import { createSpyObj } from './utils.js'; +import { describe, beforeEach, it, expect, jest } from '@jest/globals'; + +describe('WinstonLogger', function () { + describe('log level Info', function () { + let logger: FileLogger; + let winstonLogger: jest.Mocked; + + beforeEach(function () { + winstonLogger = createSpyObj(['error', 'warn', 'info', 'isErrorEnabled', 'isInfoEnabled', 'isWarnEnabled']); + winstonLogger.isErrorEnabled.mockReturnValue(true); + winstonLogger.isWarnEnabled.mockReturnValue(true); + winstonLogger.isInfoEnabled.mockReturnValue(true); + logger = new FileLogger(winstonLogger); + }); + + it('logs all message types', function () { + logger.start('test'); + logger.info('This is an info.'); + logger.warning('This is a warning.'); + logger.error('This is an error'); + logger.stop(); + expect(logger.getMessages().length).toEqual(3); + }); + + it('logs message objects', function () { + logger.start('test'); + logger.log({ type: 'Info', text: 'This is an info.', timestamp: Date.now() }); + logger.log({ type: 'Warning', text: 'This is a warning.', timestamp: Date.now() }); + logger.log({ type: 'Error', text: 'This is an error.', timestamp: Date.now() }); + logger.stop(); + expect(logger.getMessages().length).toEqual(3); + }); + + it('logs only different errors', function () { + logger.start('test'); + logger.error('This is a first error.'); + logger.error(new Error('This is a first error.')); + logger.error('This is a seconde error.'); + logger.stop(); + expect(logger.getMessages().length).toEqual(2); + }); + + it('logs only different warnings', function () { + logger.start('test'); + logger.warning('This is a first warning.'); + logger.warning('This is a first warning.'); + logger.warning('This is a seconde warning.'); + logger.stop(); + expect(logger.getMessages().length).toEqual(2); + }); + + it('logs only different info messages', function () { + logger.start('test'); + logger.info('This is a first info.'); + logger.info('This is a seconde info.'); + logger.info('This is a seconde info.'); + logger.stop(); + expect(logger.getMessages().length).toEqual(2); + }); + }); + + describe('log level Error', function () { + let logger: FileLogger; + let winstonLogger: jest.Mocked; + + beforeEach(function () { + winstonLogger = createSpyObj(['error', 'warn', 'info', 'isErrorEnabled', 'isInfoEnabled', 'isWarnEnabled']); + winstonLogger.isErrorEnabled.mockReturnValue(true); + winstonLogger.isWarnEnabled.mockReturnValue(false); + winstonLogger.isInfoEnabled.mockReturnValue(false); + logger = new FileLogger(winstonLogger); + }); + + it('logs ony errors', function () { + logger.start('test'); + logger.info('This is an info.'); + logger.warning('This is a warning.'); + logger.error('This is an error'); + logger.stop(); + expect(logger.getMessages().length).toEqual(1); + }); + }); + + describe('log level Warning', function () { + let logger: FileLogger; + let winstonLogger: jest.Mocked; + + beforeEach(function () { + winstonLogger = createSpyObj(['error', 'warn', 'info', 'isErrorEnabled', 'isInfoEnabled', 'isWarnEnabled']); + winstonLogger.isErrorEnabled.mockReturnValue(true); + winstonLogger.isWarnEnabled.mockReturnValue(true); + winstonLogger.isInfoEnabled.mockReturnValue(false); + logger = new FileLogger(winstonLogger); + }); + + it('logs warnings and errors', function () { + logger.start('test'); + logger.info('This is an info.'); + logger.warning('This is a warning.'); + logger.error('This is an error'); + logger.stop(); + expect(logger.getMessages().length).toEqual(2); + }); + }); + + describe('log debug', function () { + let logger: FileLogger; + let winstonLogger: jest.Mocked; + + beforeEach(function () { + winstonLogger = createSpyObj(['error', 'warn', 'info', 'debug', 'isErrorEnabled', 'isInfoEnabled', 'isWarnEnabled', 'isDebugEnabled']); + winstonLogger.isErrorEnabled.mockReturnValue(true); + winstonLogger.isWarnEnabled.mockReturnValue(true); + winstonLogger.isInfoEnabled.mockReturnValue(true); + winstonLogger.isDebugEnabled.mockReturnValue(true); + logger = new FileLogger(winstonLogger); + }); + + it('logs debug messages to a console', function () { + logger.debug('This is a debug message.'); + expect(winstonLogger.debug).toHaveBeenCalled(); + }); + + it('logs errors to a console', function () { + logger.debug(new Error('This is a debug message.')); + expect(winstonLogger.debug).toHaveBeenCalled(); + }); + }); +}); \ No newline at end of file