From 49263dc14f30016e6eb052df00e71bc593ee855c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dominique=20J=C3=A4ggi?= Date: Wed, 29 Nov 2023 18:44:35 +0100 Subject: [PATCH] feat: add models, access patterns and tests --- package-lock.json | 175 ++++++++++- .../docs/schema.json | 290 ++++++++++++++++++ .../spacecat-shared-data-access/package.json | 7 +- .../src/audits/accessPatterns.js | 95 ++++++ .../src/audits/index.js | 34 ++ .../src/index.d.ts | 71 +++++ .../spacecat-shared-data-access/src/index.js | 33 ++ .../src/models/audit.js | 60 ++++ .../src/models/base.js | 30 ++ .../src/models/site.js | 51 +++ .../src/sites/accessPatterns.js | 170 ++++++++++ .../src/sites/index.js | 59 ++++ .../test/audits/index.test.js | 79 +++++ .../test/index.test.js | 58 ++++ .../test/models/audit.test.js | 69 +++++ .../test/models/base.test.js | 57 ++++ .../test/models/site.test.js | 65 ++++ .../test/sites/index.test.js | 214 +++++++++++++ 18 files changed, 1612 insertions(+), 5 deletions(-) create mode 100644 packages/spacecat-shared-data-access/docs/schema.json create mode 100644 packages/spacecat-shared-data-access/src/audits/accessPatterns.js create mode 100644 packages/spacecat-shared-data-access/src/audits/index.js create mode 100644 packages/spacecat-shared-data-access/src/index.d.ts create mode 100644 packages/spacecat-shared-data-access/src/index.js create mode 100644 packages/spacecat-shared-data-access/src/models/audit.js create mode 100644 packages/spacecat-shared-data-access/src/models/base.js create mode 100644 packages/spacecat-shared-data-access/src/models/site.js create mode 100644 packages/spacecat-shared-data-access/src/sites/accessPatterns.js create mode 100644 packages/spacecat-shared-data-access/src/sites/index.js create mode 100644 packages/spacecat-shared-data-access/test/audits/index.test.js create mode 100644 packages/spacecat-shared-data-access/test/index.test.js create mode 100644 packages/spacecat-shared-data-access/test/models/audit.test.js create mode 100644 packages/spacecat-shared-data-access/test/models/base.test.js create mode 100644 packages/spacecat-shared-data-access/test/models/site.test.js create mode 100644 packages/spacecat-shared-data-access/test/sites/index.test.js diff --git a/package-lock.json b/package-lock.json index b37bce2d..0e9459ca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1491,6 +1491,50 @@ "semantic-release": ">=18.0.0-beta.1" } }, + "node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, "node_modules/@smithy/abort-controller": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-2.0.14.tgz", @@ -5588,6 +5632,12 @@ "node": "*" } }, + "node_modules/just-extend": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", + "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", + "dev": true + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -5909,6 +5959,12 @@ "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", "dev": true }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, "node_modules/lodash.ismatch": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz", @@ -6620,6 +6676,46 @@ "integrity": "sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==", "dev": true }, + "node_modules/nise": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.5.tgz", + "integrity": "sha512-VJuPIfUFaXNRzETTQEEItTOP8Y171ijr+JLq42wHes3DiryR8vT+1TXQW/Rx8JNUhyYYWyIvjXTU6dOhJcs9Nw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "@sinonjs/fake-timers": "^10.0.2", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers/node_modules/@sinonjs/commons": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", + "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, "node_modules/nock": { "version": "13.3.8", "resolved": "https://registry.npmjs.org/nock/-/nock-13.3.8.tgz", @@ -9602,6 +9698,21 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "dependencies": { + "isarray": "0.0.1" + } + }, + "node_modules/path-to-regexp/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -10910,6 +11021,33 @@ "node": ">=4" } }, + "node_modules/sinon": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.5", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" + } + }, + "node_modules/sinon/node_modules/diff": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", + "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -12101,18 +12239,49 @@ } }, "packages/spacecat-shared-data-access": { + "name": "@adobe/spacecat-shared-data-access", "version": "1.1.0", "license": "Apache-2.0", "dependencies": { - "@adobe/spacecat-shared-utils": "1.1.0" + "@adobe/spacecat-shared-dynamo": "1.1.2", + "@adobe/spacecat-shared-utils": "1.1.0", + "uuid": "9.0.1" }, "devDependencies": { - "chai": "4.3.10" + "chai": "4.3.10", + "sinon": "^17.0.1" + } + }, + "packages/spacecat-shared-data-access/node_modules/@adobe/spacecat-shared-dynamo": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@adobe/spacecat-shared-dynamo/-/spacecat-shared-dynamo-1.1.2.tgz", + "integrity": "sha512-xR1iFHhDBOhQ2ci0qH6dzz954+KJpSPoHaUynTphLNTVNlToXHkQiSE8X+z8cX9G5NCm9HG3tgNYv/9wRK6zPQ==", + "dependencies": { + "@adobe/spacecat-shared-utils": "1.0.1", + "@aws-sdk/client-dynamodb": "3.454.0", + "@aws-sdk/lib-dynamodb": "3.454.0" + } + }, + "packages/spacecat-shared-data-access/node_modules/@adobe/spacecat-shared-dynamo/node_modules/@adobe/spacecat-shared-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@adobe/spacecat-shared-utils/-/spacecat-shared-utils-1.0.1.tgz", + "integrity": "sha512-6HiCywan5y9jpbFYkQX87Vg0q5dXoJf2m6MiDvNBEJdREdUxFwE+oYA6Fr86qqd32ECnCeYCFO13HcNRn1RxkQ==" + }, + "packages/spacecat-shared-data-access/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" } }, "packages/spacecat-shared-dynamo": { "name": "@adobe/spacecat-shared-dynamo", - "version": "1.1.0", + "version": "1.1.2", "license": "Apache-2.0", "dependencies": { "@adobe/spacecat-shared-utils": "1.1.0", diff --git a/packages/spacecat-shared-data-access/docs/schema.json b/packages/spacecat-shared-data-access/docs/schema.json new file mode 100644 index 00000000..4ba87ec9 --- /dev/null +++ b/packages/spacecat-shared-data-access/docs/schema.json @@ -0,0 +1,290 @@ +{ + "ModelName": "StarCatalogue", + "ModelMetadata": { + "Author": "Dominique Jäggi", + "DateCreated": "Nov 23, 2023, 07:00 AM", + "DateLastModified": "Nov 23, 2023, 07:00 AM", + "Description": "", + "AWSService": "Amazon DynamoDB", + "Version": "3.0" + }, + "DataModel": [ + { + "TableName": "sites", + "KeyAttributes": { + "PartitionKey": { + "AttributeName": "id", + "AttributeType": "S" + } + }, + "NonKeyAttributes": [ + { + "AttributeName": "baseURL", + "AttributeType": "S" + }, + { + "AttributeName": "imsOrgId", + "AttributeType": "S" + }, + { + "AttributeName": "GSI1PK", + "AttributeType": "S" + }, + { + "AttributeName": "createdAt", + "AttributeType": "S" + }, + { + "AttributeName": "updatedAt", + "AttributeType": "S" + } + ], + "GlobalSecondaryIndexes": [ + { + "IndexName": "all_sites", + "KeyAttributes": { + "PartitionKey": { + "AttributeName": "GSI1PK", + "AttributeType": "S" + }, + "SortKey": { + "AttributeName": "baseURL", + "AttributeType": "S" + } + }, + "Projection": { + "ProjectionType": "ALL" + } + } + ], + "DataAccess": { + "MySql": {} + }, + "SampleDataFormats": { + "id": [ + "identifiers", + "UUID" + ], + "baseURL": [ + "identifiers", + "URL" + ], + "imsOrgId": [ + "identifiers", + "UUID" + ] + }, + "BillingMode": "PROVISIONED", + "ProvisionedCapacitySettings": { + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + }, + "AutoScalingRead": { + "ScalableTargetRequest": { + "MinCapacity": 1, + "MaxCapacity": 10, + "ServiceRole": "AWSServiceRoleForApplicationAutoScaling_DynamoDBTable" + }, + "ScalingPolicyConfiguration": { + "TargetValue": 70 + } + }, + "AutoScalingWrite": { + "ScalableTargetRequest": { + "MinCapacity": 1, + "MaxCapacity": 10, + "ServiceRole": "AWSServiceRoleForApplicationAutoScaling_DynamoDBTable" + }, + "ScalingPolicyConfiguration": { + "TargetValue": 70 + } + } + } + }, + { + "TableName": "audits", + "KeyAttributes": { + "PartitionKey": { + "AttributeName": "siteId", + "AttributeType": "S" + }, + "SortKey": { + "AttributeName": "SK", + "AttributeType": "S" + } + }, + "NonKeyAttributes": [ + { + "AttributeName": "auditedAt", + "AttributeType": "S" + }, + { + "AttributeName": "auditResult", + "AttributeType": "M" + }, + { + "AttributeName": "auditType", + "AttributeType": "S" + }, + { + "AttributeName": "expiresAt", + "AttributeType": "N" + }, + { + "AttributeName": "fullAuditRef", + "AttributeType": "S" + } + ], + "DataAccess": { + "MySql": {} + }, + "SampleDataFormats": { + "siteId": [ + "identifiers", + "UUID" + ], + "auditedAt": [ + "date", + "ISO 8601 date and time" + ], + "fullAuditRef": [ + "identifiers", + "URL" + ] + }, + "BillingMode": "PROVISIONED", + "ProvisionedCapacitySettings": { + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + }, + "AutoScalingRead": { + "ScalableTargetRequest": { + "MinCapacity": 1, + "MaxCapacity": 10, + "ServiceRole": "AWSServiceRoleForApplicationAutoScaling_DynamoDBTable" + }, + "ScalingPolicyConfiguration": { + "TargetValue": 70 + } + }, + "AutoScalingWrite": { + "ScalableTargetRequest": { + "MinCapacity": 1, + "MaxCapacity": 10, + "ServiceRole": "AWSServiceRoleForApplicationAutoScaling_DynamoDBTable" + }, + "ScalingPolicyConfiguration": { + "TargetValue": 70 + } + } + } + }, + { + "TableName": "latest_audits", + "KeyAttributes": { + "PartitionKey": { + "AttributeName": "siteId", + "AttributeType": "S" + }, + "SortKey": { + "AttributeName": "SK", + "AttributeType": "S" + } + }, + "NonKeyAttributes": [ + { + "AttributeName": "auditedAt", + "AttributeType": "S" + }, + { + "AttributeName": "auditResult", + "AttributeType": "M" + }, + { + "AttributeName": "auditType", + "AttributeType": "S" + }, + { + "AttributeName": "expiresAt", + "AttributeType": "N" + }, + { + "AttributeName": "fullAuditRef", + "AttributeType": "S" + }, + { + "AttributeName": "GSI1PK", + "AttributeType": "S" + }, + { + "AttributeName": "GSI1SK", + "AttributeType": "S" + } + ], + "GlobalSecondaryIndexes": [ + { + "IndexName": "all_latest_audit_scores", + "KeyAttributes": { + "PartitionKey": { + "AttributeName": "GSI1PK", + "AttributeType": "S" + }, + "SortKey": { + "AttributeName": "GSI1SK", + "AttributeType": "S" + } + }, + "Projection": { + "ProjectionType": "ALL" + } + } + ], + "DataAccess": { + "MySql": {} + }, + "SampleDataFormats": { + "siteId": [ + "identifiers", + "UUID" + ], + "auditedAt": [ + "date", + "ISO 8601 date and time" + ], + "fullAuditRef": [ + "identifiers", + "URL" + ] + }, + "BillingMode": "PROVISIONED", + "ProvisionedCapacitySettings": { + "ProvisionedThroughput": { + "ReadCapacityUnits": 5, + "WriteCapacityUnits": 5 + }, + "AutoScalingRead": { + "ScalableTargetRequest": { + "MinCapacity": 1, + "MaxCapacity": 10, + "ServiceRole": "AWSServiceRoleForApplicationAutoScaling_DynamoDBTable" + }, + "ScalingPolicyConfiguration": { + "TargetValue": 70 + } + }, + "AutoScalingWrite": { + "ScalableTargetRequest": { + "MinCapacity": 1, + "MaxCapacity": 10, + "ServiceRole": "AWSServiceRoleForApplicationAutoScaling_DynamoDBTable" + }, + "ScalingPolicyConfiguration": { + "TargetValue": 70 + } + } + } + } + ] +} diff --git a/packages/spacecat-shared-data-access/package.json b/packages/spacecat-shared-data-access/package.json index 028e9cff..04ff67eb 100644 --- a/packages/spacecat-shared-data-access/package.json +++ b/packages/spacecat-shared-data-access/package.json @@ -29,9 +29,12 @@ "access": "public" }, "dependencies": { - "@adobe/spacecat-shared-utils": "1.1.0" + "@adobe/spacecat-shared-dynamo": "1.1.2", + "@adobe/spacecat-shared-utils": "1.1.0", + "uuid": "9.0.1" }, "devDependencies": { - "chai": "4.3.10" + "chai": "4.3.10", + "sinon": "17.0.1" } } diff --git a/packages/spacecat-shared-data-access/src/audits/accessPatterns.js b/packages/spacecat-shared-data-access/src/audits/accessPatterns.js new file mode 100644 index 00000000..3be1877a --- /dev/null +++ b/packages/spacecat-shared-data-access/src/audits/accessPatterns.js @@ -0,0 +1,95 @@ +/* + * Copyright 2023 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. + */ + +/** + * Retrieves audits for a specified site. If an audit type is provided, + * it returns only audits of that type. + * + * @param {DynamoDbClient} dynamoClient - The DynamoDB client. + * @param {Logger} log - The logger. + * @param {string} siteId - The ID of the site for which audits are being retrieved. + * @param {string} [auditType] - Optional. The type of audits to retrieve. + * @returns {Promise} A promise that resolves to an array of audits for the specified site. + */ +export const getAuditsForSite = async (dynamoClient, log, siteId, auditType) => { + // Base query parameters + const queryParams = { + TableName: 'audits', + KeyConditionExpression: 'siteId = :siteId', + ExpressionAttributeValues: { + ':siteId': siteId, + }, + }; + + if (auditType !== undefined) { + queryParams.KeyConditionExpression += ' AND begins_with(SK, :auditType)'; + queryParams.ExpressionAttributeValues[':auditType'] = `${auditType}#`; + } + + return dynamoClient.query(queryParams); +}; + +/** + * Retrieves the latest audits of a specific type across all sites. + * + * @param {DynamoDbClient} dynamoClient - The DynamoDB client. + * @param {Logger} log - The logger. + * @param {string} auditType - The type of audits to retrieve. + * @param {boolean} ascending - Determines if the audits should be sorted ascending + * or descending by scores. + * @returns {Promise} A promise that resolves to an array of the latest + * audits of the specified type. + */ +export const getLatestAudits = async ( + dynamoClient, + log, + auditType, + ascending = true, +) => dynamoClient.query({ + TableName: 'latest_audits', + IndexName: 'all_latest_audit_scores', + KeyConditionExpression: 'GSI1PK = :gsi1pk AND begins_with(GSI1SK, :auditType)', + ExpressionAttributeValues: { + ':gsi1pk': 'ALL_LATEST_AUDITS', + ':auditType': `${auditType}#`, + }, + ScanIndexForward: ascending, // Sorts ascending if true, descending if false +}); + +/** + * Retrieves the latest audit for a specified site and audit type. + * + * @param {DynamoDbClient} dynamoClient - The DynamoDB client. + * @param {Logger} log - The logger. + * @param {string} siteId - The ID of the site for which the latest audit is being retrieved. + * @param {string} auditType - The type of audit to retrieve the latest instance of. + * @returns {Promise} A promise that resolves to the latest audit of the + * specified type for the site, or null if none is found. + */ +export const getLatestAuditForSite = async ( + dynamoClient, + log, + siteId, + auditType, +) => { + const latestAudit = await dynamoClient.query({ + TableName: 'latest_audits', + KeyConditionExpression: 'siteId = :siteId AND begins_with(SK, :auditType)', + ExpressionAttributeValues: { + ':siteId': siteId, + ':auditType': `${auditType}#`, + }, + Limit: 1, + }); + + return latestAudit.length > 0 ? latestAudit[0] : null; +}; diff --git a/packages/spacecat-shared-data-access/src/audits/index.js b/packages/spacecat-shared-data-access/src/audits/index.js new file mode 100644 index 00000000..3230ce4f --- /dev/null +++ b/packages/spacecat-shared-data-access/src/audits/index.js @@ -0,0 +1,34 @@ +/* + * Copyright 2023 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. + */ + +import { getAuditsForSite, getLatestAuditForSite, getLatestAudits } from './accessPatterns.js'; + +export const auditFunctions = (dynamoClient, log) => ({ + getAuditsForSite: (siteId, auditType) => getAuditsForSite( + dynamoClient, + log, + siteId, + auditType, + ), + getLatestAudits: (auditType, ascending) => getLatestAudits( + dynamoClient, + log, + auditType, + ascending, + ), + getLatestAuditForSite: (siteId, auditType) => getLatestAuditForSite( + dynamoClient, + log, + siteId, + auditType, + ), +}); diff --git a/packages/spacecat-shared-data-access/src/index.d.ts b/packages/spacecat-shared-data-access/src/index.d.ts new file mode 100644 index 00000000..2605daff --- /dev/null +++ b/packages/spacecat-shared-data-access/src/index.d.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2023 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. + */ + +export interface Site { + id: string; + baseURL: string; + imsOrgId: string; + createdAt: string; + updatedAt: string; +} + +export interface Audit { + siteId: string; + auditedAt: string; + auditResult: object; + auditType: string; + expiresAt: number; + fullAuditRef: string; + createdAt: string; + updatedAt: string; +} + +export interface DataAccess { + getAuditsForSite: ( + siteId: string, + auditType?: string + ) => Promise; + getLatestAuditForSite: ( + siteId: string, + auditType: string, + ) => Promise; + getLatestAudits: ( + auditType: string, + ascending?: boolean, + ) => Promise; + getSites: () => Promise; + getSitesToAudit: () => Promise; + getSitesWithLatestAudit: ( + auditType: string, + sortAuditsAscending?: boolean, + ) => Promise; + getSiteByBaseURL: ( + baseUrl: string, + ) => Promise; + getSiteByBaseURLWithAuditInfo: ( + baseUrl: string, + auditType: string, + latestOnly?: boolean, + ) => Promise; + getSiteByBaseURLWithAudits: ( + baseUrl: string, + auditType: string, + ) => Promise; + getSiteByBaseURLWithLatestAudit: ( + baseUrl: string, + auditType: string, + ) => Promise; +} + +export function createDataAccess( + logger: object, +): DataAccess; diff --git a/packages/spacecat-shared-data-access/src/index.js b/packages/spacecat-shared-data-access/src/index.js new file mode 100644 index 00000000..6f0b9bdd --- /dev/null +++ b/packages/spacecat-shared-data-access/src/index.js @@ -0,0 +1,33 @@ +/* + * Copyright 2023 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. + */ + +import { createClient } from '@adobe/spacecat-shared-dynamo'; +import { auditFunctions } from './audits/index.js'; +import { siteFunctions } from './sites/index.js'; + +/** + * Creates a data access object. + * + * @param {Logger} log logger + * @returns {object} data access object + */ +export const createDataAccess = (log = console) => { + const dynamoClient = createClient(log); + + const auditFuncs = auditFunctions(dynamoClient, log); + const siteFuncs = siteFunctions(dynamoClient, log); + + return { + ...auditFuncs, + ...siteFuncs, + }; +}; diff --git a/packages/spacecat-shared-data-access/src/models/audit.js b/packages/spacecat-shared-data-access/src/models/audit.js new file mode 100644 index 00000000..76d7c37f --- /dev/null +++ b/packages/spacecat-shared-data-access/src/models/audit.js @@ -0,0 +1,60 @@ +/* + * Copyright 2023 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. + */ + +import { hasText, isIsoDate, isObject } from '@adobe/spacecat-shared-utils'; +import { Base } from './base.js'; + +const EXPIRES_IN_DAYS = 30; + +const Audit = (data = {}) => { + const self = Base(data); + + self.getSiteId = () => self.state.siteId; + self.getAuditedAt = () => self.state.auditedAt; + self.getAuditResult = () => self.state.auditResult; + self.getAuditType = () => self.state.auditType.toLowerCase(); + self.getExpiresAt = () => self.state.expiresAt; + self.getFullAuditRef = () => self.state.fullAuditRef; + + return Object.freeze(self); +}; + +export const createAudit = (data) => { + const newState = { ...data }; + + if (!hasText(newState.siteId)) { + throw new Error('Site ID must be provided'); + } + + if (!isIsoDate(newState.auditedAt)) { + throw new Error('Audited at must be a valid ISO date'); + } + + if (!hasText(newState.auditType)) { + throw new Error('Audit type must be provided'); + } + + if (!isObject(newState.auditResult)) { + throw new Error('Audit result must be an object'); + } + + if (!hasText(newState.fullAuditRef)) { + throw new Error('Full audit ref must be provided'); + } + + if (!newState.expiresAt) { + newState.expiresAt = new Date(newState.auditedAt); + newState.expiresAt.setDate(newState.expiresAt.getDate() + EXPIRES_IN_DAYS); + } + + return Audit(newState); +}; diff --git a/packages/spacecat-shared-data-access/src/models/base.js b/packages/spacecat-shared-data-access/src/models/base.js new file mode 100644 index 00000000..797fa26f --- /dev/null +++ b/packages/spacecat-shared-data-access/src/models/base.js @@ -0,0 +1,30 @@ +/* + * Copyright 2023 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. + */ + +import { v4 as uuidv4 } from 'uuid'; +import { isString } from '@adobe/spacecat-shared-utils'; + +export const Base = (data = {}) => { + const self = { state: { ...data } }; + + const newRecord = !isString(self.state.id); + + if (newRecord) { + self.state.id = uuidv4(); + } + + self.getId = () => self.state.id; + self.getCreatedAt = () => self.state.createdAt; + self.getUpdatedAt = () => self.state.updatedAt; + + return self; +}; diff --git a/packages/spacecat-shared-data-access/src/models/site.js b/packages/spacecat-shared-data-access/src/models/site.js new file mode 100644 index 00000000..b08587d0 --- /dev/null +++ b/packages/spacecat-shared-data-access/src/models/site.js @@ -0,0 +1,51 @@ +/* + * Copyright 2023 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. + */ + +import { hasText, isValidUrl } from '@adobe/spacecat-shared-utils'; +import { Base } from './base.js'; + +const Site = (data = {}) => { + const self = Base(data); + + self.getBaseURL = () => self.state.baseURL; + self.getImsOrgId = () => self.state.imsOrgId; + + self.updateBaseURL = (baseURL) => { + if (!isValidUrl(baseURL)) { + throw new Error('Base URL must be a valid URL'); + } + + self.state.baseURL = baseURL; + return self; + }; + + self.updateImsOrgId = (imsOrgId) => { + if (!hasText(imsOrgId)) { + throw new Error('IMS Org ID must be provided'); + } + + self.state.imsOrgId = imsOrgId; + return self; + }; + + return Object.freeze(self); +}; + +export const createSite = (data) => { + const newState = { ...data }; + + if (!isValidUrl(newState.baseURL)) { + throw new Error('Base URL must be a valid URL'); + } + + return Site(newState); +}; diff --git a/packages/spacecat-shared-data-access/src/sites/accessPatterns.js b/packages/spacecat-shared-data-access/src/sites/accessPatterns.js new file mode 100644 index 00000000..ca862e49 --- /dev/null +++ b/packages/spacecat-shared-data-access/src/sites/accessPatterns.js @@ -0,0 +1,170 @@ +/* + * Copyright 2023 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. + */ + +import { + getAuditsForSite, + getLatestAuditForSite, + getLatestAudits, +} from '../audits/accessPatterns.js'; + +/** + * Retrieves all sites. + * + * @param {DynamoDbClient} dynamoClient - The DynamoDB client. + * @returns {Promise} A promise that resolves to an array of all sites. + */ +export const getSites = async (dynamoClient) => dynamoClient.query({ + TableName: 'sites', + IndexName: 'all_sites', // GSI name + KeyConditionExpression: 'GSI1PK = :gsi1pk', + ExpressionAttributeValues: { + ':gsi1pk': 'ALL_SITES', + }, +}); + +/** + * Retrieves a list of base URLs for all sites. + * + * @param {DynamoDbClient} dynamoClient - The DynamoDB client. + * @param {Logger} log - The logger. + * @returns {Promise>} A promise that resolves to an array of base URLs for all sites. + */ +export const getSitesToAudit = async (dynamoClient, log) => { + const sites = await getSites(dynamoClient, log); + + return sites.map((item) => item.baseURL); +}; + +/** + * Retrieves sites with their latest audit of a specified type. + * + * @param {DynamoDbClient} dynamoClient - The DynamoDB client. + * @param {Logger} log - The logger. + * @param {string} auditType - The type of the latest audits to retrieve for each site. + * @param {boolean} [sortAuditsAscending] - Optional. Determines if the audits + * should be sorted ascending or descending by scores. + * @returns {Promise} A promise that resolves to an array of site objects, + * each with its latest audit of the specified type. + */ +export const getSitesWithLatestAudit = async ( + dynamoClient, + log, + auditType, + sortAuditsAscending = true, +) => { + const [sites, latestAudits] = await Promise.all([ + getSites(dynamoClient), + getLatestAudits(dynamoClient, log, auditType, sortAuditsAscending), + ]); + + const sitesMap = new Map(sites.map((site) => [site.id, site])); + + return latestAudits.reduce((result, audit) => { + const site = sitesMap.get(audit.siteId); + if (site) { + site.audits = [audit]; + result.push(site); + } + return result; + }, []); +}; + +/** + * Retrieves a site by its base URL. + * + * @param {DynamoDbClient} dynamoClient - The DynamoDB client. + * @param {Logger} log - The logger. + * @param {string} baseUrl - The base URL of the site to retrieve. + * @returns {Promise} A promise that resolves to the site object if found, + * otherwise null. + */ +export const getSiteByBaseURL = async ( + dynamoClient, + log, + baseUrl, +) => dynamoClient.getItem('sites', { + GSI1PK: 'ALL_SITES', + baseUrl, +}); + +/** + * Retrieves a site by its base URL, along with associated audit information. + * + * @param {DynamoDbClient} dynamoClient - The DynamoDB client. + * @param {Logger} log - The logger. + * @param {string} baseUrl - The base URL of the site to retrieve. + * @param {string} auditType - The type of audits to retrieve for the site. + * @param {boolean} [latestOnly=false] - Determines if only the latest audit should be retrieved. + * @returns {Promise} A promise that resolves to the site object with audit + * data if found, otherwise null. + */ +export const getSiteByBaseURLWithAuditInfo = async ( + dynamoClient, + log, + baseUrl, + auditType, + latestOnly = false, +) => { + const site = await getSiteByBaseURL(dynamoClient, log, baseUrl); + + if (!site) { + return null; + } + + site.audits = latestOnly + ? [await getLatestAuditForSite( + dynamoClient, + log, + site.id, + auditType, + )].filter((audit) => audit != null) + : await getAuditsForSite( + dynamoClient, + log, + site.id, + auditType, + ); + + return site; +}; + +/** + * Retrieves a site by its base URL, including all its audits. + * + * @param {DynamoDbClient} dynamoClient - The DynamoDB client. + * @param {Logger} log - The logger. + * @param {string} baseUrl - The base URL of the site to retrieve. + * @param {string} auditType - The type of audits to retrieve for the site. + * @returns {Promise} A promise that resolves to the site object with all its audits. + */ +export const getSiteByBaseURLWithAudits = async ( + dynamoClient, + log, + baseUrl, + auditType, +) => getSiteByBaseURLWithAuditInfo(dynamoClient, log, baseUrl, auditType, false); + +/** + * Retrieves a site by its base URL, including only its latest audit. + * + * @param {DynamoDbClient} dynamoClient - The DynamoDB client. + * @param {Logger} log - The logger. + * @param {string} baseUrl - The base URL of the site to retrieve. + * @param {string} auditType - The type of the latest audit to retrieve for the site. + * @returns {Promise} A promise that resolves to the site object with its latest audit. + */ +export const getSiteByBaseURLWithLatestAudit = async ( + dynamoClient, + log, + baseUrl, + auditType, +) => getSiteByBaseURLWithAuditInfo(dynamoClient, log, baseUrl, auditType, true); diff --git a/packages/spacecat-shared-data-access/src/sites/index.js b/packages/spacecat-shared-data-access/src/sites/index.js new file mode 100644 index 00000000..e9ac693b --- /dev/null +++ b/packages/spacecat-shared-data-access/src/sites/index.js @@ -0,0 +1,59 @@ +/* + * Copyright 2023 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. + */ + +import { + getSiteByBaseURL, + getSiteByBaseURLWithAuditInfo, + getSiteByBaseURLWithAudits, + getSiteByBaseURLWithLatestAudit, getSites, getSitesToAudit, getSitesWithLatestAudit, +} from './accessPatterns.js'; + +export const siteFunctions = (dynamoClient, log) => ({ + getSites: () => getSites( + dynamoClient, + log, + ), + getSitesToAudit: () => getSitesToAudit( + dynamoClient, + log, + ), + getSitesWithLatestAudit: (auditType, sortAuditsAscending) => getSitesWithLatestAudit( + dynamoClient, + log, + auditType, + sortAuditsAscending, + ), + getSiteByBaseURL: (baseUrl) => getSiteByBaseURL( + dynamoClient, + log, + baseUrl, + ), + getSiteByBaseURLWithAuditInfo: (baseUrl, auditType, latestOnly) => getSiteByBaseURLWithAuditInfo( + dynamoClient, + log, + baseUrl, + auditType, + latestOnly, + ), + getSiteByBaseURLWithAudits: (baseUrl, auditType) => getSiteByBaseURLWithAudits( + dynamoClient, + log, + baseUrl, + auditType, + ), + getSiteByBaseURLWithLatestAudit: (baseUrl, auditType) => getSiteByBaseURLWithLatestAudit( + dynamoClient, + log, + baseUrl, + auditType, + ), +}); diff --git a/packages/spacecat-shared-data-access/test/audits/index.test.js b/packages/spacecat-shared-data-access/test/audits/index.test.js new file mode 100644 index 00000000..9e8a63db --- /dev/null +++ b/packages/spacecat-shared-data-access/test/audits/index.test.js @@ -0,0 +1,79 @@ +/* + * Copyright 2023 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 } from 'chai'; +import sinon from 'sinon'; + +import { auditFunctions } from '../../src/audits/index.js'; + +describe('Audit Index Tests', () => { + describe('Audit Functions Export Tests', () => { + const mockDynamoClient = {}; + const mockLog = {}; + + const exportedFunctions = auditFunctions(mockDynamoClient, mockLog); + + it('exports getAuditsForSite function', () => { + expect(exportedFunctions).to.have.property('getAuditsForSite'); + expect(exportedFunctions.getAuditsForSite).to.be.a('function'); + }); + + it('exports getLatestAudits function', () => { + expect(exportedFunctions).to.have.property('getLatestAudits'); + expect(exportedFunctions.getLatestAudits).to.be.a('function'); + }); + + it('exports getLatestAuditForSite function', () => { + expect(exportedFunctions).to.have.property('getLatestAuditForSite'); + expect(exportedFunctions.getLatestAuditForSite).to.be.a('function'); + }); + }); + + describe('Audit Functions Tests', () => { + let mockDynamoClient; + let mockLog; + let exportedFunctions; + + beforeEach(() => { + mockDynamoClient = { + query: sinon.stub().returns(Promise.resolve([])), + }; + mockLog = { log: sinon.stub() }; + + exportedFunctions = auditFunctions(mockDynamoClient, mockLog); + }); + + it('calls getAuditsForSite and return an array', async () => { + const result = await exportedFunctions.getAuditsForSite('siteId', 'auditType'); + expect(result).to.be.an('array'); + // eslint-disable-next-line no-unused-expressions + expect(mockDynamoClient.query.called).to.be.true; + }); + + it('calls getLatestAudits and return an array', async () => { + const result = await exportedFunctions.getLatestAudits('auditType', true); + expect(result).to.be.an('array'); + // eslint-disable-next-line no-unused-expressions + expect(mockDynamoClient.query.called).to.be.true; + }); + + it('calls getLatestAuditForSite and return an array', async () => { + const result = await exportedFunctions.getLatestAuditForSite('siteId', 'auditType'); + // eslint-disable-next-line no-unused-expressions + expect(result).to.be.null; + // eslint-disable-next-line no-unused-expressions + expect(mockDynamoClient.query.called).to.be.true; + }); + }); +}); diff --git a/packages/spacecat-shared-data-access/test/index.test.js b/packages/spacecat-shared-data-access/test/index.test.js new file mode 100644 index 00000000..656b2252 --- /dev/null +++ b/packages/spacecat-shared-data-access/test/index.test.js @@ -0,0 +1,58 @@ +/* + * Copyright 2023 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 } from 'chai'; +import { createDataAccess } from '../src/index.js'; + +describe('Data Access Object Tests', () => { + const auditFunctions = [ + 'getAuditsForSite', + 'getLatestAudits', + 'getLatestAuditForSite', + ]; + const siteFunctions = [ + 'getSites', + 'getSitesToAudit', + 'getSitesWithLatestAudit', + 'getSiteByBaseURL', + 'getSiteByBaseURLWithAuditInfo', + 'getSiteByBaseURLWithAudits', + 'getSiteByBaseURLWithLatestAudit', + ]; + + let dao; + + before(() => { + dao = createDataAccess(); + }); + + it('contains all known audit functions', () => { + auditFunctions.forEach((funcName) => { + expect(dao).to.have.property(funcName); + }); + }); + + it('contains all known site functions', () => { + siteFunctions.forEach((funcName) => { + expect(dao).to.have.property(funcName); + }); + }); + + it('does not contain any unexpected functions', () => { + const expectedFunctions = new Set([...auditFunctions, ...siteFunctions]); + Object.keys(dao).forEach((funcName) => { + expect(expectedFunctions).to.include(funcName); + }); + }); +}); diff --git a/packages/spacecat-shared-data-access/test/models/audit.test.js b/packages/spacecat-shared-data-access/test/models/audit.test.js new file mode 100644 index 00000000..f6d48683 --- /dev/null +++ b/packages/spacecat-shared-data-access/test/models/audit.test.js @@ -0,0 +1,69 @@ +/* + * Copyright 2023 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 } from 'chai'; +import { createAudit } from '../../src/models/audit.js'; + +// Constants for testing +const validData = { + siteId: '123', + auditedAt: new Date().toISOString(), + auditType: 'Type', + auditResult: {}, + fullAuditRef: 'ref123', +}; + +describe('Audit Module Tests', () => { + describe('Validation Tests', () => { + it('throws an error if siteId is not provided', () => { + expect(() => createAudit({ ...validData, siteId: '' })).to.throw('Site ID must be provided'); + }); + + it('throws an error if auditedAt is not a valid ISO date', () => { + expect(() => createAudit({ ...validData, auditedAt: 'invalid-date' })).to.throw('Audited at must be a valid ISO date'); + }); + + it('throws an error if auditType is not provided', () => { + expect(() => createAudit({ ...validData, auditType: '' })).to.throw('Audit type must be provided'); + }); + + it('throws an error if auditResult is not an object', () => { + expect(() => createAudit({ ...validData, auditResult: 'not-an-object' })).to.throw('Audit result must be an object'); + }); + + it('throws an error if fullAuditRef is not provided', () => { + expect(() => createAudit({ ...validData, fullAuditRef: '' })).to.throw('Full audit ref must be provided'); + }); + }); + + describe('Functionality Tests', () => { + it('creates an audit object with correct properties', () => { + const audit = createAudit(validData); + expect(audit).to.be.an('object'); + expect(audit.getSiteId()).to.equal(validData.siteId); + expect(audit.getAuditedAt()).to.equal(validData.auditedAt); + expect(audit.getAuditType()).to.equal(validData.auditType.toLowerCase()); + expect(audit.getAuditResult()).to.deep.equal(validData.auditResult); + expect(audit.getFullAuditRef()).to.equal(validData.fullAuditRef); + }); + + it('automatically sets expiresAt if not provided', () => { + const audit = createAudit(validData); + expect(audit.getExpiresAt()).to.be.a('Date'); + const expectedDate = new Date(validData.auditedAt); + expectedDate.setDate(expectedDate.getDate() + 30); + expect(audit.getExpiresAt().toDateString()).to.equal(expectedDate.toDateString()); + }); + }); +}); diff --git a/packages/spacecat-shared-data-access/test/models/base.test.js b/packages/spacecat-shared-data-access/test/models/base.test.js new file mode 100644 index 00000000..e9264aad --- /dev/null +++ b/packages/spacecat-shared-data-access/test/models/base.test.js @@ -0,0 +1,57 @@ +/* + * Copyright 2023 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 } from 'chai'; +import { Base } from '../../src/models/base.js'; + +describe('Base Entity Tests', () => { + describe('Initialization Tests', () => { + it('should automatically assign a UUID if no id is provided', () => { + const baseEntity = Base(); + expect(baseEntity.getId()).to.match(/[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/); + }); + + it('should retain the provided id if one is provided', () => { + const id = 'test-id'; + const baseEntity = Base({ id }); + expect(baseEntity.getId()).to.equal(id); + }); + }); + + describe('Getter Method Tests', () => { + it('should correctly return the createdAt date if provided', () => { + const createdAt = new Date().toISOString(); + const baseEntity = Base({ createdAt }); + expect(baseEntity.getCreatedAt()).to.equal(createdAt); + }); + + it('should return undefined for createdAt if not provided', () => { + const baseEntity = Base(); + // eslint-disable-next-line no-unused-expressions + expect(baseEntity.getCreatedAt()).to.be.undefined; + }); + + it('should correctly return the updatedAt date if provided', () => { + const updatedAt = new Date().toISOString(); + const baseEntity = Base({ updatedAt }); + expect(baseEntity.getUpdatedAt()).to.equal(updatedAt); + }); + + it('should return undefined for updatedAt if not provided', () => { + const baseEntity = Base(); + // eslint-disable-next-line no-unused-expressions + expect(baseEntity.getUpdatedAt()).to.be.undefined; + }); + }); +}); diff --git a/packages/spacecat-shared-data-access/test/models/site.test.js b/packages/spacecat-shared-data-access/test/models/site.test.js new file mode 100644 index 00000000..899c8fad --- /dev/null +++ b/packages/spacecat-shared-data-access/test/models/site.test.js @@ -0,0 +1,65 @@ +/* + * Copyright 2023 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 } from 'chai'; +import { createSite } from '../../src/models/site.js'; + +// Constants for testing +const validData = { + baseURL: 'https://www.example.com', + imsOrgId: 'org123', +}; + +describe('Site Module Tests', () => { + describe('Validation Tests', () => { + it('throws an error if baseURL is not a valid URL', () => { + expect(() => createSite({ ...validData, baseURL: 'invalid-url' })).to.throw('Base URL must be a valid URL'); + }); + + it('creates a site object with valid baseURL', () => { + const site = createSite({ ...validData }); + expect(site).to.be.an('object'); + expect(site.getBaseURL()).to.equal(validData.baseURL); + }); + }); + + describe('Site Object Functionality', () => { + let site; + + beforeEach(() => { + // Reset site object before each test + site = createSite(validData); + }); + + it('updates baseURL correctly', () => { + const newURL = 'https://www.newexample.com'; + site.updateBaseURL(newURL); + expect(site.getBaseURL()).to.equal(newURL); + }); + + it('throws an error when updating with an invalid baseURL', () => { + expect(() => site.updateBaseURL('invalid-url')).to.throw('Base URL must be a valid URL'); + }); + + it('updates imsOrgId correctly', () => { + const newImsOrgId = 'newOrg123'; + site.updateImsOrgId(newImsOrgId); + expect(site.getImsOrgId()).to.equal(newImsOrgId); + }); + + it('throws an error when updating with an empty imsOrgId', () => { + expect(() => site.updateImsOrgId('')).to.throw('IMS Org ID must be provided'); + }); + }); +}); diff --git a/packages/spacecat-shared-data-access/test/sites/index.test.js b/packages/spacecat-shared-data-access/test/sites/index.test.js new file mode 100644 index 00000000..a3874983 --- /dev/null +++ b/packages/spacecat-shared-data-access/test/sites/index.test.js @@ -0,0 +1,214 @@ +/* + * Copyright 2023 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 } from 'chai'; +import sinon from 'sinon'; + +import { siteFunctions } from '../../src/sites/index.js'; + +describe('Site Index Tests', () => { + describe('Site Functions Export Tests', () => { + const mockDynamoClient = {}; + const mockLog = {}; + + const exportedFunctions = siteFunctions(mockDynamoClient, mockLog); + + it('exports getSites function', () => { + expect(exportedFunctions).to.have.property('getSites'); + expect(exportedFunctions.getSites).to.be.a('function'); + }); + + it('exports getSitesToAudit function', () => { + expect(exportedFunctions).to.have.property('getSitesToAudit'); + expect(exportedFunctions.getSitesToAudit).to.be.a('function'); + }); + + it('exports getSitesWithLatestAudit function', () => { + expect(exportedFunctions).to.have.property('getSitesWithLatestAudit'); + expect(exportedFunctions.getSitesWithLatestAudit).to.be.a('function'); + }); + + it('exports getSiteByBaseURL function', () => { + expect(exportedFunctions).to.have.property('getSiteByBaseURL'); + expect(exportedFunctions.getSiteByBaseURL).to.be.a('function'); + }); + + it('exports getSiteByBaseURLWithAuditInfo function', () => { + expect(exportedFunctions).to.have.property('getSiteByBaseURLWithAuditInfo'); + expect(exportedFunctions.getSiteByBaseURLWithAuditInfo).to.be.a('function'); + }); + + it('exports getSiteByBaseURLWithAudits function', () => { + expect(exportedFunctions).to.have.property('getSiteByBaseURLWithAudits'); + expect(exportedFunctions.getSiteByBaseURLWithAudits).to.be.a('function'); + }); + + it('exports getSiteByBaseURLWithLatestAudit function', () => { + expect(exportedFunctions).to.have.property('getSiteByBaseURLWithLatestAudit'); + expect(exportedFunctions.getSiteByBaseURLWithLatestAudit).to.be.a('function'); + }); + }); + + describe('Site Functions Tests', () => { + let mockDynamoClient; + let mockLog; + let exportedFunctions; + + beforeEach(() => { + mockDynamoClient = { + query: sinon.stub().returns(Promise.resolve([])), + getItem: sinon.stub().returns(Promise.resolve(null)), + }; + mockLog = { log: sinon.stub() }; + + exportedFunctions = siteFunctions(mockDynamoClient, mockLog); + }); + + it('calls getSites and returns an array', async () => { + const result = await exportedFunctions.getSites(); + expect(result).to.be.an('array'); + // eslint-disable-next-line no-unused-expressions + expect(mockDynamoClient.query.called).to.be.true; + }); + + it('calls getSitesToAudit and returns an array', async () => { + const result = await exportedFunctions.getSitesToAudit(); + expect(result).to.be.an('array'); + // eslint-disable-next-line no-unused-expressions + expect(mockDynamoClient.query.called).to.be.true; + }); + + it('calls getSitesWithLatestAudit and returns an array', async () => { + const result = await exportedFunctions.getSitesWithLatestAudit(); + expect(result).to.be.an('array'); + // eslint-disable-next-line no-unused-expressions + expect(mockDynamoClient.query.called).to.be.true; + }); + + it('calls getSitesWithLatestAudit and handles latestAudits', async () => { + const mockSiteData = [{ + id: 'site1', + baseUrl: 'https://example.com', + }]; + + const mockAuditData = [{ + id: 'audit1', + siteId: 'site1', + auditType: 'type1', + }]; + + mockDynamoClient.query.onFirstCall().resolves(mockSiteData); + mockDynamoClient.query.onSecondCall().resolves(mockAuditData); + + const result = await exportedFunctions.getSitesWithLatestAudit('auditType'); + // eslint-disable-next-line no-unused-expressions + expect(result).to.be.an('array').that.has.lengthOf(1); + }); + + it('calls getSitesWithLatestAudit and handles empty latestAudits', async () => { + const mockSiteData = [{ + id: 'site1', + baseUrl: 'https://example.com', + }]; + + const mockAuditData = []; + + mockDynamoClient.query.onFirstCall().resolves(mockSiteData); + mockDynamoClient.query.onSecondCall().resolves(mockAuditData); + + const result = await exportedFunctions.getSitesWithLatestAudit('auditType'); + // eslint-disable-next-line no-unused-expressions + expect(result).to.be.an('array').that.is.empty; + }); + + it('calls getSiteByBaseURL and returns an array/object', async () => { + const result = await exportedFunctions.getSiteByBaseURL(); + // eslint-disable-next-line no-unused-expressions + expect(result).to.be.null; + // eslint-disable-next-line no-unused-expressions + expect(mockDynamoClient.getItem.called).to.be.true; + }); + + it('calls getSiteByBaseURLWithAuditInfo and returns an array/object', async () => { + const result = await exportedFunctions.getSiteByBaseURLWithAuditInfo(); + // eslint-disable-next-line no-unused-expressions + expect(result).to.be.null; + // eslint-disable-next-line no-unused-expressions + expect(mockDynamoClient.getItem.called).to.be.true; + }); + + it('calls getSiteByBaseURLWithAuditInfo and returns null when site is undefined', async () => { + mockDynamoClient.query.resolves(undefined); + + const result = await exportedFunctions.getSiteByBaseURLWithAuditInfo('baseUrl', 'auditType'); + // eslint-disable-next-line no-unused-expressions + expect(result).to.be.null; + }); + + it('calls getSiteByBaseURLWithAuditInfo and assigns latest audit when latestOnly is true', async () => { + const mockSiteData = { + id: 'site1', + baseUrl: 'https://example.com', + }; + + const mockLatestAuditData = [{ + id: 'audit1', + siteId: 'site1', + auditType: 'type1', + }]; + + mockDynamoClient.getItem.onFirstCall().resolves(mockSiteData); + mockDynamoClient.query.onFirstCall().resolves(mockLatestAuditData); + + const result = await exportedFunctions.getSiteByBaseURLWithAuditInfo('https://example.com', 'type1', true); + expect(result).to.have.property('audits').that.is.an('array').with.lengthOf(1); + expect(result.audits[0]).to.deep.equal(mockLatestAuditData[0]); + }); + + it('calls getSiteByBaseURLWithAuditInfo and assigns all audits when latestOnly is false', async () => { + const mockSiteData = { + id: 'site1', + baseUrl: 'https://example.com', + }; + + const mockLatestAuditData = [{ + id: 'audit1', + siteId: 'site1', + auditType: 'type1', + }]; + + mockDynamoClient.getItem.onFirstCall().resolves(mockSiteData); + mockDynamoClient.query.onFirstCall().resolves(mockLatestAuditData); + + const result = await exportedFunctions.getSiteByBaseURLWithAuditInfo('baseUrl', 'auditType', false); + expect(result).to.have.property('audits').that.is.an('array'); + }); + + it('calls getSiteByBaseURLWithAudits and returns an array/object', async () => { + const result = await exportedFunctions.getSiteByBaseURLWithAudits(); + // eslint-disable-next-line no-unused-expressions + expect(result).to.be.null; + // eslint-disable-next-line no-unused-expressions + expect(mockDynamoClient.getItem.called).to.be.true; + }); + + it('calls getSiteByBaseURLWithLatestAudit and returns an array/object', async () => { + const result = await exportedFunctions.getSiteByBaseURLWithLatestAudit(); + // eslint-disable-next-line no-unused-expressions + expect(result).to.be.null; + // eslint-disable-next-line no-unused-expressions + expect(mockDynamoClient.getItem.called).to.be.true; + }); + }); +});