diff --git a/packages/spacecat-shared-data-access/src/models/plg-onboarding/plg-onboarding.model.js b/packages/spacecat-shared-data-access/src/models/plg-onboarding/plg-onboarding.model.js index 4e7676dd0..797900854 100644 --- a/packages/spacecat-shared-data-access/src/models/plg-onboarding/plg-onboarding.model.js +++ b/packages/spacecat-shared-data-access/src/models/plg-onboarding/plg-onboarding.model.js @@ -24,7 +24,9 @@ class PlgOnboarding extends BaseModel { static IMS_ORG_ID_PATTERN = /^[a-z0-9]{24}@AdobeOrg$/i; - static DOMAIN_PATTERN = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/; + // Matches plain hostnames and subpath domains (e.g. nba.com, nba.com/kings). + // Rejects schemes (https://), IPv4 addresses, and query strings/fragments. + static DOMAIN_PATTERN = /^(?!\d+(\.\d+){3})[a-z0-9][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*)*(\/[a-z0-9._~-]*)*$/; static STATUSES = { PRE_ONBOARDING: 'PRE_ONBOARDING', diff --git a/packages/spacecat-shared-data-access/src/models/plg-onboarding/plg-onboarding.schema.js b/packages/spacecat-shared-data-access/src/models/plg-onboarding/plg-onboarding.schema.js index 5233a4f6b..c76d47f44 100644 --- a/packages/spacecat-shared-data-access/src/models/plg-onboarding/plg-onboarding.schema.js +++ b/packages/spacecat-shared-data-access/src/models/plg-onboarding/plg-onboarding.schema.js @@ -28,7 +28,7 @@ const schema = new SchemaBuilder(PlgOnboarding, PlgOnboardingCollection) type: 'string', required: true, readOnly: true, - validate: (value) => PlgOnboarding.DOMAIN_PATTERN.test(value) && value.length <= 253, + validate: (value) => PlgOnboarding.DOMAIN_PATTERN.test(value) && value.split('/')[0].length <= 253, }) .addAttribute('baseURL', { type: 'string', diff --git a/packages/spacecat-shared-data-access/test/unit/models/plg-onboarding/plg-onboarding.model.test.js b/packages/spacecat-shared-data-access/test/unit/models/plg-onboarding/plg-onboarding.model.test.js index 39d4e817d..31445aa02 100644 --- a/packages/spacecat-shared-data-access/test/unit/models/plg-onboarding/plg-onboarding.model.test.js +++ b/packages/spacecat-shared-data-access/test/unit/models/plg-onboarding/plg-onboarding.model.test.js @@ -79,6 +79,45 @@ describe('PlgOnboardingModel', () => { }); }); + describe('DOMAIN_PATTERN', () => { + const { DOMAIN_PATTERN } = PlgOnboarding; + + describe('valid values', () => { + [ + 'nba.com', + 'www.nba.com', + 'sub.domain.example.com', + 'nba.com/kings', + 'nba.com/us/kings', + 'example.com/path/with-hyphens', + 'example.com/path.with.dots', + 'example.io/a/b/c', + ].forEach((value) => { + it(`accepts "${value}"`, () => { + expect(DOMAIN_PATTERN.test(value)).to.be.true; + }); + }); + }); + + describe('invalid values', () => { + [ + ['empty string', ''], + ['scheme prefix', 'https://nba.com'], + ['scheme prefix http', 'http://nba.com'], + ['IPv4 address', '127.0.0.1'], + ['IPv4 address 8.8.8.8', '8.8.8.8'], + ['query string', 'nba.com?foo=bar'], + ['fragment', 'nba.com#section'], + ['path with query string', 'nba.com/kings?q=1'], + ['path with fragment', 'nba.com/kings#top'], + ].forEach(([label, value]) => { + it(`rejects ${label}: "${value}"`, () => { + expect(DOMAIN_PATTERN.test(value)).to.be.false; + }); + }); + }); + }); + describe('REVIEW_DECISIONS', () => { it('defines all expected review decisions', () => { expect(PlgOnboarding.REVIEW_DECISIONS).to.deep.equal({ diff --git a/packages/spacecat-shared-data-access/test/unit/models/plg-onboarding/plg-onboarding.schema.test.js b/packages/spacecat-shared-data-access/test/unit/models/plg-onboarding/plg-onboarding.schema.test.js index 9eabd2617..34cbc48b0 100644 --- a/packages/spacecat-shared-data-access/test/unit/models/plg-onboarding/plg-onboarding.schema.test.js +++ b/packages/spacecat-shared-data-access/test/unit/models/plg-onboarding/plg-onboarding.schema.test.js @@ -14,6 +14,66 @@ import { expect } from 'chai'; import plgOnboardingSchema from '../../../../src/models/plg-onboarding/plg-onboarding.schema.js'; describe('PlgOnboarding Schema', () => { + describe('domain attribute', () => { + let domainAttr; + + before(() => { + const attributes = plgOnboardingSchema.getAttributes(); + domainAttr = attributes.domain; + }); + + it('is a required read-only string', () => { + expect(domainAttr.type).to.equal('string'); + expect(domainAttr.required).to.be.true; + expect(domainAttr.readOnly).to.be.true; + }); + + it('has a validate function', () => { + expect(domainAttr.validate).to.be.a('function'); + }); + + describe('valid values', () => { + [ + 'nba.com', + 'www.nba.com', + 'nba.com/kings', + 'nba.com/us/kings', + 'example.com/path-with-hyphens', + ].forEach((value) => { + it(`accepts "${value}"`, () => { + expect(domainAttr.validate(value)).to.be.true; + }); + }); + }); + + describe('invalid values', () => { + [ + ['empty string', ''], + ['scheme prefix', 'https://nba.com'], + ['IPv4 address', '127.0.0.1'], + ['query string', 'nba.com?q=1'], + ['fragment', 'nba.com#top'], + ['hostname over 253 chars', `${'a'.repeat(250)}.com`], + ].forEach(([label, value]) => { + it(`rejects ${label}`, () => { + expect(domainAttr.validate(value)).to.be.false; + }); + }); + }); + + it('allows a subpath domain whose hostname is exactly 253 chars', () => { + const hostname = `${'a'.repeat(249)}.com`; + expect(hostname.length).to.equal(253); + expect(domainAttr.validate(`${hostname}/path`)).to.be.true; + }); + + it('rejects when only the hostname exceeds 253 chars (path does not inflate count)', () => { + const hostname = `${'a'.repeat(250)}.com`; + expect(hostname.length).to.equal(254); + expect(domainAttr.validate(`${hostname}/path`)).to.be.false; + }); + }); + describe('reviews attribute', () => { let reviewsAttr;