From 6cab6135273aca8e96718d872bb5ccac83746537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20J=C3=A4ggi?= Date: Tue, 10 Dec 2024 08:37:45 +0100 Subject: [PATCH] feat: move legacy entities (WIP) --- .../src/v2/models/base/base.collection.js | 5 +- .../src/v2/models/index.js | 9 +- .../src/v2/util/util.js | 50 ++--- .../test/unit/index.test.js | 6 +- .../test/unit/service/index.test.js | 11 + .../models/api-key/api-key.collection.test.js | 98 +++++++++ .../v2/models/audit/audit.collection.test.js | 98 +++++++++ .../models/{ => base}/base.collection.test.js | 116 +++++----- .../v2/models/{ => base}/base.model.test.js | 37 ++-- .../configuration.collection.test.js | 141 ++++++++++++ .../experiment/experiment.collection.test.js | 98 +++++++++ .../import-job/import-job.collection.test.js | 130 +++++++++++ .../import-url/import-url.collection.test.js | 98 +++++++++ .../key-event/key-event.collection.test.js | 98 +++++++++ .../v2/models/opportunity.collection.test.js | 154 -------------- .../opportunity.collection.test.js | 98 +++++++++ .../opportunity.model.test.js | 14 +- .../organization.collection.test.js | 98 +++++++++ .../site-candidate.collection.test.js | 102 +++++++++ .../site-top-page.collection.test.js | 137 ++++++++++++ .../v2/models/site/site.collection.test.js | 109 ++++++++++ .../v2/models/suggestion.collection.test.js | 197 ----------------- .../suggestion/suggestion.collection.test.js | 140 ++++++++++++ .../{ => suggestion}/suggestion.model.test.js | 4 +- .../test/unit/v2/util/guards.test.js | 48 ++++- .../test/unit/v2/util/patcher.test.js | 49 +++-- .../test/unit/v2/util/util.test.js | 201 ++++++++++++++++++ 27 files changed, 1842 insertions(+), 504 deletions(-) create mode 100755 packages/spacecat-shared-data-access/test/unit/v2/models/api-key/api-key.collection.test.js create mode 100755 packages/spacecat-shared-data-access/test/unit/v2/models/audit/audit.collection.test.js rename packages/spacecat-shared-data-access/test/unit/v2/models/{ => base}/base.collection.test.js (72%) rename packages/spacecat-shared-data-access/test/unit/v2/models/{ => base}/base.model.test.js (83%) create mode 100755 packages/spacecat-shared-data-access/test/unit/v2/models/configuration/configuration.collection.test.js create mode 100755 packages/spacecat-shared-data-access/test/unit/v2/models/experiment/experiment.collection.test.js create mode 100755 packages/spacecat-shared-data-access/test/unit/v2/models/import-job/import-job.collection.test.js create mode 100755 packages/spacecat-shared-data-access/test/unit/v2/models/import-url/import-url.collection.test.js create mode 100755 packages/spacecat-shared-data-access/test/unit/v2/models/key-event/key-event.collection.test.js delete mode 100755 packages/spacecat-shared-data-access/test/unit/v2/models/opportunity.collection.test.js create mode 100755 packages/spacecat-shared-data-access/test/unit/v2/models/opportunity/opportunity.collection.test.js rename packages/spacecat-shared-data-access/test/unit/v2/models/{ => opportunity}/opportunity.model.test.js (93%) create mode 100755 packages/spacecat-shared-data-access/test/unit/v2/models/organization/organization.collection.test.js create mode 100755 packages/spacecat-shared-data-access/test/unit/v2/models/site-candidate/site-candidate.collection.test.js create mode 100755 packages/spacecat-shared-data-access/test/unit/v2/models/site-top-page/site-top-page.collection.test.js create mode 100755 packages/spacecat-shared-data-access/test/unit/v2/models/site/site.collection.test.js delete mode 100755 packages/spacecat-shared-data-access/test/unit/v2/models/suggestion.collection.test.js create mode 100755 packages/spacecat-shared-data-access/test/unit/v2/models/suggestion/suggestion.collection.test.js rename packages/spacecat-shared-data-access/test/unit/v2/models/{ => suggestion}/suggestion.model.test.js (96%) create mode 100644 packages/spacecat-shared-data-access/test/unit/v2/util/util.test.js diff --git a/packages/spacecat-shared-data-access/src/v2/models/base/base.collection.js b/packages/spacecat-shared-data-access/src/v2/models/base/base.collection.js index 0da9886c..7d42bc4e 100755 --- a/packages/spacecat-shared-data-access/src/v2/models/base/base.collection.js +++ b/packages/spacecat-shared-data-access/src/v2/models/base/base.collection.js @@ -369,10 +369,7 @@ class BaseCollection { items.forEach((item) => { try { const { Item } = this.entity.put(item).params(); - validatedItems.push({ - ...removeElectroProperties(Item), - ...item, - }); + validatedItems.push({ ...removeElectroProperties(Item), ...item }); } catch (error) { if (error instanceof ElectroValidationError) { errorItems.push({ item, error: new ValidationError(error) }); diff --git a/packages/spacecat-shared-data-access/src/v2/models/index.js b/packages/spacecat-shared-data-access/src/v2/models/index.js index c8b5016a..839b609e 100755 --- a/packages/spacecat-shared-data-access/src/v2/models/index.js +++ b/packages/spacecat-shared-data-access/src/v2/models/index.js @@ -10,14 +10,17 @@ * governing permissions and limitations under the License. */ -export * from './base/index.js'; - +export * from './api-key/index.js'; export * from './audit/index.js'; +export * from './base/index.js'; +export * from './configuration/index.js'; export * from './experiment/index.js'; +export * from './import-job/index.js'; +export * from './import-url/index.js'; export * from './key-event/index.js'; export * from './opportunity/index.js'; export * from './organization/index.js'; -export * from './site/index.js'; export * from './site-candidate/index.js'; export * from './site-top-page/index.js'; +export * from './site/index.js'; export * from './suggestion/index.js'; diff --git a/packages/spacecat-shared-data-access/src/v2/util/util.js b/packages/spacecat-shared-data-access/src/v2/util/util.js index 5e27036a..017f69b5 100644 --- a/packages/spacecat-shared-data-access/src/v2/util/util.js +++ b/packages/spacecat-shared-data-access/src/v2/util/util.js @@ -10,57 +10,47 @@ * governing permissions and limitations under the License. */ +import { hasText, isInteger } from '@adobe/spacecat-shared-utils'; import pluralize from 'pluralize'; -import { isInteger } from '@adobe/spacecat-shared-utils'; -const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1); +const capitalize = (str) => (hasText(str) ? str[0].toUpperCase() + str.slice(1) : ''); +const decapitalize = (str) => (hasText(str) ? str[0].toLowerCase() + str.slice(1) : ''); + const collectionNameToEntityName = (collectionName) => collectionName.replace('Collection', ''); -const decapitalize = (str) => str.charAt(0).toLowerCase() + str.slice(1); + const entityNameToCollectionName = (entityName) => `${pluralize.singular(entityName)}Collection`; -const entityNameToIdName = (collectionName) => `${decapitalize(collectionName)}Id`; -const entityNameToReferenceMethodName = (target, type) => { - let baseName = capitalize(target); - baseName = type === 'has_many' - ? pluralize.plural(baseName) - : pluralize.singular(baseName); +const entityNameToIdName = (entityName) => `${decapitalize(entityName)}Id`; + +const entityNameToReferenceMethodName = (target, type) => { + const baseName = type === 'has_many' + ? pluralize.plural(capitalize(target)) + : pluralize.singular(capitalize(target)); return `get${baseName}`; }; + const entityNameToAllPKValue = (entityName) => `ALL_${pluralize.plural(entityName.toUpperCase())}`; const idNameToEntityName = (idName) => capitalize(pluralize.singular(idName.replace('Id', ''))); -const keyNamesToIndexName = (keyNames) => { - const capitalizedKeyNames = keyNames.map((keyName) => capitalize(keyName)); - return `by${capitalizedKeyNames.join('And')}`; -}; +const keyNamesToIndexName = (keyNames) => `by${keyNames.map(capitalize).join('And')}`; const modelNameToEntityName = (modelName) => decapitalize(modelName); const sanitizeTimestamps = (data) => { - const sanitizedData = { ...data }; - - delete sanitizedData.createdAt; - delete sanitizedData.updatedAt; - - return sanitizedData; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { createdAt, updatedAt, ...rest } = data; + return rest; }; const sanitizeIdAndAuditFields = (entityName, data) => { const idName = entityNameToIdName(entityName); - const sanitizedData = { ...data }; - - delete sanitizedData[idName]; - - return sanitizeTimestamps(sanitizedData); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { [idName]: _, ...rest } = data; + return sanitizeTimestamps(rest); }; -function incrementVersion(version) { - if (!isInteger(version)) return 1; - - const versionNumber = parseInt(version, 10); - return versionNumber + 1; -} +const incrementVersion = (version) => (isInteger(version) ? parseInt(version, 10) + 1 : 1); export { capitalize, diff --git a/packages/spacecat-shared-data-access/test/unit/index.test.js b/packages/spacecat-shared-data-access/test/unit/index.test.js index 68a6abaa..af9dbf03 100644 --- a/packages/spacecat-shared-data-access/test/unit/index.test.js +++ b/packages/spacecat-shared-data-access/test/unit/index.test.js @@ -25,7 +25,11 @@ describe('Data Access Wrapper Tests', () => { mockFn = sinon.stub().resolves('function response'); mockContext = { env: {}, - log: sinon.spy(), + log: { + info: sinon.spy(), + debug: sinon.spy(), + error: sinon.spy(), + }, }; mockRequest = {}; }); diff --git a/packages/spacecat-shared-data-access/test/unit/service/index.test.js b/packages/spacecat-shared-data-access/test/unit/service/index.test.js index 02e383ce..87e9969b 100644 --- a/packages/spacecat-shared-data-access/test/unit/service/index.test.js +++ b/packages/spacecat-shared-data-access/test/unit/service/index.test.js @@ -110,7 +110,18 @@ describe('Data Access Object Tests', () => { ]; const electroServiceFunctions = [ + 'ApiKey', + 'Audit', + 'Configuration', + 'Experiment', + 'ImportJob', + 'ImportUrl', + 'KeyEvent', 'Opportunity', + 'Organization', + 'Site', + 'SiteCandidate', + 'SiteTopPage', 'Suggestion', ]; diff --git a/packages/spacecat-shared-data-access/test/unit/v2/models/api-key/api-key.collection.test.js b/packages/spacecat-shared-data-access/test/unit/v2/models/api-key/api-key.collection.test.js new file mode 100755 index 00000000..cfff419a --- /dev/null +++ b/packages/spacecat-shared-data-access/test/unit/v2/models/api-key/api-key.collection.test.js @@ -0,0 +1,98 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-env mocha */ + +import { expect, use as chaiUse } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { Entity } from 'electrodb'; +import { spy, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; + +import ApiKeyCollection from '../../../../../src/v2/models/api-key/api-key.collection.js'; +import ApiKey from '../../../../../src/v2/models/api-key/api-key.model.js'; +import ApiKeySchema from '../../../../../src/v2/models/api-key/api-key.schema.js'; + +chaiUse(chaiAsPromised); +chaiUse(sinonChai); + +const { attributes } = new Entity(ApiKeySchema).model.schema; + +let mockElectroService; + +describe('ApiKeyCollection', () => { + let instance; + let mockApiKeyModel; + let mockLogger; + let mockEntityRegistry; + + const mockRecord = { + apiKeyId: 's12345', + }; + + beforeEach(() => { + mockLogger = { + error: spy(), + warn: spy(), + }; + + mockEntityRegistry = { + getCollection: stub(), + }; + + mockElectroService = { + entities: { + apiKey: { + model: { + name: 'apiKey', + schema: { attributes }, + original: { + references: {}, + }, + indexes: { + primary: { + pk: { + field: 'pk', + composite: ['apiKeyId'], + }, + }, + }, + }, + }, + }, + }; + + mockApiKeyModel = new ApiKey( + mockElectroService, + mockEntityRegistry, + mockRecord, + mockLogger, + ); + + instance = new ApiKeyCollection( + mockElectroService, + mockEntityRegistry, + mockLogger, + ); + }); + + describe('constructor', () => { + it('initializes the ApiKeyCollection instance correctly', () => { + expect(instance).to.be.an('object'); + expect(instance.electroService).to.equal(mockElectroService); + expect(instance.entityRegistry).to.equal(mockEntityRegistry); + expect(instance.log).to.equal(mockLogger); + + expect(mockApiKeyModel).to.be.an('object'); + }); + }); +}); diff --git a/packages/spacecat-shared-data-access/test/unit/v2/models/audit/audit.collection.test.js b/packages/spacecat-shared-data-access/test/unit/v2/models/audit/audit.collection.test.js new file mode 100755 index 00000000..b3385051 --- /dev/null +++ b/packages/spacecat-shared-data-access/test/unit/v2/models/audit/audit.collection.test.js @@ -0,0 +1,98 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-env mocha */ + +import { expect, use as chaiUse } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { Entity } from 'electrodb'; +import { spy, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; + +import AuditCollection from '../../../../../src/v2/models/audit/audit.collection.js'; +import Audit from '../../../../../src/v2/models/audit/audit.model.js'; +import AuditSchema from '../../../../../src/v2/models/audit/audit.schema.js'; + +chaiUse(chaiAsPromised); +chaiUse(sinonChai); + +const { attributes } = new Entity(AuditSchema).model.schema; + +let mockElectroService; + +describe('AuditCollection', () => { + let instance; + let mockAuditModel; + let mockLogger; + let mockEntityRegistry; + + const mockRecord = { + auditId: 's12345', + }; + + beforeEach(() => { + mockLogger = { + error: spy(), + warn: spy(), + }; + + mockEntityRegistry = { + getCollection: stub(), + }; + + mockElectroService = { + entities: { + audit: { + model: { + name: 'audit', + schema: { attributes }, + original: { + references: {}, + }, + indexes: { + primary: { + pk: { + field: 'pk', + composite: ['auditId'], + }, + }, + }, + }, + }, + }, + }; + + mockAuditModel = new Audit( + mockElectroService, + mockEntityRegistry, + mockRecord, + mockLogger, + ); + + instance = new AuditCollection( + mockElectroService, + mockEntityRegistry, + mockLogger, + ); + }); + + describe('constructor', () => { + it('initializes the AuditCollection instance correctly', () => { + expect(instance).to.be.an('object'); + expect(instance.electroService).to.equal(mockElectroService); + expect(instance.entityRegistry).to.equal(mockEntityRegistry); + expect(instance.log).to.equal(mockLogger); + + expect(mockAuditModel).to.be.an('object'); + }); + }); +}); diff --git a/packages/spacecat-shared-data-access/test/unit/v2/models/base.collection.test.js b/packages/spacecat-shared-data-access/test/unit/v2/models/base/base.collection.test.js similarity index 72% rename from packages/spacecat-shared-data-access/test/unit/v2/models/base.collection.test.js rename to packages/spacecat-shared-data-access/test/unit/v2/models/base/base.collection.test.js index 04d9b921..cfb3ced2 100755 --- a/packages/spacecat-shared-data-access/test/unit/v2/models/base.collection.test.js +++ b/packages/spacecat-shared-data-access/test/unit/v2/models/base/base.collection.test.js @@ -17,14 +17,14 @@ import { ElectroValidationError } from 'electrodb'; import { spy, stub } from 'sinon'; import chaiAsPromised from 'chai-as-promised'; -import BaseCollection from '../../../../src/v2/models/base/base.collection.js'; +import BaseCollection from '../../../../../src/v2/models/base/base.collection.js'; chaiUse(chaiAsPromised); describe('BaseCollection', () => { let baseCollectionInstance; let mockElectroService; - let mockModelFactory; + let mockEntityRegistry; let mockLogger; const mockRecord = { @@ -38,7 +38,7 @@ describe('BaseCollection', () => { }; beforeEach(() => { - mockModelFactory = { + mockEntityRegistry = { getCollection: stub(), }; @@ -49,13 +49,14 @@ describe('BaseCollection', () => { mockElectroService = { entities: { - mockentitymodel: { + mockEntityModel: { get: stub(), put: stub(), create: stub(), query: stub(), model: { name: 'mockentitymodel', + indexes: [], table: 'mockentitymodel', }, }, @@ -64,7 +65,7 @@ describe('BaseCollection', () => { baseCollectionInstance = new BaseCollection( mockElectroService, - mockModelFactory, + mockEntityRegistry, class MockEntityModel { constructor(service, factory, data) { this.data = data; @@ -80,59 +81,59 @@ describe('BaseCollection', () => { describe('findById', () => { it('returns the entity if found', async () => { const mockFindResult = { data: mockRecord }; - mockElectroService.entities.mockentitymodel.get.returns( + mockElectroService.entities.mockEntityModel.get.returns( { go: () => Promise.resolve(mockFindResult) }, ); const result = await baseCollectionInstance.findById('ef39921f-9a02-41db-b491-02c98987d956'); expect(result).to.deep.include(mockEntityModel); - expect(mockElectroService.entities.mockentitymodel.get.calledOnce).to.be.true; + expect(mockElectroService.entities.mockEntityModel.get.calledOnce).to.be.true; }); it('returns null if the entity is not found', async () => { - mockElectroService.entities.mockentitymodel.get.returns( + mockElectroService.entities.mockEntityModel.get.returns( { go: () => Promise.resolve(null) }, ); const result = await baseCollectionInstance.findById('ef39921f-9a02-41db-b491-02c98987d956'); expect(result).to.be.null; - expect(mockElectroService.entities.mockentitymodel.get.calledOnce).to.be.true; + expect(mockElectroService.entities.mockEntityModel.get.calledOnce).to.be.true; }); }); describe('findByIndexKeys', () => { it('throws error if keys is not provided', async () => { await expect(baseCollectionInstance.findByIndexKeys()) - .to.be.rejectedWith('Failed to find by index keys [mockentitymodel]: keys are required'); + .to.be.rejectedWith('Failed to query [mockEntityModel]: keys are required'); expect(mockLogger.error.calledOnce).to.be.true; }); it('throws error if index is not found', async () => { await expect(baseCollectionInstance.findByIndexKeys({ someKey: 'someValue' })) - .to.be.rejectedWith('Failed to find by index keys [mockentitymodel]: index [bySomeKey] not found'); + .to.be.rejectedWith('Failed to query [mockEntityModel]: index [primary] not found'); expect(mockLogger.error.calledOnce).to.be.true; }); }); describe('create', () => { it('throws an error if the record is empty', async () => { - await expect(baseCollectionInstance.create(null)).to.be.rejectedWith('Failed to create [mockentitymodel]'); + await expect(baseCollectionInstance.create(null)).to.be.rejectedWith('Failed to create [mockEntityModel]'); expect(mockLogger.error.calledOnce).to.be.true; }); it('creates a new entity successfully', async () => { - mockElectroService.entities.mockentitymodel.create.returns( + mockElectroService.entities.mockEntityModel.create.returns( { go: () => Promise.resolve(mockEntityModel) }, ); const result = await baseCollectionInstance.create(mockRecord); expect(result).to.deep.include(mockEntityModel); - expect(mockElectroService.entities.mockentitymodel.create.calledOnce).to.be.true; + expect(mockElectroService.entities.mockEntityModel.create.calledOnce).to.be.true; }); it('logs an error and throws when creation fails', async () => { const error = new Error('Create failed'); - mockElectroService.entities.mockentitymodel.create.returns( + mockElectroService.entities.mockEntityModel.create.returns( { go: () => Promise.reject(error) }, ); @@ -144,7 +145,7 @@ describe('BaseCollection', () => { describe('createMany', () => { it('throws an error if the records are empty', async () => { await expect(baseCollectionInstance.createMany(null)) - .to.be.rejectedWith('Failed to create many [mockentitymodel]: items must be a non-empty array'); + .to.be.rejectedWith('Failed to create many [mockEntityModel]: items must be a non-empty array'); expect(mockLogger.error.calledOnce).to.be.true; }); @@ -155,29 +156,24 @@ describe('BaseCollection', () => { method: 'batchWrite', params: { RequestItems: { - mockentitymodel: [ + mockEntityModel: [ { PutRequest: { Item: mockRecord } }, { PutRequest: { Item: mockRecord } }, ], }, }, }; - mockElectroService.entities.mockentitymodel.put.returns( + mockElectroService.entities.mockEntityModel.put.returns( { - go: (options) => { - options.listeners[0](mockPutResults); - options.listeners[0]({ type: 'result' }); - options.listeners[0]({ type: 'query', method: 'ignore' }); - return Promise.resolve({ unprocessed: [] }); - }, - params: () => {}, + go: () => Promise.resolve(mockPutResults), + params: () => ({ Item: { ...mockRecord } }), }, ); const result = await baseCollectionInstance.createMany(mockRecords); expect(result.createdItems).to.be.an('array').that.has.length(2); expect(result.createdItems).to.deep.include(mockEntityModel); - expect(mockElectroService.entities.mockentitymodel.put.calledThrice).to.be.true; + expect(mockElectroService.entities.mockEntityModel.put.calledThrice).to.be.true; }); it('creates many with a parent entity', async () => { @@ -187,67 +183,57 @@ describe('BaseCollection', () => { method: 'batchWrite', params: { RequestItems: { - mockentitymodel: [ + mockEntityModel: [ { PutRequest: { Item: mockRecord } }, { PutRequest: { Item: mockRecord } }, ], }, }, }; - mockElectroService.entities.mockentitymodel.put.returns( + mockElectroService.entities.mockEntityModel.put.returns( { - go: (options) => { - options.listeners[0](mockPutResults); - options.listeners[0]({ type: 'result' }); - options.listeners[0]({ type: 'query', method: 'ignore' }); - return Promise.resolve({ unprocessed: [] }); - }, - params: () => {}, + go: () => Promise.resolve(mockPutResults), + params: () => ({ Item: { ...mockRecord } }), }, ); const result = await baseCollectionInstance.createMany( mockRecords, - { entity: { model: { name: 'mockentitymodel' } } }, + { entity: { model: { name: 'mockEntityModel' } } }, ); expect(result.createdItems).to.be.an('array').that.has.length(2); expect(result.createdItems).to.deep.include(mockEntityModel); - expect(mockElectroService.entities.mockentitymodel.put.calledThrice).to.be.true; + expect(mockElectroService.entities.mockEntityModel.put.calledThrice).to.be.true; }); it('creates some entities successfully with unprocessed items', async () => { const mockRecords = [mockRecord, mockRecord]; - const mockPutResults = { - type: 'query', - method: 'batchWrite', - params: { - RequestItems: { - mockentitymodel: [ - { PutRequest: { Item: mockRecord } }, - ], - }, - }, - }; - mockElectroService.entities.mockentitymodel.put.returns( + let itemCount = 0; + + mockElectroService.entities.mockEntityModel.put.returns( { - go: (options) => { - options.listeners[0](mockPutResults); - return Promise.resolve({ unprocessed: [mockRecord] }); + go: () => Promise.resolve({ unprocessed: [mockRecord] }), + params: () => { + if (itemCount === 0) { + itemCount += 1; + return { Item: { ...mockRecord } }; + } else { + throw new ElectroValidationError('Validation failed'); + } }, - params: () => {}, }, ); const result = await baseCollectionInstance.createMany(mockRecords); expect(result.createdItems).to.be.an('array').that.has.length(1); expect(result.createdItems).to.deep.include(mockEntityModel); - expect(mockElectroService.entities.mockentitymodel.put.calledThrice).to.be.true; - expect(mockLogger.error.calledOnceWith(`Failed to process all items in batch write for [mockentitymodel]: ${JSON.stringify([mockRecord])}`)).to.be.true; + expect(mockElectroService.entities.mockEntityModel.put.calledThrice).to.be.true; + expect(mockLogger.error.calledOnceWith(`Failed to process all items in batch write for [mockEntityModel]: ${JSON.stringify([mockRecord])}`)).to.be.true; }); it('fails creating some items due to ValidationError', async () => { const error = new ElectroValidationError('Validation failed'); - mockElectroService.entities.mockentitymodel.put.returns( + mockElectroService.entities.mockEntityModel.put.returns( { params: () => { throw error; } }, ); @@ -260,10 +246,10 @@ describe('BaseCollection', () => { it('logs an error and throws when creation fails', async () => { const error = new Error('Create failed'); const mockRecords = [mockRecord, mockRecord]; - mockElectroService.entities.mockentitymodel.put.returns( + mockElectroService.entities.mockEntityModel.put.returns( { go: () => Promise.reject(error), - params: () => {}, + params: () => ({ Item: { ...mockRecord } }), }, ); @@ -275,22 +261,22 @@ describe('BaseCollection', () => { describe('_saveMany', () => { /* eslint-disable no-underscore-dangle */ it('throws an error if the records are empty', async () => { await expect(baseCollectionInstance._saveMany(null)) - .to.be.rejectedWith('Failed to save many [mockentitymodel]: items must be a non-empty array'); + .to.be.rejectedWith('Failed to save many [mockEntityModel]: items must be a non-empty array'); expect(mockLogger.error.calledOnce).to.be.true; }); it('saves multiple entities successfully', async () => { const mockRecords = [mockRecord, mockRecord]; - mockElectroService.entities.mockentitymodel.put.returns({ go: () => [] }); + mockElectroService.entities.mockEntityModel.put.returns({ go: () => [] }); const result = await baseCollectionInstance._saveMany(mockRecords); expect(result).to.be.undefined; - expect(mockElectroService.entities.mockentitymodel.put.calledOnce).to.be.true; + expect(mockElectroService.entities.mockEntityModel.put.calledOnce).to.be.true; }); it('saves some entities successfully with unprocessed items', async () => { const mockRecords = [mockRecord, mockRecord]; - mockElectroService.entities.mockentitymodel.put.returns( + mockElectroService.entities.mockEntityModel.put.returns( { go: () => Promise.resolve({ unprocessed: [mockRecord] }), }, @@ -298,14 +284,14 @@ describe('BaseCollection', () => { const result = await baseCollectionInstance._saveMany(mockRecords); expect(result).to.be.undefined; - expect(mockElectroService.entities.mockentitymodel.put.calledOnce).to.be.true; - expect(mockLogger.error.calledOnceWith(`Failed to process all items in batch write for [mockentitymodel]: ${JSON.stringify([mockRecord])}`)).to.be.true; + expect(mockElectroService.entities.mockEntityModel.put.calledOnce).to.be.true; + expect(mockLogger.error.calledOnceWith(`Failed to process all items in batch write for [mockEntityModel]: ${JSON.stringify([mockRecord])}`)).to.be.true; }); it('throws error and logs when save fails', async () => { const error = new Error('Save failed'); const mockRecords = [mockRecord, mockRecord]; - mockElectroService.entities.mockentitymodel.put.returns( + mockElectroService.entities.mockEntityModel.put.returns( { go: () => Promise.reject(error) }, ); diff --git a/packages/spacecat-shared-data-access/test/unit/v2/models/base.model.test.js b/packages/spacecat-shared-data-access/test/unit/v2/models/base/base.model.test.js similarity index 83% rename from packages/spacecat-shared-data-access/test/unit/v2/models/base.model.test.js rename to packages/spacecat-shared-data-access/test/unit/v2/models/base/base.model.test.js index d72b3e91..79433ecd 100755 --- a/packages/spacecat-shared-data-access/test/unit/v2/models/base.model.test.js +++ b/packages/spacecat-shared-data-access/test/unit/v2/models/base/base.model.test.js @@ -17,8 +17,8 @@ import { Entity } from 'electrodb'; import { spy, stub } from 'sinon'; import chaiAsPromised from 'chai-as-promised'; -import BaseModel from '../../../../src/v2/models/base/base.model.js'; -import OpportunitySchema from '../../../../src/v2/models/opportunity/opportunity.schema.js'; +import BaseModel from '../../../../../src/v2/models/base/base.model.js'; +import OpportunitySchema from '../../../../../src/v2/models/opportunity/opportunity.schema.js'; chaiUse(chaiAsPromised); @@ -28,10 +28,10 @@ describe('BaseModel', () => { let mockElectroService; let baseModelInstance; let mockLogger; - let mockModelFactory; + let mockEntityRegistry; const mockRecord = { - basemodelId: '12345', + baseModelId: '12345', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; @@ -41,13 +41,13 @@ describe('BaseModel', () => { error: spy(), }; - mockModelFactory = { + mockEntityRegistry = { getCollection: stub(), }; mockElectroService = { entities: { - basemodel: { + baseModel: { name: 'basemodel', model: { name: 'basemodel', @@ -68,7 +68,12 @@ describe('BaseModel', () => { }, }; - baseModelInstance = new BaseModel(mockElectroService, mockModelFactory, mockRecord, mockLogger); + baseModelInstance = new BaseModel( + mockElectroService, + mockEntityRegistry, + mockRecord, + mockLogger, + ); }); describe('base', () => { @@ -77,8 +82,8 @@ describe('BaseModel', () => { }); it('returns when initializeAttributes has no attributes', () => { - mockElectroService.entities.basemodel.model.schema.attributes = {}; - const instance = new BaseModel(mockElectroService, mockModelFactory, {}, mockLogger); + mockElectroService.entities.baseModel.model.schema.attributes = {}; + const instance = new BaseModel(mockElectroService, mockEntityRegistry, {}, mockLogger); expect(instance).to.be.an.instanceOf(BaseModel); }); }); @@ -106,15 +111,15 @@ describe('BaseModel', () => { describe('remove', () => { it('removes the record and returns the current instance', async () => { - mockElectroService.entities.basemodel.remove.returns({ go: () => Promise.resolve() }); + mockElectroService.entities.baseModel.remove.returns({ go: () => Promise.resolve() }); await expect(baseModelInstance.remove()).to.eventually.equal(baseModelInstance); - expect(mockElectroService.entities.basemodel.remove.calledOnce).to.be.true; + expect(mockElectroService.entities.baseModel.remove.calledOnce).to.be.true; expect(mockLogger.error.notCalled).to.be.true; }); it('logs an error and throws when remove fails', async () => { const error = new Error('Remove failed'); - mockElectroService.entities.basemodel.remove.returns({ go: () => Promise.reject(error) }); + mockElectroService.entities.baseModel.remove.returns({ go: () => Promise.reject(error) }); await expect(baseModelInstance.remove()).to.be.rejectedWith('Remove failed'); expect(mockLogger.error.calledOnce).to.be.true; @@ -151,27 +156,27 @@ describe('BaseModel', () => { }); it('returns undefined if the reference does not exist', async () => { - mockModelFactory.getCollection.returns({ findByIndexKeys: stub() }); + mockEntityRegistry.getCollection.returns({ allByIndexKeys: stub() }); const result = await baseModelInstance._fetchReference('has_many', 'Foo'); expect(result).to.be.undefined; }); it('fetches a belongs_to reference by ID', async () => { - mockModelFactory.getCollection.returns({ findById: stub().returns('bar') }); + mockEntityRegistry.getCollection.returns({ findById: stub().returns('bar') }); baseModelInstance.record.fooId = '12345'; const result = await baseModelInstance._fetchReference('belongs_to', 'Foo'); expect(result).to.equal('bar'); }); it('fetches a has_one reference by ID', async () => { - mockModelFactory.getCollection.returns({ findById: stub().returns('bar') }); + mockEntityRegistry.getCollection.returns({ findById: stub().returns('bar') }); baseModelInstance.record.fooId = '12345'; const result = await baseModelInstance._fetchReference('has_one', 'Foo'); expect(result).to.equal('bar'); }); it('fetches a has_many reference by foreign key', async () => { - mockModelFactory.getCollection.returns({ findByIndexKeys: stub().returns(['bar']) }); + mockEntityRegistry.getCollection.returns({ allByIndexKeys: stub().returns(['bar']) }); const result = await baseModelInstance._fetchReference('has_many', 'Foo'); expect(result).to.deep.equal(['bar']); }); diff --git a/packages/spacecat-shared-data-access/test/unit/v2/models/configuration/configuration.collection.test.js b/packages/spacecat-shared-data-access/test/unit/v2/models/configuration/configuration.collection.test.js new file mode 100755 index 00000000..bd046efc --- /dev/null +++ b/packages/spacecat-shared-data-access/test/unit/v2/models/configuration/configuration.collection.test.js @@ -0,0 +1,141 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-env mocha */ + +import { expect, use as chaiUse } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { Entity } from 'electrodb'; +import { spy, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; + +import ConfigurationCollection from '../../../../../src/v2/models/configuration/configuration.collection.js'; +import Configuration from '../../../../../src/v2/models/configuration/configuration.model.js'; +import ConfigurationSchema from '../../../../../src/v2/models/configuration/configuration.schema.js'; + +chaiUse(chaiAsPromised); +chaiUse(sinonChai); + +const { attributes } = new Entity(ConfigurationSchema).model.schema; + +let mockElectroService; + +describe('ConfigurationCollection', () => { + let instance; + let mockConfigurationModel; + let mockLogger; + let mockEntityRegistry; + + const mockRecord = { + configurationId: 's12345', + }; + + beforeEach(() => { + mockLogger = { + error: spy(), + warn: spy(), + }; + + mockEntityRegistry = { + getCollection: stub(), + }; + + mockElectroService = { + entities: { + configuration: { + model: { + name: 'configuration', + schema: { attributes }, + original: { + references: {}, + }, + indexes: { + primary: { + pk: { + field: 'pk', + composite: ['configurationId'], + }, + }, + }, + }, + create: stub().returns({ + go: stub().resolves({ data: mockRecord }), + }), + }, + }, + }; + + mockConfigurationModel = new Configuration( + mockElectroService, + mockEntityRegistry, + mockRecord, + mockLogger, + ); + + instance = new ConfigurationCollection( + mockElectroService, + mockEntityRegistry, + mockLogger, + ); + }); + + describe('constructor', () => { + it('initializes the ConfigurationCollection instance correctly', () => { + expect(instance).to.be.an('object'); + expect(instance.electroService).to.equal(mockElectroService); + expect(instance.entityRegistry).to.equal(mockEntityRegistry); + expect(instance.log).to.equal(mockLogger); + + expect(mockConfigurationModel).to.be.an('object'); + }); + }); + + describe('create', () => { + it('creates a new configuration as first version', async () => { + instance.findLatest = stub().resolves(null); + + const result = await instance.create(mockRecord); + + expect(result).to.be.an('object'); + expect(result.getId()).to.equal(mockRecord.configurationId); + }); + + it('creates a new configuration as a new version', async () => { + const latestConfiguration = { + getId: () => 's12345', + getVersion: () => 1, + }; + + instance.findLatest = stub().resolves(latestConfiguration); + mockRecord.version = 2; + + const result = await instance.create(mockRecord); + + expect(result).to.be.an('object'); + expect(result.getId()).to.equal(mockRecord.configurationId); + expect(result.getVersion()).to.equal(2); + }); + }); + + describe('findLatest', () => { + it('returns the latest configuration', async () => { + const mockResult = { configurationId: 's12345' }; + + instance.findByAll = stub().resolves(mockResult); + + const result = await instance.findLatest(); + + expect(result).to.deep.equal(mockResult); + expect(instance.findByAll).to.have.been.calledWithExactly({}, { order: 'desc' }); + }); + }); +}); diff --git a/packages/spacecat-shared-data-access/test/unit/v2/models/experiment/experiment.collection.test.js b/packages/spacecat-shared-data-access/test/unit/v2/models/experiment/experiment.collection.test.js new file mode 100755 index 00000000..a7fe3693 --- /dev/null +++ b/packages/spacecat-shared-data-access/test/unit/v2/models/experiment/experiment.collection.test.js @@ -0,0 +1,98 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-env mocha */ + +import { expect, use as chaiUse } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { Entity } from 'electrodb'; +import { spy, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; + +import ExperimentCollection from '../../../../../src/v2/models/experiment/experiment.collection.js'; +import Experiment from '../../../../../src/v2/models/experiment/experiment.model.js'; +import ExperimentSchema from '../../../../../src/v2/models/experiment/experiment.schema.js'; + +chaiUse(chaiAsPromised); +chaiUse(sinonChai); + +const { attributes } = new Entity(ExperimentSchema).model.schema; + +let mockElectroService; + +describe('ExperimentCollection', () => { + let instance; + let mockExperimentModel; + let mockLogger; + let mockEntityRegistry; + + const mockRecord = { + experimentId: 's12345', + }; + + beforeEach(() => { + mockLogger = { + error: spy(), + warn: spy(), + }; + + mockEntityRegistry = { + getCollection: stub(), + }; + + mockElectroService = { + entities: { + experiment: { + model: { + name: 'experiment', + schema: { attributes }, + original: { + references: {}, + }, + indexes: { + primary: { + pk: { + field: 'pk', + composite: ['experimentId'], + }, + }, + }, + }, + }, + }, + }; + + mockExperimentModel = new Experiment( + mockElectroService, + mockEntityRegistry, + mockRecord, + mockLogger, + ); + + instance = new ExperimentCollection( + mockElectroService, + mockEntityRegistry, + mockLogger, + ); + }); + + describe('constructor', () => { + it('initializes the ExperimentCollection instance correctly', () => { + expect(instance).to.be.an('object'); + expect(instance.electroService).to.equal(mockElectroService); + expect(instance.entityRegistry).to.equal(mockEntityRegistry); + expect(instance.log).to.equal(mockLogger); + + expect(mockExperimentModel).to.be.an('object'); + }); + }); +}); diff --git a/packages/spacecat-shared-data-access/test/unit/v2/models/import-job/import-job.collection.test.js b/packages/spacecat-shared-data-access/test/unit/v2/models/import-job/import-job.collection.test.js new file mode 100755 index 00000000..9a6fad62 --- /dev/null +++ b/packages/spacecat-shared-data-access/test/unit/v2/models/import-job/import-job.collection.test.js @@ -0,0 +1,130 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-env mocha */ + +import { expect, use as chaiUse } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { Entity } from 'electrodb'; +import { spy, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; + +import ImportJobCollection from '../../../../../src/v2/models/import-job/import-job.collection.js'; +import ImportJob from '../../../../../src/v2/models/import-job/import-job.model.js'; +import ImportJobSchema from '../../../../../src/v2/models/import-job/import-job.schema.js'; + +chaiUse(chaiAsPromised); +chaiUse(sinonChai); + +const { attributes } = new Entity(ImportJobSchema).model.schema; + +let mockElectroService; + +describe('ImportJobCollection', () => { + let instance; + let mockImportJobModel; + let mockLogger; + let mockEntityRegistry; + + const mockRecord = { + importJobId: 's12345', + }; + + beforeEach(() => { + mockLogger = { + error: spy(), + warn: spy(), + }; + + mockEntityRegistry = { + getCollection: stub(), + }; + + mockElectroService = { + entities: { + importJob: { + model: { + name: 'importJob', + schema: { attributes }, + original: { + references: {}, + }, + indexes: { + primary: { + pk: { + field: 'pk', + composite: ['importJobId'], + }, + }, + }, + }, + }, + }, + }; + + mockImportJobModel = new ImportJob( + mockElectroService, + mockEntityRegistry, + mockRecord, + mockLogger, + ); + + instance = new ImportJobCollection( + mockElectroService, + mockEntityRegistry, + mockLogger, + ); + }); + + describe('constructor', () => { + it('initializes the ImportJobCollection instance correctly', () => { + expect(instance).to.be.an('object'); + expect(instance.electroService).to.equal(mockElectroService); + expect(instance.entityRegistry).to.equal(mockEntityRegistry); + expect(instance.log).to.equal(mockLogger); + + expect(mockImportJobModel).to.be.an('object'); + }); + }); + + describe('allByDateRange', () => { + it('throws an error if the startDate is not a valid iso date', async () => { + await expect(instance.allByDateRange()).to.be.rejectedWith('Invalid start date: undefined'); + }); + + it('throws an error if the endDate is not a valid iso date', async () => { + const startIsoDate = '2024-12-06T08:35:24.125Z'; + await expect(instance.allByDateRange(startIsoDate)).to.be.rejectedWith('Invalid end date: undefined'); + }); + + it('returns all import jobs by date range', async () => { + const startIsoDate = '2024-12-06T08:35:24.125Z'; + const endIsoDate = '2024-12-07T08:35:24.125Z'; + + const mockResult = [{ importJobId: 's12345' }]; + + instance.all = stub().resolves(mockResult); + + const result = await instance.allByDateRange(startIsoDate, endIsoDate); + + expect(result).to.deep.equal(mockResult); + expect(instance.all).to.have.been.calledWithExactly({}, { + between: + { + attribute: 'startedAt', + start: '2024-12-06T08:35:24.125Z', + end: '2024-12-07T08:35:24.125Z', + }, + }); + }); + }); +}); diff --git a/packages/spacecat-shared-data-access/test/unit/v2/models/import-url/import-url.collection.test.js b/packages/spacecat-shared-data-access/test/unit/v2/models/import-url/import-url.collection.test.js new file mode 100755 index 00000000..8e865684 --- /dev/null +++ b/packages/spacecat-shared-data-access/test/unit/v2/models/import-url/import-url.collection.test.js @@ -0,0 +1,98 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-env mocha */ + +import { expect, use as chaiUse } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { Entity } from 'electrodb'; +import { spy, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; + +import ImportUrlCollection from '../../../../../src/v2/models/import-url/import-url.collection.js'; +import ImportUrl from '../../../../../src/v2/models/import-url/import-url.model.js'; +import ImportUrlSchema from '../../../../../src/v2/models/import-url/import-url.schema.js'; + +chaiUse(chaiAsPromised); +chaiUse(sinonChai); + +const { attributes } = new Entity(ImportUrlSchema).model.schema; + +let mockElectroService; + +describe('ImportUrlCollection', () => { + let instance; + let mockImportUrlModel; + let mockLogger; + let mockEntityRegistry; + + const mockRecord = { + importUrlId: 's12345', + }; + + beforeEach(() => { + mockLogger = { + error: spy(), + warn: spy(), + }; + + mockEntityRegistry = { + getCollection: stub(), + }; + + mockElectroService = { + entities: { + importUrl: { + model: { + name: 'importUrl', + schema: { attributes }, + original: { + references: {}, + }, + indexes: { + primary: { + pk: { + field: 'pk', + composite: ['importUrlId'], + }, + }, + }, + }, + }, + }, + }; + + mockImportUrlModel = new ImportUrl( + mockElectroService, + mockEntityRegistry, + mockRecord, + mockLogger, + ); + + instance = new ImportUrlCollection( + mockElectroService, + mockEntityRegistry, + mockLogger, + ); + }); + + describe('constructor', () => { + it('initializes the ImportUrlCollection instance correctly', () => { + expect(instance).to.be.an('object'); + expect(instance.electroService).to.equal(mockElectroService); + expect(instance.entityRegistry).to.equal(mockEntityRegistry); + expect(instance.log).to.equal(mockLogger); + + expect(mockImportUrlModel).to.be.an('object'); + }); + }); +}); diff --git a/packages/spacecat-shared-data-access/test/unit/v2/models/key-event/key-event.collection.test.js b/packages/spacecat-shared-data-access/test/unit/v2/models/key-event/key-event.collection.test.js new file mode 100755 index 00000000..a9ee513e --- /dev/null +++ b/packages/spacecat-shared-data-access/test/unit/v2/models/key-event/key-event.collection.test.js @@ -0,0 +1,98 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-env mocha */ + +import { expect, use as chaiUse } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { Entity } from 'electrodb'; +import { spy, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; + +import KeyEventCollection from '../../../../../src/v2/models/key-event/key-event.collection.js'; +import KeyEvent from '../../../../../src/v2/models/key-event/key-event.model.js'; +import KeyEventSchema from '../../../../../src/v2/models/key-event/key-event.schema.js'; + +chaiUse(chaiAsPromised); +chaiUse(sinonChai); + +const { attributes } = new Entity(KeyEventSchema).model.schema; + +let mockElectroService; + +describe('KeyEventCollection', () => { + let instance; + let mockKeyEventModel; + let mockLogger; + let mockEntityRegistry; + + const mockRecord = { + keyEventId: 's12345', + }; + + beforeEach(() => { + mockLogger = { + error: spy(), + warn: spy(), + }; + + mockEntityRegistry = { + getCollection: stub(), + }; + + mockElectroService = { + entities: { + keyEvent: { + model: { + name: 'keyEvent', + schema: { attributes }, + original: { + references: {}, + }, + indexes: { + primary: { + pk: { + field: 'pk', + composite: ['keyEventId'], + }, + }, + }, + }, + }, + }, + }; + + mockKeyEventModel = new KeyEvent( + mockElectroService, + mockEntityRegistry, + mockRecord, + mockLogger, + ); + + instance = new KeyEventCollection( + mockElectroService, + mockEntityRegistry, + mockLogger, + ); + }); + + describe('constructor', () => { + it('initializes the KeyEventCollection instance correctly', () => { + expect(instance).to.be.an('object'); + expect(instance.electroService).to.equal(mockElectroService); + expect(instance.entityRegistry).to.equal(mockEntityRegistry); + expect(instance.log).to.equal(mockLogger); + + expect(mockKeyEventModel).to.be.an('object'); + }); + }); +}); diff --git a/packages/spacecat-shared-data-access/test/unit/v2/models/opportunity.collection.test.js b/packages/spacecat-shared-data-access/test/unit/v2/models/opportunity.collection.test.js deleted file mode 100755 index 601b88b4..00000000 --- a/packages/spacecat-shared-data-access/test/unit/v2/models/opportunity.collection.test.js +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright 2024 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -/* eslint-env mocha */ - -import { expect, use as chaiUse } from 'chai'; -import { Entity } from 'electrodb'; -import { spy, stub } from 'sinon'; -import chaiAsPromised from 'chai-as-promised'; - -import Opportunity from '../../../../src/v2/models/opportunity/opportunity.model.js'; -import OpportunityCollection from '../../../../src/v2/models/opportunity/opportunity.collection.js'; -import OpportunitySchema from '../../../../src/v2/models/opportunity/opportunity.schema.js'; - -chaiUse(chaiAsPromised); - -const opportunityEntity = new Entity(OpportunitySchema); - -const mockElectroService = { - entities: { - opportunity: { - model: { - name: 'opportunity', - schema: opportunityEntity.model.schema, - original: { - references: {}, - }, - }, - query: { - bySiteId: stub(), - bySiteIdAndStatus: stub(), - }, - put: stub(), - }, - }, -}; - -// OpportunityCollection Unit Tests -describe('OpportunityCollection', () => { - let opportunityCollectionInstance; - let mockLogger; - let mockModelFactory; - - const mockRecord = { - opportunityId: 'op12345', - siteId: 'site67890', - data: { - foo: 'bar', - bing: 'batz', - }, - }; - const mockOpportunityModel = new Opportunity( - mockElectroService, - mockModelFactory, - mockRecord, - mockLogger, - ); - - beforeEach(() => { - mockLogger = { - error: spy(), - warn: spy(), - }; - - mockModelFactory = { - getCollection: stub(), - }; - - opportunityCollectionInstance = new OpportunityCollection( - mockElectroService, - mockModelFactory, - mockLogger, - ); - }); - - describe('constructor', () => { - it('initializes the OpportunityCollection instance correctly', () => { - expect(opportunityCollectionInstance).to.be.an('object'); - expect(opportunityCollectionInstance.electroService).to.equal(mockElectroService); - expect(opportunityCollectionInstance.modelFactory).to.equal(mockModelFactory); - expect(opportunityCollectionInstance.log).to.equal(mockLogger); - }); - }); - - describe('allBySiteId', () => { - it('returns an array of Opportunity instances when opportunities exist', async () => { - const mockFindResults = { data: [mockRecord] }; - mockElectroService.entities.opportunity.query.bySiteId.returns( - { go: () => Promise.resolve(mockFindResults) }, - ); - - const results = await opportunityCollectionInstance.allBySiteId('site67890'); - expect(results).to.be.an('array').that.has.length(1); - expect(results[0]).to.be.instanceOf(Opportunity); - expect(results[0].record).to.deep.include(mockOpportunityModel.record); - }); - - it('returns an empty array if no opportunities exist for the given site ID', async () => { - mockElectroService.entities.opportunity.query.bySiteId.returns( - { go: () => Promise.resolve([]) }, - ); - - const results = await opportunityCollectionInstance.allBySiteId('site67890'); - expect(results).to.be.an('array').that.is.empty; - }); - - it('throws an error if siteId is not provided', async () => { - await expect(opportunityCollectionInstance.allBySiteId('')) - .to.be.rejectedWith('SiteId is required'); - }); - }); - - describe('allBySiteIdAndStatus', () => { - it('returns an array of Opportunity instances when opportunities exist', async () => { - const mockFindResults = { data: [mockRecord] }; - mockElectroService.entities.opportunity.query.bySiteIdAndStatus.returns( - { go: () => Promise.resolve(mockFindResults) }, - ); - - const results = await opportunityCollectionInstance.allBySiteIdAndStatus('site67890', 'IN_PROGRESS'); - expect(results).to.be.an('array').that.has.length(1); - expect(results[0]).to.be.instanceOf(Opportunity); - expect(results[0].record).to.deep.include(mockOpportunityModel.record); - }); - - it('returns an empty array if no opportunities exist for the given site ID and status', async () => { - mockElectroService.entities.opportunity.query.bySiteIdAndStatus.returns( - { go: () => Promise.resolve([]) }, - ); - - const results = await opportunityCollectionInstance.allBySiteIdAndStatus('site67890', 'IN_PROGRESS'); - expect(results).to.be.an('array').that.is.empty; - }); - - it('throws an error if siteId is not provided', async () => { - await expect(opportunityCollectionInstance.allBySiteIdAndStatus('', 'IN_PROGRESS')) - .to.be.rejectedWith('SiteId is required'); - }); - - it('throws an error if status is not provided', async () => { - await expect(opportunityCollectionInstance.allBySiteIdAndStatus('site67890', '')) - .to.be.rejectedWith('Status is required'); - }); - }); -}); diff --git a/packages/spacecat-shared-data-access/test/unit/v2/models/opportunity/opportunity.collection.test.js b/packages/spacecat-shared-data-access/test/unit/v2/models/opportunity/opportunity.collection.test.js new file mode 100755 index 00000000..71d8cb3b --- /dev/null +++ b/packages/spacecat-shared-data-access/test/unit/v2/models/opportunity/opportunity.collection.test.js @@ -0,0 +1,98 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-env mocha */ + +import { expect, use as chaiUse } from 'chai'; +import { Entity } from 'electrodb'; +import { spy, stub } from 'sinon'; +import chaiAsPromised from 'chai-as-promised'; + +import Opportunity from '../../../../../src/v2/models/opportunity/opportunity.model.js'; +import OpportunityCollection from '../../../../../src/v2/models/opportunity/opportunity.collection.js'; +import OpportunitySchema from '../../../../../src/v2/models/opportunity/opportunity.schema.js'; + +chaiUse(chaiAsPromised); + +const opportunityEntity = new Entity(OpportunitySchema); + +const mockElectroService = { + entities: { + opportunity: { + model: { + name: 'opportunity', + indexes: [], + schema: opportunityEntity.model.schema, + original: { + references: {}, + }, + }, + query: { + bySiteId: stub(), + bySiteIdAndStatus: stub(), + }, + put: stub(), + }, + }, +}; + +// OpportunityCollection Unit Tests +describe('OpportunityCollection', () => { + let opportunityCollectionInstance; + let mockLogger; + let mockEntityRegistry; + let mockOpportunityModel; + + const mockRecord = { + opportunityId: 'op12345', + siteId: 'site67890', + data: { + foo: 'bar', + bing: 'batz', + }, + }; + + beforeEach(() => { + mockLogger = { + error: spy(), + warn: spy(), + }; + + mockEntityRegistry = { + getCollection: stub(), + }; + + mockOpportunityModel = new Opportunity( + mockElectroService, + mockEntityRegistry, + mockRecord, + mockLogger, + ); + + opportunityCollectionInstance = new OpportunityCollection( + mockElectroService, + mockEntityRegistry, + mockLogger, + ); + }); + + describe('constructor', () => { + it('initializes the OpportunityCollection instance correctly', () => { + expect(opportunityCollectionInstance).to.be.an('object'); + expect(opportunityCollectionInstance.electroService).to.equal(mockElectroService); + expect(opportunityCollectionInstance.entityRegistry).to.equal(mockEntityRegistry); + expect(opportunityCollectionInstance.log).to.equal(mockLogger); + + expect(mockOpportunityModel).to.be.an('object'); + }); + }); +}); diff --git a/packages/spacecat-shared-data-access/test/unit/v2/models/opportunity.model.test.js b/packages/spacecat-shared-data-access/test/unit/v2/models/opportunity/opportunity.model.test.js similarity index 93% rename from packages/spacecat-shared-data-access/test/unit/v2/models/opportunity.model.test.js rename to packages/spacecat-shared-data-access/test/unit/v2/models/opportunity/opportunity.model.test.js index caad6e85..3ec74c7f 100755 --- a/packages/spacecat-shared-data-access/test/unit/v2/models/opportunity.model.test.js +++ b/packages/spacecat-shared-data-access/test/unit/v2/models/opportunity/opportunity.model.test.js @@ -17,8 +17,8 @@ import { Entity } from 'electrodb'; import { spy, stub } from 'sinon'; import chaiAsPromised from 'chai-as-promised'; -import Opportunity from '../../../../src/v2/models/opportunity/opportunity.model.js'; -import OpportunitySchema from '../../../../src/v2/models/opportunity/opportunity.schema.js'; +import Opportunity from '../../../../../src/v2/models/opportunity/opportunity.model.js'; +import OpportunitySchema from '../../../../../src/v2/models/opportunity/opportunity.schema.js'; chaiUse(chaiAsPromised); @@ -51,7 +51,7 @@ const mockElectroService = { describe('Opportunity', () => { let opportunityInstance; - let mockModelFactory; + let mockEntityRegistry; let mockLogger; const mockRecord = { @@ -72,7 +72,7 @@ describe('Opportunity', () => { }; beforeEach(() => { - mockModelFactory = { + mockEntityRegistry = { getCollection: stub(), }; @@ -82,7 +82,7 @@ describe('Opportunity', () => { opportunityInstance = new Opportunity( mockElectroService, - mockModelFactory, + mockEntityRegistry, mockRecord, mockLogger, ); @@ -100,11 +100,11 @@ describe('Opportunity', () => { const mockSuggestionCollection = { createMany: stub().returns(Promise.resolve({ id: 'suggestion-1' })), }; - mockModelFactory.getCollection.withArgs('SuggestionCollection').returns(mockSuggestionCollection); + mockEntityRegistry.getCollection.withArgs('SuggestionCollection').returns(mockSuggestionCollection); const suggestion = await opportunityInstance.addSuggestions([{ text: 'Suggestion text' }]); expect(suggestion).to.deep.equal({ id: 'suggestion-1' }); - expect(mockModelFactory.getCollection.calledOnceWith('SuggestionCollection')).to.be.true; + expect(mockEntityRegistry.getCollection.calledWith('SuggestionCollection')).to.be.true; expect(mockSuggestionCollection.createMany.calledOnceWith([{ text: 'Suggestion text', opportunityId: 'op12345' }])).to.be.true; }); }); diff --git a/packages/spacecat-shared-data-access/test/unit/v2/models/organization/organization.collection.test.js b/packages/spacecat-shared-data-access/test/unit/v2/models/organization/organization.collection.test.js new file mode 100755 index 00000000..0edef7f2 --- /dev/null +++ b/packages/spacecat-shared-data-access/test/unit/v2/models/organization/organization.collection.test.js @@ -0,0 +1,98 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-env mocha */ + +import { expect, use as chaiUse } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { Entity } from 'electrodb'; +import { spy, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; + +import OrganizationCollection from '../../../../../src/v2/models/organization/organization.collection.js'; +import Organization from '../../../../../src/v2/models/organization/organization.model.js'; +import OrganizationSchema from '../../../../../src/v2/models/organization/organization.schema.js'; + +chaiUse(chaiAsPromised); +chaiUse(sinonChai); + +const { attributes } = new Entity(OrganizationSchema).model.schema; + +let mockElectroService; + +describe('OrganizationCollection', () => { + let instance; + let mockOrganizationModel; + let mockLogger; + let mockEntityRegistry; + + const mockRecord = { + organizationId: 's12345', + }; + + beforeEach(() => { + mockLogger = { + error: spy(), + warn: spy(), + }; + + mockEntityRegistry = { + getCollection: stub(), + }; + + mockElectroService = { + entities: { + organization: { + model: { + name: 'organization', + schema: { attributes }, + original: { + references: {}, + }, + indexes: { + primary: { + pk: { + field: 'pk', + composite: ['organizationId'], + }, + }, + }, + }, + }, + }, + }; + + mockOrganizationModel = new Organization( + mockElectroService, + mockEntityRegistry, + mockRecord, + mockLogger, + ); + + instance = new OrganizationCollection( + mockElectroService, + mockEntityRegistry, + mockLogger, + ); + }); + + describe('constructor', () => { + it('initializes the OrganizationCollection instance correctly', () => { + expect(instance).to.be.an('object'); + expect(instance.electroService).to.equal(mockElectroService); + expect(instance.entityRegistry).to.equal(mockEntityRegistry); + expect(instance.log).to.equal(mockLogger); + + expect(mockOrganizationModel).to.be.an('object'); + }); + }); +}); diff --git a/packages/spacecat-shared-data-access/test/unit/v2/models/site-candidate/site-candidate.collection.test.js b/packages/spacecat-shared-data-access/test/unit/v2/models/site-candidate/site-candidate.collection.test.js new file mode 100755 index 00000000..ba0fd97d --- /dev/null +++ b/packages/spacecat-shared-data-access/test/unit/v2/models/site-candidate/site-candidate.collection.test.js @@ -0,0 +1,102 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-env mocha */ + +import { expect, use as chaiUse } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { Entity } from 'electrodb'; +import { spy, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; + +import SiteCandidateCollection from '../../../../../src/v2/models/site-candidate/site-candidate.collection.js'; +import SiteCandidate from '../../../../../src/v2/models/site-candidate/site-candidate.model.js'; +import SiteCandidateSchema from '../../../../../src/v2/models/site-candidate/site-candidate.schema.js'; + +chaiUse(chaiAsPromised); +chaiUse(sinonChai); + +const { attributes } = new Entity(SiteCandidateSchema).model.schema; + +let mockElectroService; + +describe('SiteCandidateCollection', () => { + let instance; + let mockSiteCandidateModel; + let mockLogger; + let mockEntityRegistry; + + const mockRecord = { + siteCandidateId: 's12345', + siteId: 's67890', + }; + + beforeEach(() => { + mockLogger = { + error: spy(), + warn: spy(), + }; + + mockEntityRegistry = { + getCollection: stub(), + }; + + mockElectroService = { + entities: { + siteCandidate: { + model: { + name: 'siteCandidate', + schema: { attributes }, + original: { + references: {}, + }, + indexes: { + primary: { + pk: { + field: 'pk', + composite: ['siteCandidateId'], + }, + }, + }, + }, + delete: stub().returns({ + go: stub().resolves({}), + }), + }, + }, + }; + + mockSiteCandidateModel = new SiteCandidate( + mockElectroService, + mockEntityRegistry, + mockRecord, + mockLogger, + ); + + instance = new SiteCandidateCollection( + mockElectroService, + mockEntityRegistry, + mockLogger, + ); + }); + + describe('constructor', () => { + it('initializes the SiteCandidateCollection instance correctly', () => { + expect(instance).to.be.an('object'); + expect(instance.electroService).to.equal(mockElectroService); + expect(instance.entityRegistry).to.equal(mockEntityRegistry); + expect(instance.log).to.equal(mockLogger); + + expect(mockSiteCandidateModel).to.be.an('object'); + }); + }); +}); diff --git a/packages/spacecat-shared-data-access/test/unit/v2/models/site-top-page/site-top-page.collection.test.js b/packages/spacecat-shared-data-access/test/unit/v2/models/site-top-page/site-top-page.collection.test.js new file mode 100755 index 00000000..c5294a13 --- /dev/null +++ b/packages/spacecat-shared-data-access/test/unit/v2/models/site-top-page/site-top-page.collection.test.js @@ -0,0 +1,137 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-env mocha */ + +import { expect, use as chaiUse } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { Entity } from 'electrodb'; +import { spy, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; + +import SiteTopPageCollection from '../../../../../src/v2/models/site-top-page/site-top-page.collection.js'; +import SiteTopPage from '../../../../../src/v2/models/site-top-page/site-top-page.model.js'; +import SiteTopPageSchema from '../../../../../src/v2/models/site-top-page/site-top-page.schema.js'; + +chaiUse(chaiAsPromised); +chaiUse(sinonChai); + +const { attributes } = new Entity(SiteTopPageSchema).model.schema; + +let mockElectroService; + +describe('SiteTopPageCollection', () => { + let instance; + let mockSiteTopPageModel; + let mockLogger; + let mockEntityRegistry; + + const mockRecord = { + siteTopPageId: 's12345', + siteId: 's67890', + url: 'https://www.example.com', + traffic: 1000, + source: 'ahrefs', + geo: 'global', + topKeywords: 'keyword1', + importedAt: '2024-01-01T00:00:00.000Z', + }; + + beforeEach(() => { + mockLogger = { + error: spy(), + warn: spy(), + }; + + mockEntityRegistry = { + getCollection: stub(), + }; + + mockElectroService = { + entities: { + siteTopPage: { + model: { + name: 'siteTopPage', + schema: { attributes }, + original: { + references: {}, + }, + indexes: { + primary: { + pk: { + field: 'pk', + composite: ['siteTopPageId'], + }, + }, + }, + }, + delete: stub().returns({ + go: stub().resolves({}), + }), + }, + }, + }; + + mockSiteTopPageModel = new SiteTopPage( + mockElectroService, + mockEntityRegistry, + mockRecord, + mockLogger, + ); + + instance = new SiteTopPageCollection( + mockElectroService, + mockEntityRegistry, + mockLogger, + ); + }); + + describe('constructor', () => { + it('initializes the SiteTopPageCollection instance correctly', () => { + expect(instance).to.be.an('object'); + expect(instance.electroService).to.equal(mockElectroService); + expect(instance.entityRegistry).to.equal(mockEntityRegistry); + expect(instance.log).to.equal(mockLogger); + }); + }); + + describe('removeForSiteId', () => { + it('throws an error if siteId is not provided', async () => { + await expect(instance.removeForSiteId()).to.be.rejectedWith('SiteId is required'); + }); + + it('removes all SiteTopPages for a given siteId', async () => { + const siteId = 'site12345'; + + instance.allBySiteId = stub().resolves([mockSiteTopPageModel]); + + await instance.removeForSiteId(siteId); + + expect(instance.allBySiteId.calledOnceWith(siteId)).to.be.true; + expect(mockElectroService.entities.siteTopPage.delete.calledOnceWith([{ siteTopPageId: 's12345' }])) + .to.be.true; + }); + + it('remove all SiteTopPages for a given siteId, source and geo', async () => { + const siteId = 'site12345'; + const source = 'ahrefs'; + const geo = 'global'; + + instance.allBySiteIdAndSourceAndGeo = stub().resolves([mockSiteTopPageModel]); + + await instance.removeForSiteId(siteId, source, geo); + + expect(instance.allBySiteIdAndSourceAndGeo).to.have.been.calledOnceWith(siteId, source, geo); + expect(mockElectroService.entities.siteTopPage.delete).to.have.been.calledOnceWith([{ siteTopPageId: 's12345' }]); + }); + }); +}); diff --git a/packages/spacecat-shared-data-access/test/unit/v2/models/site/site.collection.test.js b/packages/spacecat-shared-data-access/test/unit/v2/models/site/site.collection.test.js new file mode 100755 index 00000000..82b27794 --- /dev/null +++ b/packages/spacecat-shared-data-access/test/unit/v2/models/site/site.collection.test.js @@ -0,0 +1,109 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-env mocha */ + +import { expect, use as chaiUse } from 'chai'; +import chaiAsPromised from 'chai-as-promised'; +import { Entity } from 'electrodb'; +import { spy, stub } from 'sinon'; +import sinonChai from 'sinon-chai'; + +import SiteCollection from '../../../../../src/v2/models/site/site.collection.js'; +import Site from '../../../../../src/v2/models/site/site.model.js'; +import SiteSchema from '../../../../../src/v2/models/site/site.schema.js'; + +chaiUse(chaiAsPromised); +chaiUse(sinonChai); + +const { attributes } = new Entity(SiteSchema).model.schema; + +let mockElectroService; + +describe('SiteCollection', () => { + let instance; + let mockSiteModel; + let mockLogger; + let mockEntityRegistry; + + const mockRecord = { + siteId: 's12345', + }; + + beforeEach(() => { + mockLogger = { + error: spy(), + warn: spy(), + }; + + mockEntityRegistry = { + getCollection: stub(), + }; + + mockElectroService = { + entities: { + site: { + model: { + name: 'site', + schema: { attributes }, + original: { + references: {}, + }, + indexes: { + primary: { + pk: { + field: 'pk', + composite: ['siteId'], + }, + }, + }, + }, + }, + }, + }; + + mockSiteModel = new Site( + mockElectroService, + mockEntityRegistry, + mockRecord, + mockLogger, + ); + + instance = new SiteCollection( + mockElectroService, + mockEntityRegistry, + mockLogger, + ); + }); + + describe('constructor', () => { + it('initializes the SiteCollection instance correctly', () => { + expect(instance).to.be.an('object'); + expect(instance.electroService).to.equal(mockElectroService); + expect(instance.entityRegistry).to.equal(mockEntityRegistry); + expect(instance.log).to.equal(mockLogger); + + expect(mockSiteModel).to.be.an('object'); + }); + }); + + describe('allSitesToAudit', () => { + it('returns all sites to audit', async () => { + instance.all = stub().resolves([{ getId: () => 's12345' }]); + + const result = await instance.allSitesToAudit(); + + expect(result).to.deep.equal(['s12345']); + expect(instance.all).to.have.been.calledOnceWithExactly({ attributes: ['siteId'] }); + }); + }); +}); diff --git a/packages/spacecat-shared-data-access/test/unit/v2/models/suggestion.collection.test.js b/packages/spacecat-shared-data-access/test/unit/v2/models/suggestion.collection.test.js deleted file mode 100755 index 8f06f337..00000000 --- a/packages/spacecat-shared-data-access/test/unit/v2/models/suggestion.collection.test.js +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright 2024 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -/* eslint-env mocha */ - -import { expect, use as chaiUse } from 'chai'; -import { Entity } from 'electrodb'; -import { spy, stub } from 'sinon'; -import chaiAsPromised from 'chai-as-promised'; - -import SuggestionCollection from '../../../../src/v2/models/suggestion/suggestion.collection.js'; -import Suggestion from '../../../../src/v2/models/suggestion/suggestion.model.js'; -import SuggestionSchema from '../../../../src/v2/models/suggestion/suggestion.schema.js'; - -chaiUse(chaiAsPromised); - -const { attributes } = new Entity(SuggestionSchema).model.schema; - -const mockElectroService = { - entities: { - suggestion: { - model: { - name: 'suggestion', - schema: { attributes }, - original: { - references: {}, - }, - indexes: { - primary: { - pk: { - field: 'pk', - composite: ['suggestionId'], - }, - }, - }, - }, - query: { - byOpportunityId: stub(), - byOpportunityIdAndStatus: stub(), - }, - put: stub().returns({ - go: stub().resolves({}), - }), - patch: stub().returns({ - set: stub(), - }), - }, - }, -}; - -// SuggestionCollection Unit Tests -describe('SuggestionCollection', () => { - let suggestionCollectionInstance; - let mockLogger; - let mockModelFactory; - - const mockRecord = { - suggestionId: 's12345', - opportunityId: 'op67890', - data: { - title: 'Test Suggestion', - description: 'This is a test suggestion.', - }, - }; - const mockSuggestionModel = new Suggestion( - mockElectroService, - mockModelFactory, - mockRecord, - mockLogger, - ); - - beforeEach(() => { - mockLogger = { - error: spy(), - warn: spy(), - }; - - mockModelFactory = { - getCollection: stub(), - }; - - suggestionCollectionInstance = new SuggestionCollection( - mockElectroService, - mockModelFactory, - mockLogger, - ); - }); - - describe('constructor', () => { - it('initializes the SuggestionCollection instance correctly', () => { - expect(suggestionCollectionInstance).to.be.an('object'); - expect(suggestionCollectionInstance.electroService).to.equal(mockElectroService); - expect(suggestionCollectionInstance.modelFactory).to.equal(mockModelFactory); - expect(suggestionCollectionInstance.log).to.equal(mockLogger); - }); - }); - - describe('allByOpportunityId', () => { - it('returns the suggestions by opportunity', async () => { - const mockFindResults = { data: [mockRecord] }; - mockElectroService.entities.suggestion.query.byOpportunityId.returns( - { go: () => Promise.resolve(mockFindResults) }, - ); - - const results = await suggestionCollectionInstance.allByOpportunityId('op67890'); - expect(results).to.be.an('array').that.has.length(1); - expect(results[0]).to.be.instanceOf(Suggestion); - expect(results[0].record).to.deep.include(mockSuggestionModel.record); - }); - - it('returns an empty array if no suggestions exist for the given opportunity ID', async () => { - mockElectroService.entities.suggestion.query.byOpportunityId.returns( - { go: () => Promise.resolve([]) }, - ); - - const results = await suggestionCollectionInstance.allByOpportunityId('op67890'); - expect(results).to.be.an('array').that.is.empty; - }); - - it('throws an error if opportunityId is not provided', async () => { - await expect(suggestionCollectionInstance.allByOpportunityId('')) - .to.be.rejectedWith('OpportunityId is required'); - }); - }); - - describe('allByOpportunityIdAndStatus', () => { - it('returns the suggestions by opportunity and status', async () => { - const mockFindResults = { data: [mockRecord] }; - mockElectroService.entities.suggestion.query.byOpportunityIdAndStatus.returns( - { go: () => Promise.resolve(mockFindResults) }, - ); - - const results = await suggestionCollectionInstance.allByOpportunityIdAndStatus('op67890', 'NEW'); - expect(results).to.be.an('array').that.has.length(1); - expect(results[0]).to.be.instanceOf(Suggestion); - expect(results[0].record).to.deep.include(mockSuggestionModel.record); - }); - - it('returns an empty array if no suggestions exist for the given opportunity ID and status', async () => { - mockElectroService.entities.suggestion.query.byOpportunityIdAndStatus.returns( - { go: () => Promise.resolve([]) }, - ); - - const results = await suggestionCollectionInstance.allByOpportunityIdAndStatus('op67890', 'NEW'); - expect(results).to.be.an('array').that.is.empty; - }); - - it('throws an error if opportunityId is not provided', async () => { - await expect(suggestionCollectionInstance.allByOpportunityIdAndStatus('', 'NEW')) - .to.be.rejectedWith('OpportunityId is required'); - }); - - it('throws an error if status is not provided', async () => { - await expect(suggestionCollectionInstance.allByOpportunityIdAndStatus('op67890', '')) - .to.be.rejectedWith('Status is required'); - }); - }); - - describe('bulkUpdateStatus', () => { - it('updates the status of multiple suggestions', async () => { - const mockSuggestions = [mockSuggestionModel]; - const mockStatus = 'NEW'; - - await suggestionCollectionInstance.bulkUpdateStatus(mockSuggestions, mockStatus); - - expect(mockElectroService.entities.suggestion.put.calledOnce).to.be.true; - expect(mockElectroService.entities.suggestion.put.firstCall.args[0]).to.deep.equal([{ - suggestionId: 's12345', - opportunityId: 'op67890', - data: { - title: 'Test Suggestion', - description: 'This is a test suggestion.', - }, - status: 'NEW', - }]); - }); - - it('throws an error if suggestions is not an array', async () => { - await expect(suggestionCollectionInstance.bulkUpdateStatus({}, 'NEW')) - .to.be.rejectedWith('Suggestions must be an array'); - }); - - it('throws an error if status is not provided', async () => { - await expect(suggestionCollectionInstance.bulkUpdateStatus([mockSuggestionModel], 'foo')) - .to.be.rejectedWith('Invalid status: foo. Must be one of: NEW, APPROVED, SKIPPED, FIXED, ERROR'); - }); - }); -}); diff --git a/packages/spacecat-shared-data-access/test/unit/v2/models/suggestion/suggestion.collection.test.js b/packages/spacecat-shared-data-access/test/unit/v2/models/suggestion/suggestion.collection.test.js new file mode 100755 index 00000000..d4ba3530 --- /dev/null +++ b/packages/spacecat-shared-data-access/test/unit/v2/models/suggestion/suggestion.collection.test.js @@ -0,0 +1,140 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-env mocha */ + +import { expect, use as chaiUse } from 'chai'; +import { Entity } from 'electrodb'; +import { spy, stub } from 'sinon'; +import chaiAsPromised from 'chai-as-promised'; + +import SuggestionCollection from '../../../../../src/v2/models/suggestion/suggestion.collection.js'; +import Suggestion from '../../../../../src/v2/models/suggestion/suggestion.model.js'; +import SuggestionSchema from '../../../../../src/v2/models/suggestion/suggestion.schema.js'; + +chaiUse(chaiAsPromised); + +const { attributes } = new Entity(SuggestionSchema).model.schema; + +const mockElectroService = { + entities: { + suggestion: { + model: { + name: 'suggestion', + schema: { attributes }, + original: { + references: {}, + }, + indexes: { + primary: { + pk: { + field: 'pk', + composite: ['suggestionId'], + }, + }, + }, + }, + query: { + byOpportunityId: stub(), + byOpportunityIdAndStatus: stub(), + }, + put: stub().returns({ + go: stub().resolves({}), + }), + patch: stub().returns({ + set: stub(), + }), + }, + }, +}; + +// SuggestionCollection Unit Tests +describe('SuggestionCollection', () => { + let instance; + let mockSuggestionModel; + let mockLogger; + let mockEntityRegistry; + + const mockRecord = { + suggestionId: 's12345', + opportunityId: 'op67890', + data: { + title: 'Test Suggestion', + description: 'This is a test suggestion.', + }, + }; + + beforeEach(() => { + mockLogger = { + error: spy(), + warn: spy(), + }; + + mockEntityRegistry = { + getCollection: stub(), + }; + + mockSuggestionModel = new Suggestion( + mockElectroService, + mockEntityRegistry, + mockRecord, + mockLogger, + ); + + instance = new SuggestionCollection( + mockElectroService, + mockEntityRegistry, + mockLogger, + ); + }); + + describe('constructor', () => { + it('initializes the SuggestionCollection instance correctly', () => { + expect(instance).to.be.an('object'); + expect(instance.electroService).to.equal(mockElectroService); + expect(instance.entityRegistry).to.equal(mockEntityRegistry); + expect(instance.log).to.equal(mockLogger); + + expect(mockSuggestionModel).to.be.an('object'); + }); + }); + + describe('bulkUpdateStatus', () => { + it('updates the status of multiple suggestions', async () => { + const mockSuggestions = [mockSuggestionModel]; + const mockStatus = 'NEW'; + + await instance.bulkUpdateStatus(mockSuggestions, mockStatus); + + expect(mockElectroService.entities.suggestion.put.calledOnce).to.be.true; + expect(mockElectroService.entities.suggestion.put.firstCall.args[0]).to.deep.equal([{ + suggestionId: 's12345', + opportunityId: 'op67890', + data: { + title: 'Test Suggestion', + description: 'This is a test suggestion.', + }, + status: 'NEW', + }]); + }); + + it('throws an error if suggestions is not an array', async () => { + await expect(instance.bulkUpdateStatus({}, 'NEW')) + .to.be.rejectedWith('Suggestions must be an array'); + }); + + it('throws an error if status is not provided', async () => { + await expect(instance.bulkUpdateStatus([mockSuggestionModel], 'foo')) + .to.be.rejectedWith('Invalid status: foo. Must be one of: NEW, APPROVED, SKIPPED, FIXED, ERROR'); + }); + }); +}); diff --git a/packages/spacecat-shared-data-access/test/unit/v2/models/suggestion.model.test.js b/packages/spacecat-shared-data-access/test/unit/v2/models/suggestion/suggestion.model.test.js similarity index 96% rename from packages/spacecat-shared-data-access/test/unit/v2/models/suggestion.model.test.js rename to packages/spacecat-shared-data-access/test/unit/v2/models/suggestion/suggestion.model.test.js index b6abf60d..622f7590 100644 --- a/packages/spacecat-shared-data-access/test/unit/v2/models/suggestion.model.test.js +++ b/packages/spacecat-shared-data-access/test/unit/v2/models/suggestion/suggestion.model.test.js @@ -17,8 +17,8 @@ import { Entity } from 'electrodb'; import { spy, stub } from 'sinon'; import chaiAsPromised from 'chai-as-promised'; -import Suggestion from '../../../../src/v2/models/suggestion/suggestion.model.js'; -import SuggestionSchema from '../../../../src/v2/models/suggestion/suggestion.schema.js'; +import Suggestion from '../../../../../src/v2/models/suggestion/suggestion.model.js'; +import SuggestionSchema from '../../../../../src/v2/models/suggestion/suggestion.schema.js'; chaiUse(chaiAsPromised); diff --git a/packages/spacecat-shared-data-access/test/unit/v2/util/guards.test.js b/packages/spacecat-shared-data-access/test/unit/v2/util/guards.test.js index 9f67c1a8..f6a6a93f 100644 --- a/packages/spacecat-shared-data-access/test/unit/v2/util/guards.test.js +++ b/packages/spacecat-shared-data-access/test/unit/v2/util/guards.test.js @@ -18,6 +18,7 @@ import chaiAsPromised from 'chai-as-promised'; import { guardAny, guardArray, + guardBoolean, guardEnum, guardId, guardMap, @@ -70,13 +71,13 @@ describe('Guards', () => { }); it('allows specifying type as object', () => { - expect(() => guardArray('testProperty', [{ key: 'value' }, { anotherKey: 'anotherValue' }], 'TestEntity', 'object')) + expect(() => guardArray('testProperty', [{ key: 'value' }, { anotherKey: 'anotherValue' }], 'TestEntity', 'map')) .not.to.throw(); }); it('throws an error if array contains wrong type when expecting objects', () => { - expect(() => guardArray('testProperty', [{ key: 'value' }, 'notAnObject'], 'TestEntity', 'object')) - .to.throw('Validation failed in TestEntity: testProperty must contain items of type object'); + expect(() => guardArray('testProperty', [{ key: 'value' }, 'notAnObject'], 'TestEntity', 'map')) + .to.throw('Validation failed in TestEntity: testProperty must contain items of type map'); }); it('throws an error if an unsupported type is specified', () => { @@ -85,6 +86,43 @@ describe('Guards', () => { }); }); + describe('guardBoolean', () => { + it('throws an error if value is not a boolean', () => { + expect(() => guardBoolean('testProperty', 'notABoolean', 'TestEntity')) + .to.throw('Validation failed in TestEntity: testProperty must be a boolean'); + }); + + it('does not throw if value is a boolean', () => { + expect(() => guardBoolean('testProperty', true, 'TestEntity')) + .not.to.throw(); + }); + + it('does not throw if value is null and nullable is true', () => { + expect(() => guardBoolean('testProperty', null, 'TestEntity', true)) + .not.to.throw(); + }); + + it('does not throw if value is undefined and nullable is true', () => { + expect(() => guardBoolean('testProperty', undefined, 'TestEntity', true)) + .not.to.throw(); + }); + + it('throws an error if value is undefined and nullable is false', () => { + expect(() => guardBoolean('testProperty', undefined, 'TestEntity', false)) + .to.throw('Validation failed in TestEntity: testProperty must be a boolean'); + }); + + it('throws an error if value is null and nullable is false', () => { + expect(() => guardBoolean('testProperty', null, 'TestEntity', false)) + .to.throw('Validation failed in TestEntity: testProperty must be a boolean'); + }); + + it('throws an error if value is an empty string and nullable is false', () => { + expect(() => guardBoolean('testProperty', '', 'TestEntity', false)) + .to.throw('Validation failed in TestEntity: testProperty must be a boolean'); + }); + }); + describe('guardSet', () => { it('throws an error if value is not an array', () => { expect(() => guardSet('testProperty', 'notArray', 'TestEntity')) @@ -139,8 +177,8 @@ describe('Guards', () => { }); it('throws an error if array contains wrong type when expecting objects', () => { - expect(() => guardSet('testProperty', [{}, { a: 'b' }, 3], 'TestEntity', 'object')) - .to.throw('Validation failed in TestEntity: testProperty must contain items of type object'); + expect(() => guardSet('testProperty', [{}, { a: 'b' }, 3], 'TestEntity', 'map')) + .to.throw('Validation failed in TestEntity: testProperty must contain items of type map'); }); it('throws an error if array contains wrong type when expecting strings', () => { diff --git a/packages/spacecat-shared-data-access/test/unit/v2/util/patcher.test.js b/packages/spacecat-shared-data-access/test/unit/v2/util/patcher.test.js index 31673b14..ee62b11e 100755 --- a/packages/spacecat-shared-data-access/test/unit/v2/util/patcher.test.js +++ b/packages/spacecat-shared-data-access/test/unit/v2/util/patcher.test.js @@ -12,6 +12,8 @@ /* eslint-env mocha */ +import { isIsoDate } from '@adobe/spacecat-shared-utils'; + import { expect, use as chaiUse } from 'chai'; import sinon from 'sinon'; import chaiAsPromised from 'chai-as-promised'; @@ -31,15 +33,16 @@ describe('Patcher', () => { name: 'TestEntity', schema: { attributes: { - name: { type: 'string' }, - age: { type: 'number' }, - tags: { type: 'set', items: { type: 'string' } }, - status: { type: 'enum', enumArray: ['active', 'inactive'] }, - referenceId: { type: 'string' }, - metadata: { type: 'map' }, - profile: { type: 'any' }, - nickNames: { type: 'list', items: { type: 'string' } }, - settings: { type: 'any', required: true }, + name: { type: 'string', name: 'name' }, + age: { type: 'number', name: 'age' }, + tags: { type: 'set', name: 'tags', items: { type: 'string' } }, + status: { type: 'enum', name: 'status', enumArray: ['active', 'inactive'] }, + referenceId: { type: 'string', name: 'referenceId' }, + metadata: { type: 'map', name: 'metadata' }, + profile: { type: 'any', name: 'profile' }, + nickNames: { type: 'list', name: 'nickNames', items: { type: 'string' } }, + settings: { type: 'any', name: 'settings', required: true }, + isActive: { type: 'boolean', name: 'isActive' }, }, }, indexes: { @@ -50,6 +53,7 @@ describe('Patcher', () => { }, }, patch: sinon.stub().returns({ + composite: sinon.stub().returnsThis(), set: sinon.stub().returnsThis(), go: sinon.stub().resolves(), }), @@ -73,7 +77,7 @@ describe('Patcher', () => { it('patches a string value', () => { patcher.patchValue('name', 'UpdatedName'); - expect(mockEntity.patch().set.calledWith({ name: 'UpdatedName', age: 25 })).to.be.true; + expect(mockEntity.patch().set.calledWith({ name: 'UpdatedName' })).to.be.true; expect(mockRecord.name).to.equal('UpdatedName'); }); @@ -90,7 +94,7 @@ describe('Patcher', () => { it('throws error for unsupported enum value', () => { expect(() => patcher.patchValue('status', 'unknown')) - .to.throw('Validation failed in testentity: status must be one of active,inactive'); + .to.throw('Validation failed in testEntity: status must be one of active,inactive'); }); it('patches a reference id with proper validation', () => { @@ -100,7 +104,7 @@ describe('Patcher', () => { it('throws error for non-existent property', () => { expect(() => patcher.patchValue('nonExistent', 'value')) - .to.throw('Property nonExistent does not exist on entity testentity.'); + .to.throw('Property nonExistent does not exist on entity testEntity.'); }); it('tracks updates', () => { @@ -116,7 +120,7 @@ describe('Patcher', () => { await patcher.save(); expect(mockEntity.patch().go.calledOnce).to.be.true; - expect(mockRecord.updatedAt).to.be.a('number'); + expect(isIsoDate(mockRecord.updatedAt)).to.be.true; }); it('does not save if there are no updates', async () => { @@ -137,7 +141,7 @@ describe('Patcher', () => { it('throws error for invalid set attribute', () => { expect(() => patcher.patchValue('tags', ['tag1', 123])) - .to.throw('Validation failed in testentity: tags must contain items of type string'); + .to.throw('Validation failed in testEntity: tags must contain items of type string'); }); it('validates and patches a number attribute', () => { @@ -147,7 +151,7 @@ describe('Patcher', () => { it('throws error for invalid number attribute', () => { expect(() => patcher.patchValue('age', 'notANumber')) - .to.throw('Validation failed in testentity: age must be a number'); + .to.throw('Validation failed in testEntity: age must be a number'); }); it('validates and patch a map attribute', () => { @@ -157,7 +161,7 @@ describe('Patcher', () => { it('throws error for invalid map attribute', () => { expect(() => patcher.patchValue('metadata', 'notAMap')) - .to.throw('Validation failed in testentity: metadata must be an object'); + .to.throw('Validation failed in testEntity: metadata must be an object'); }); it('validates and patches an any attribute', () => { @@ -167,12 +171,17 @@ describe('Patcher', () => { it('throws error for undefined any attribute', () => { expect(() => patcher.patchValue('settings', undefined)) - .to.throw('Validation failed in testentity: settings is required'); + .to.throw('Validation failed in testEntity: settings is required'); }); it('throws error for null any attribute', () => { expect(() => patcher.patchValue('settings', null)) - .to.throw('Validation failed in testentity: settings is required'); + .to.throw('Validation failed in testEntity: settings is required'); + }); + + it('validates and patches a boolean attribute', () => { + patcher.patchValue('isActive', true); + expect(mockRecord.isActive).to.be.true; }); it('validates and patches a list attribute', () => { @@ -182,11 +191,11 @@ describe('Patcher', () => { it('throws error for invalid list attribute', () => { expect(() => patcher.patchValue('nickNames', 'notAList')) - .to.throw('Validation failed in testentity: nickNames must be an array'); + .to.throw('Validation failed in testEntity: nickNames must be an array'); }); it('throws error for invalid list attribute items', () => { expect(() => patcher.patchValue('nickNames', ['name1', 123])) - .to.throw('Validation failed in testentity: nickNames must contain items of type string'); + .to.throw('Validation failed in testEntity: nickNames must contain items of type string'); }); }); diff --git a/packages/spacecat-shared-data-access/test/unit/v2/util/util.test.js b/packages/spacecat-shared-data-access/test/unit/v2/util/util.test.js new file mode 100644 index 00000000..72768cb0 --- /dev/null +++ b/packages/spacecat-shared-data-access/test/unit/v2/util/util.test.js @@ -0,0 +1,201 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-env mocha */ + +// utils.test.js +// This suite tests all utility functions from the provided utils file. +// Requires Mocha for tests, Chai for assertions, and Sinon for spying/stubbing. + +import { expect } from 'chai'; +import { + capitalize, + collectionNameToEntityName, + decapitalize, + entityNameToCollectionName, + entityNameToIdName, + entityNameToAllPKValue, + entityNameToReferenceMethodName, + idNameToEntityName, + incrementVersion, + keyNamesToIndexName, + modelNameToEntityName, + sanitizeIdAndAuditFields, + sanitizeTimestamps, +} from '../../../../src/v2/util/util.js'; + +describe('Utilities', () => { + describe('capitalize', () => { + it('Convert first character to uppercase', () => { + expect(capitalize('hello')).to.equal('Hello'); + }); + + it('Return empty string if input empty', () => { + expect(capitalize('')).to.equal(''); + }); + + it('Not alter already capitalized strings', () => { + expect(capitalize('Hello')).to.equal('Hello'); + }); + }); + + describe('decapitalize', () => { + it('Convert first character to lowercase', () => { + expect(decapitalize('Hello')).to.equal('hello'); + }); + + it('Return empty string if input empty', () => { + expect(decapitalize('')).to.equal(''); + }); + + it('Not alter already lowercased strings', () => { + expect(decapitalize('hello')).to.equal('hello'); + }); + }); + + describe('collectionNameToEntityName', () => { + it('Remove "Collection" suffix from a given string', () => { + expect(collectionNameToEntityName('UserCollection')).to.equal('User'); + }); + + it('Return the original string if no "Collection" present', () => { + expect(collectionNameToEntityName('User')).to.equal('User'); + }); + }); + + describe('entityNameToCollectionName', () => { + it('Append "Collection" to a singular form of entity name', () => { + expect(entityNameToCollectionName('User')).to.equal('UserCollection'); + }); + + it('Handle plural entity names by converting to singular first', () => { + expect(entityNameToCollectionName('Users')).to.equal('UserCollection'); + }); + }); + + describe('entityNameToIdName', () => { + it('Convert entityName to a lowercaseId format', () => { + expect(entityNameToIdName('User')).to.equal('userId'); + }); + + it('Handle already lowercase entityName', () => { + expect(entityNameToIdName('user')).to.equal('userId'); + }); + }); + + describe('entityNameToAllPKValue', () => { + it('Convert entity name to ALL_ upper plural form', () => { + expect(entityNameToAllPKValue('User')).to.equal('ALL_USERS'); + }); + + it('Handle already plural entity name', () => { + expect(entityNameToAllPKValue('Users')).to.equal('ALL_USERS'); + }); + }); + + describe('entityNameToReferenceMethodName', () => { + it('Generate "get" + pluralized capitalized target if type is has_many', () => { + expect(entityNameToReferenceMethodName('user', 'has_many')).to.equal('getUsers'); + }); + + it('Generate "get" + singular capitalized target if type is not has_many', () => { + expect(entityNameToReferenceMethodName('users', 'has_one')).to.equal('getUser'); + }); + + it('Handle already capitalized target', () => { + expect(entityNameToReferenceMethodName('User', 'has_many')).to.equal('getUsers'); + }); + }); + + describe('idNameToEntityName', () => { + it('Convert idName to singular, capitalized entityName', () => { + expect(idNameToEntityName('userId')).to.equal('User'); + }); + + it('Handle plural-like idNames', () => { + expect(idNameToEntityName('usersId')).to.equal('User'); + }); + }); + + describe('incrementVersion', () => { + it('Increment version by 1 if it is an integer', () => { + expect(incrementVersion(1)).to.equal(2); + }); + + it('Return 1 if version is not an integer', () => { + expect(incrementVersion('not-a-number')).to.equal(1); + }); + + it('Return 1 if version is undefined', () => { + expect(incrementVersion(undefined)).to.equal(1); + }); + }); + + describe('keyNamesToIndexName', () => { + it('Create index name by capitalizing and joining key names', () => { + expect(keyNamesToIndexName(['user', 'status'])).to.equal('byUserAndStatus'); + }); + + it('Handle single key name', () => { + expect(keyNamesToIndexName(['user'])).to.equal('byUser'); + }); + }); + + describe('modelNameToEntityName', () => { + it('Decapitalize model name', () => { + expect(modelNameToEntityName('UserModel')).to.equal('userModel'); + }); + + it('Handle already lowercase', () => { + expect(modelNameToEntityName('usermodel')).to.equal('usermodel'); + }); + }); + + describe('sanitizeTimestamps', () => { + it('Remove createdAt and updatedAt fields', () => { + const data = { foo: 'bar', createdAt: 'yesterday', updatedAt: 'today' }; + expect(sanitizeTimestamps(data)).to.deep.equal({ foo: 'bar' }); + }); + + it('Return object unchanged if no timestamps present', () => { + const data = { foo: 'bar' }; + expect(sanitizeTimestamps(data)).to.deep.equal({ foo: 'bar' }); + }); + }); + + describe('sanitizeIdAndAuditFields', () => { + it('Remove entity ID and timestamps', () => { + const data = { + userId: '123', + foo: 'bar', + createdAt: 'yesterday', + updatedAt: 'today', + }; + expect(sanitizeIdAndAuditFields('User', data)).to.deep.equal({ foo: 'bar' }); + }); + + it('Handle entityName that results in different idName', () => { + const data = { + productId: 'abc', + name: 'Gadget', + createdAt: 'yesterday', + updatedAt: 'today', + }; + expect(sanitizeIdAndAuditFields('Product', data)).to.deep.equal({ name: 'Gadget' }); + }); + + it('Return object unchanged if no ID or timestamps present', () => { + const data = { foo: 'bar' }; + expect(sanitizeIdAndAuditFields('User', data)).to.deep.equal({ foo: 'bar' }); + }); + }); +});