diff --git a/src/components/Identity/createComponent.js b/src/components/Identity/createComponent.js index e97310244..f5a8eb8fe 100644 --- a/src/components/Identity/createComponent.js +++ b/src/components/Identity/createComponent.js @@ -9,8 +9,8 @@ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTA OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import getIdentityOptionsValidator from "./getIdentity/getIdentityOptionsValidator.js"; import appendIdentityToUrlOptionsValidator from "./appendIdentityToUrl/appendIdentityToUrlOptionsValidator.js"; +import ecidNamespace from "../../constants/ecidNamespace.js"; export default ({ addEcidQueryToPayload, @@ -18,13 +18,14 @@ export default ({ ensureSingleIdentity, setLegacyEcid, handleResponseForIdSyncs, - getEcidFromResponse, + getNamespacesFromResponse, getIdentity, consent, appendIdentityToUrl, logger, + getIdentityOptionsValidator, }) => { - let ecid; + let namespaces; let edge = {}; return { lifecycle: { @@ -36,15 +37,17 @@ export default ({ return ensureSingleIdentity({ request, onResponse, onRequestFailure }); }, onResponse({ response }) { - if (!ecid) { - ecid = getEcidFromResponse(response); - + const newNamespaces = getNamespacesFromResponse(response); + if ( + (!namespaces || !namespaces[ecidNamespace]) && + newNamespaces && + newNamespaces[ecidNamespace] + ) { // Only data collection calls will have an ECID in the response. // https://jira.corp.adobe.com/browse/EXEG-1234 - if (ecid) { - setLegacyEcid(ecid); - } + setLegacyEcid(newNamespaces[ecidNamespace]); } + namespaces = newNamespaces; // For sendBeacon requests, getEdge() will return {}, so we are using assign here // so that sendBeacon requests don't override the edge info from before. edge = { ...edge, ...response.getEdge() }; @@ -56,16 +59,18 @@ export default ({ getIdentity: { optionsValidator: getIdentityOptionsValidator, run: (options) => { + const { namespaces: requestedNamespaces } = options; return consent .awaitConsent() .then(() => { - return ecid ? undefined : getIdentity(options); + return namespaces ? undefined : getIdentity(options); }) .then(() => { return { - identity: { - ECID: ecid, - }, + identity: requestedNamespaces.reduce((acc, namespace) => { + acc[namespace] = namespaces[namespace] || null; + return acc; + }, {}), edge, }; }); @@ -77,10 +82,15 @@ export default ({ return consent .withConsent() .then(() => { - return ecid ? undefined : getIdentity(options); + return namespaces ? undefined : getIdentity(options); }) .then(() => { - return { url: appendIdentityToUrl(ecid, options.url) }; + return { + url: appendIdentityToUrl( + namespaces[ecidNamespace], + options.url, + ), + }; }) .catch((error) => { logger.warn(`Unable to append identity to url. ${error.message}`); diff --git a/src/components/Identity/getIdentity/getIdentityOptionsValidator.js b/src/components/Identity/getIdentity/createGetIdentityOptionsValidator.js similarity index 53% rename from src/components/Identity/getIdentity/getIdentityOptionsValidator.js rename to src/components/Identity/getIdentity/createGetIdentityOptionsValidator.js index d8a439f34..6a663bd17 100644 --- a/src/components/Identity/getIdentity/getIdentityOptionsValidator.js +++ b/src/components/Identity/getIdentity/createGetIdentityOptionsValidator.js @@ -11,20 +11,39 @@ governing permissions and limitations under the License. */ import { validateConfigOverride } from "../../../utils/index.js"; -import { objectOf, literal, arrayOf } from "../../../utils/validation/index.js"; +import { objectOf, enumOf, arrayOf } from "../../../utils/validation/index.js"; +import ecidNamespace from "../../../constants/ecidNamespace.js"; +import coreNamespace from "../../../constants/coreNamespace.js"; + /** * Verifies user provided event options. * @param {*} options The user event options to validate * @returns {*} Validated options */ -export default objectOf({ - namespaces: arrayOf(literal("ECID")) + +const validator = objectOf({ + namespaces: arrayOf(enumOf(ecidNamespace, coreNamespace)) .nonEmpty() .uniqueItems() - .default(["ECID"]), + .default([ecidNamespace]), edgeConfigOverrides: validateConfigOverride, }) .noUnknownFields() .default({ - namespaces: ["ECID"], + namespaces: [ecidNamespace], }); + +export default ({ thirdPartyCookiesEnabled }) => { + return (options) => { + const validatedOptions = validator(options); + if ( + !thirdPartyCookiesEnabled && + validatedOptions.namespaces.includes(coreNamespace) + ) { + throw new Error( + `namespaces: The ${coreNamespace} namespace cannot be requested when third-party cookies are disabled.`, + ); + } + return validatedOptions; + }; +}; diff --git a/src/components/Identity/getEcidFromResponse.js b/src/components/Identity/getNamespacesFromResponse.js similarity index 73% rename from src/components/Identity/getEcidFromResponse.js rename to src/components/Identity/getNamespacesFromResponse.js index 3dae0e6bb..9c952557c 100644 --- a/src/components/Identity/getEcidFromResponse.js +++ b/src/components/Identity/getNamespacesFromResponse.js @@ -9,12 +9,12 @@ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTA OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import ecidNamespace from "../../constants/ecidNamespace.js"; - export default (response) => { const identityResultPayloads = response.getPayloadsByType("identity:result"); - const ecidPayload = identityResultPayloads.find( - (payload) => payload.namespace && payload.namespace.code === ecidNamespace, - ); - return ecidPayload ? ecidPayload.id : undefined; + return identityResultPayloads.reduce((acc, payload) => { + if (payload.namespace && payload.namespace.code) { + acc[payload.namespace.code] = payload.id; + } + return acc; + }, {}); }; diff --git a/src/components/Identity/index.js b/src/components/Identity/index.js index 47ca0df4e..f751de0cd 100644 --- a/src/components/Identity/index.js +++ b/src/components/Identity/index.js @@ -25,17 +25,18 @@ import awaitVisitorOptIn from "./visitorService/awaitVisitorOptIn.js"; import injectGetEcidFromVisitor from "./visitorService/injectGetEcidFromVisitor.js"; import injectHandleResponseForIdSyncs from "./injectHandleResponseForIdSyncs.js"; import injectEnsureSingleIdentity from "./injectEnsureSingleIdentity.js"; -import addEcidQueryToPayload from "./addEcidQueryToPayload.js"; +import injectAddEcidQueryToPayload from "./injectAddEcidQueryToPayload.js"; import injectSetDomainForInitialIdentityPayload from "./injectSetDomainForInitialIdentityPayload.js"; import injectAddLegacyEcidToPayload from "./injectAddLegacyEcidToPayload.js"; import injectAddQueryStringIdentityToPayload from "./injectAddQueryStringIdentityToPayload.js"; import addEcidToPayload from "./addEcidToPayload.js"; import injectAwaitIdentityCookie from "./injectAwaitIdentityCookie.js"; -import getEcidFromResponse from "./getEcidFromResponse.js"; +import getNamespacesFromResponse from "./getNamespacesFromResponse.js"; import createGetIdentity from "./getIdentity/createGetIdentity.js"; import createIdentityRequest from "./getIdentity/createIdentityRequest.js"; import createIdentityRequestPayload from "./getIdentity/createIdentityRequestPayload.js"; import injectAppendIdentityToUrl from "./appendIdentityToUrl/injectAppendIdentityToUrl.js"; +import createGetIdentityOptionsValidator from "./getIdentity/createGetIdentityOptionsValidator.js"; const createIdentity = ({ config, @@ -115,18 +116,26 @@ const createIdentity = ({ orgId, globalConfigOverrides, }); + const getIdentityOptionsValidator = createGetIdentityOptionsValidator({ + thirdPartyCookiesEnabled, + }); + const addEcidQueryToPayload = injectAddEcidQueryToPayload({ + thirdPartyCookiesEnabled, + areThirdPartyCookiesSupportedByDefault, + }); return createComponent({ addEcidQueryToPayload, addQueryStringIdentityToPayload, ensureSingleIdentity, setLegacyEcid: legacyIdentity.setEcid, handleResponseForIdSyncs, - getEcidFromResponse, + getNamespacesFromResponse, getIdentity, consent, appendIdentityToUrl, logger, config, + getIdentityOptionsValidator, }); }; diff --git a/test/unit/specs/components/Identity/addEcidQueryToPayload.spec.js b/src/components/Identity/injectAddEcidQueryToPayload.js similarity index 54% rename from test/unit/specs/components/Identity/addEcidQueryToPayload.spec.js rename to src/components/Identity/injectAddEcidQueryToPayload.js index f0e4addd0..b8601816d 100644 --- a/test/unit/specs/components/Identity/addEcidQueryToPayload.spec.js +++ b/src/components/Identity/injectAddEcidQueryToPayload.js @@ -10,16 +10,22 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import addEcidQueryToPayload from "../../../../../src/components/Identity/addEcidQueryToPayload.js"; +import ecidNamespace from "../../constants/ecidNamespace.js"; +import coreNamespace from "../../constants/coreNamespace.js"; -describe("Identity::addEcidQueryToPayload", () => { - it("adds an ECID query to the event", () => { - const payload = jasmine.createSpyObj("payload", ["mergeQuery"]); - addEcidQueryToPayload(payload); - expect(payload.mergeQuery).toHaveBeenCalledWith({ - identity: { - fetch: ["ECID"], - }, - }); - }); -}); +export default ({ + thirdPartyCookiesEnabled, + areThirdPartyCookiesSupportedByDefault, +}) => { + const query = { + identity: { + fetch: [ecidNamespace], + }, + }; + if (thirdPartyCookiesEnabled && areThirdPartyCookiesSupportedByDefault()) { + query.identity.fetch.push(coreNamespace); + } + return (payload) => { + payload.mergeQuery(query); + }; +}; diff --git a/src/components/Identity/addEcidQueryToPayload.js b/src/constants/coreNamespace.js similarity index 71% rename from src/components/Identity/addEcidQueryToPayload.js rename to src/constants/coreNamespace.js index e11ec0286..5eaaacbd9 100644 --- a/src/components/Identity/addEcidQueryToPayload.js +++ b/src/constants/coreNamespace.js @@ -1,5 +1,5 @@ /* -Copyright 2020 Adobe. All rights reserved. +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 @@ -10,12 +10,4 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import ecidNamespace from "../../constants/ecidNamespace.js"; - -export default (payload) => { - payload.mergeQuery({ - identity: { - fetch: [ecidNamespace], - }, - }); -}; +export default "CORE"; diff --git a/src/utils/validation/createUniqueItemsValidator.js b/src/utils/validation/createUniqueItemsValidator.js index 619f3e592..7dc196b08 100644 --- a/src/utils/validation/createUniqueItemsValidator.js +++ b/src/utils/validation/createUniqueItemsValidator.js @@ -15,5 +15,6 @@ import isUnique from "../isUnique.js"; export default () => { return (value, path) => { assertValid(isUnique(value), value, path, "array values to be unique"); + return value; }; }; diff --git a/src/utils/validation/index.js b/src/utils/validation/index.js index 20c36050e..1eefb93e8 100644 --- a/src/utils/validation/index.js +++ b/src/utils/validation/index.js @@ -245,7 +245,7 @@ const boundString = string.bind(base); const boundEnumOf = function boundEnumOf(...values) { return boundAnyOf( values.map(boundLiteral), - `one of these values: [${JSON.stringify(values)}]`, + `one of these values: ${JSON.stringify(values)}`, ); }; diff --git a/test/functional/specs/Identity/C19160486.js b/test/functional/specs/Identity/C19160486.js new file mode 100644 index 000000000..82b5df745 --- /dev/null +++ b/test/functional/specs/Identity/C19160486.js @@ -0,0 +1,153 @@ +/* +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 { t } from "testcafe"; +import createFixture from "../../helpers/createFixture/index.js"; +import { + compose, + orgMainConfigMain, + debugEnabled, + thirdPartyCookiesEnabled, + thirdPartyCookiesDisabled, +} from "../../helpers/constants/configParts/index.js"; +import createAlloyProxy from "../../helpers/createAlloyProxy.js"; +import createNetworkLogger from "../../helpers/networkLogger/index.js"; +import areThirdPartyCookiesSupported from "../../helpers/areThirdPartyCookiesSupported.js"; +import { SECONDARY_TEST_PAGE } from "../../helpers/constants/url.js"; + +const thirdPartyCookiesEnabledConfig = compose( + orgMainConfigMain, + debugEnabled, + thirdPartyCookiesEnabled, +); +const thirdPartyCookiesDisabledConfig = compose( + orgMainConfigMain, + debugEnabled, + thirdPartyCookiesDisabled, +); + +const networkLogger = createNetworkLogger(); + +createFixture({ + title: "C19160486: The CORE identity is returned correctly from getIdentity", + requestHooks: [ + networkLogger.edgeEndpointLogs, + networkLogger.acquireEndpointLogs, + ], +}); + +test.meta({ + ID: "C19160486", + SEVERITY: "P0", + TEST_RUN: "Regression", +}); + +test("C19160486: CORE identity is the same across domains when called first", async () => { + const alloy = createAlloyProxy(); + await alloy.configure(thirdPartyCookiesEnabledConfig); + const { + identity: { ECID: ecid1, CORE: core1 }, + } = await alloy.getIdentity({ namespaces: ["ECID", "CORE"] }); + await t.navigateTo(SECONDARY_TEST_PAGE); + + await alloy.configure(thirdPartyCookiesEnabledConfig); + const { + identity: { ECID: ecid2, CORE: core2 }, + } = await alloy.getIdentity({ namespaces: ["ECID", "CORE"] }); + + if (areThirdPartyCookiesSupported()) { + // ecids are the same because the same orgId is used to go from CORE -> ECID + await t.expect(ecid1).eql(ecid2); + await t.expect(core1).eql(core2); + await t.expect(ecid1).notEql(core1); + } else { + // ecids are different because third party cookies are not written + await t.expect(ecid1).notEql(ecid2); + // CORE identity is null because Experience Edge only creates it when called on demdex domain + // which only happens on Chrome browsers. + await t.expect(core1).eql(null); + await t.expect(core2).eql(null); + } +}); + +test("C19160486: CORE identity is the same across domains when called after sendEvent", async () => { + const alloy = createAlloyProxy(); + await alloy.configure(thirdPartyCookiesEnabledConfig); + await alloy.sendEvent(); + const { + identity: { ECID: ecid1, CORE: core1 }, + } = await alloy.getIdentity({ namespaces: ["ECID", "CORE"] }); + await t.expect(networkLogger.acquireEndpointLogs.requests.length).eql(0); + await t.navigateTo(SECONDARY_TEST_PAGE); + + await alloy.configure(thirdPartyCookiesEnabledConfig); + await alloy.sendEvent(); + const { + identity: { ECID: ecid2, CORE: core2 }, + } = await alloy.getIdentity({ namespaces: ["ECID", "CORE"] }); + await t.expect(networkLogger.acquireEndpointLogs.requests.length).eql(0); + + if (areThirdPartyCookiesSupported()) { + // ecids are the same because the same orgId is used to go from CORE -> ECID + await t.expect(ecid1).eql(ecid2); + await t.expect(core1).eql(core2); + await t.expect(ecid1).notEql(core1); + } else { + // ecids are different because third party cookies are not written + await t.expect(ecid1).notEql(ecid2); + // CORE identity is null because Experience Edge only creates it when called on demdex domain + // which only happens on Chrome browsers. + await t.expect(core1).eql(null); + await t.expect(core2).eql(null); + } +}); + +test("C19160486: CORE identity is not requested when third party cookies are disabled", async () => { + const alloy = createAlloyProxy(); + await alloy.configure(thirdPartyCookiesDisabledConfig); + await alloy.sendEvent(); + await t.expect(networkLogger.edgeEndpointLogs.requests.length).eql(1); + await t.expect(networkLogger.edgeEndpointLogs.count(() => true)).eql(1); + const requestBody = JSON.parse( + networkLogger.edgeEndpointLogs.requests[0].request.body, + ); + await t.expect(requestBody.query.identity.fetch).eql(["ECID"]); +}); + +test("C19160486: CORE identity cannot be requested from getIdentity when third party cookies are disabled", async () => { + const alloy = createAlloyProxy(); + await alloy.configure(thirdPartyCookiesDisabledConfig); + const errorMessage = await alloy.getIdentityErrorMessage({ + namespaces: ["CORE"], + }); + await t + .expect(errorMessage) + .contains( + "The CORE namespace cannot be requested when third-party cookies are disabled", + ); +}); + +test("C19160486: Requesting CORE identity and ECID can be done separately", async () => { + const alloy = createAlloyProxy(); + await alloy.configure(thirdPartyCookiesEnabledConfig); + const { + identity: { ECID: ecid }, + } = await alloy.getIdentity({ namespaces: ["ECID"] }); + await t.expect(ecid).ok(); + const { + identity: { CORE: core }, + } = await alloy.getIdentity({ namespaces: ["CORE"] }); + if (areThirdPartyCookiesSupported()) { + await t.expect(core).ok(); + } else { + await t.expect(core).eql(null); + } +}); diff --git a/test/functional/specs/Privacy/C14410.js b/test/functional/specs/Privacy/C14410.js index 28a558603..6e0319f7d 100644 --- a/test/functional/specs/Privacy/C14410.js +++ b/test/functional/specs/Privacy/C14410.js @@ -43,7 +43,7 @@ test("Test C14410: Configuring default consent to 'unknown' fails", async (t) => await t .expect(errorMessage) .contains( - `Expected one of these values: [["in","out","pending"]], but got "unknown"`, + `Expected one of these values: ["in","out","pending"], but got "unknown"`, ); }); diff --git a/test/functional/specs/Privacy/IAB/C224670.js b/test/functional/specs/Privacy/IAB/C224670.js index d06a4a48f..aa1c47999 100644 --- a/test/functional/specs/Privacy/IAB/C224670.js +++ b/test/functional/specs/Privacy/IAB/C224670.js @@ -67,7 +67,6 @@ test("Test C224670: Opt in to IAB", async () => { // 2. The ECID should exist in the response payload as well, if queried const identityHandle = consentResponse.getPayloadsByType("identity:result"); const returnedNamespaces = identityHandle.map((i) => i.namespace.code); - await t.expect(identityHandle.length).eql(1); await t.expect(returnedNamespaces).contains("ECID"); await alloy.sendEvent(); diff --git a/test/functional/specs/Privacy/IAB/C224671.js b/test/functional/specs/Privacy/IAB/C224671.js index dd1277944..e62ddea57 100644 --- a/test/functional/specs/Privacy/IAB/C224671.js +++ b/test/functional/specs/Privacy/IAB/C224671.js @@ -70,7 +70,8 @@ test.meta({ // 2. The ECID should exist in the response payload as well, if queried const identityHandle = consentResponse.getPayloadsByType("identity:result"); - await t.expect(identityHandle.length).eql(1); + const returnedNamespaces = identityHandle.map((i) => i.namespace.code); + await t.expect(returnedNamespaces).contains("ECID"); await alloy.sendEvent(); await t.expect(networkLogger.edgeEndpointLogs.requests.length).eql(0); diff --git a/test/functional/specs/Privacy/IAB/C224672.js b/test/functional/specs/Privacy/IAB/C224672.js index 3eb7a991f..690ab5976 100644 --- a/test/functional/specs/Privacy/IAB/C224672.js +++ b/test/functional/specs/Privacy/IAB/C224672.js @@ -68,7 +68,6 @@ test("Test C224672: Passing the `gdprContainsPersonalData` flag should return in // 2. The ECID should exist in the response payload as well, if queried const identityHandle = consentResponse.getPayloadsByType("identity:result"); const returnedNamespaces = identityHandle.map((i) => i.namespace.code); - await t.expect(identityHandle.length).eql(1); await t.expect(returnedNamespaces).contains("ECID"); await alloy.sendEvent(); diff --git a/test/functional/specs/Privacy/IAB/C224673.js b/test/functional/specs/Privacy/IAB/C224673.js index 547e16d3a..6e832349f 100644 --- a/test/functional/specs/Privacy/IAB/C224673.js +++ b/test/functional/specs/Privacy/IAB/C224673.js @@ -67,7 +67,6 @@ test("Test C224673: Opt in to IAB while gdprApplies is FALSE", async () => { // 2. The ECID should exist in the response payload as well, if queried const identityHandle = consentResponse.getPayloadsByType("identity:result"); const returnedNamespaces = identityHandle.map((i) => i.namespace.code); - await t.expect(identityHandle.length).eql(1); await t.expect(returnedNamespaces).contains("ECID"); await alloy.sendEvent(); diff --git a/test/functional/specs/Privacy/IAB/C224674.js b/test/functional/specs/Privacy/IAB/C224674.js index 5c70d5261..61a5bde00 100644 --- a/test/functional/specs/Privacy/IAB/C224674.js +++ b/test/functional/specs/Privacy/IAB/C224674.js @@ -67,7 +67,6 @@ test("Test C224674: Opt out to IAB while gdprApplies is FALSE", async () => { // 2. The ECID should exist in the response payload as well, if queried const identityHandle = consentResponse.getPayloadsByType("identity:result"); const returnedNamespaces = identityHandle.map((i) => i.namespace.code); - await t.expect(identityHandle.length).eql(1); await t.expect(returnedNamespaces).contains("ECID"); await alloy.sendEvent(); diff --git a/test/functional/specs/Privacy/IAB/C224676.js b/test/functional/specs/Privacy/IAB/C224676.js index af400b66c..b3681d5fe 100644 --- a/test/functional/specs/Privacy/IAB/C224676.js +++ b/test/functional/specs/Privacy/IAB/C224676.js @@ -74,10 +74,9 @@ test("Test C224676: Passing a positive Consent in the sendEvent command", async await t.expect(consentCookieValue).eql("general=in"); // 2. The ECID should exist in the response payload as well, if queried - // TODO: We are seeing 2 `identity:result` handles. Bug logged on Konductor side: - // https://jira.corp.adobe.com/browse/EXEG-1960 const identityHandle = response.getPayloadsByType("identity:result"); - await t.expect(identityHandle.length).eql(1); + const returnedNamespaces = identityHandle.map((i) => i.namespace.code); + await t.expect(returnedNamespaces).contains("ECID"); await alloy.sendEvent(); await t.expect(networkLogger.edgeEndpointLogs.requests.length).eql(2); diff --git a/test/functional/specs/Privacy/IAB/C224677.js b/test/functional/specs/Privacy/IAB/C224677.js index 1f45bf2f4..735d464e6 100644 --- a/test/functional/specs/Privacy/IAB/C224677.js +++ b/test/functional/specs/Privacy/IAB/C224677.js @@ -64,7 +64,6 @@ test("Test C224677: Call setConsent when purpose 10 is FALSE", async () => { // 2. The ECID should exist in the response payload as well, if queried const identityHandle = response.getPayloadsByType("identity:result"); const returnedNamespaces = identityHandle.map((i) => i.namespace.code); - await t.expect(identityHandle.length).eql(1); await t.expect(returnedNamespaces).contains("ECID"); // 3. Event calls going forward should remain opted in, even though AAM opts out consents with no purpose 10. diff --git a/test/functional/specs/Privacy/IAB/C224678.js b/test/functional/specs/Privacy/IAB/C224678.js index ed2ade059..0ae00d37d 100644 --- a/test/functional/specs/Privacy/IAB/C224678.js +++ b/test/functional/specs/Privacy/IAB/C224678.js @@ -76,7 +76,8 @@ test("Test C224678: Passing a negative Consent in the sendEvent command", async // 2. The ECID should exist in the response payload as well, even if queried const identityHandle = response.getPayloadsByType("identity:result"); - await t.expect(identityHandle.length).eql(1); + const returnedNamespaces = identityHandle.map((i) => i.namespace.code); + await t.expect(returnedNamespaces).contains("ECID"); // 3. Should not have any activation, ID Syncs or decisions in the response. const handlesThatShouldBeMissing = [ diff --git a/test/unit/specs/components/Identity/createComponent.spec.js b/test/unit/specs/components/Identity/createComponent.spec.js index 2ab550f7a..b50a3195c 100644 --- a/test/unit/specs/components/Identity/createComponent.spec.js +++ b/test/unit/specs/components/Identity/createComponent.spec.js @@ -15,22 +15,22 @@ import { defer } from "../../../../../src/utils/index.js"; import flushPromiseChains from "../../../helpers/flushPromiseChains.js"; describe("Identity::createComponent", () => { - let ensureSingleIdentity; let addEcidQueryToPayload; let addQueryStringIdentityToPayload; + let ensureSingleIdentity; let setLegacyEcid; let handleResponseForIdSyncs; - let getEcidFromResponse; - let component; + let getNamespacesFromResponse; let getIdentity; - let awaitConsentDeferred; - let withConsentDeferred; let consent; let appendIdentityToUrl; let logger; + let getIdentityOptionsValidator; + let awaitConsentDeferred; + let withConsentDeferred; let getIdentityDeferred; let response; - let config; + let component; beforeEach(() => { ensureSingleIdentity = jasmine.createSpy("ensureSingleIdentity"); @@ -40,7 +40,7 @@ describe("Identity::createComponent", () => { ); setLegacyEcid = jasmine.createSpy("setLegacyEcid"); handleResponseForIdSyncs = jasmine.createSpy("handleResponseForIdSyncs"); - getEcidFromResponse = jasmine.createSpy("getEcidFromResponse"); + getNamespacesFromResponse = jasmine.createSpy("getNamespacesFromResponse"); getIdentityDeferred = defer(); awaitConsentDeferred = defer(); withConsentDeferred = defer(); @@ -53,21 +53,19 @@ describe("Identity::createComponent", () => { getIdentity = jasmine .createSpy("getIdentity") .and.returnValue(getIdentityDeferred.promise); - config = { - edgeConfigOverrides: {}, - }; + getIdentityOptionsValidator = (options) => options; component = createComponent({ ensureSingleIdentity, addEcidQueryToPayload, addQueryStringIdentityToPayload, setLegacyEcid, handleResponseForIdSyncs, - getEcidFromResponse, + getNamespacesFromResponse, getIdentity, consent, appendIdentityToUrl, logger, - config, + getIdentityOptionsValidator, }); response = jasmine.createSpyObj("response", ["getEdge"]); }); @@ -123,20 +121,20 @@ describe("Identity::createComponent", () => { const idSyncsPromise = Promise.resolve(); handleResponseForIdSyncs.and.returnValue(idSyncsPromise); component.lifecycle.onResponse({ response }); - expect(getEcidFromResponse).toHaveBeenCalledWith(response); + expect(getNamespacesFromResponse).toHaveBeenCalledWith(response); expect(setLegacyEcid).not.toHaveBeenCalled(); }); it("creates legacy identity cookie if response contains ECID", () => { const idSyncsPromise = Promise.resolve(); handleResponseForIdSyncs.and.returnValue(idSyncsPromise); - getEcidFromResponse.and.returnValue("user@adobe"); + getNamespacesFromResponse.and.returnValue({ ECID: "user@adobe" }); component.lifecycle.onResponse({ response }); - expect(getEcidFromResponse).toHaveBeenCalledWith(response); + expect(getNamespacesFromResponse).toHaveBeenCalledWith(response); expect(setLegacyEcid).toHaveBeenCalledWith("user@adobe"); component.lifecycle.onResponse({ response }); - expect(getEcidFromResponse).toHaveBeenCalledTimes(1); + expect(getNamespacesFromResponse).toHaveBeenCalledTimes(2); expect(setLegacyEcid).toHaveBeenCalledTimes(1); }); @@ -152,6 +150,7 @@ describe("Identity::createComponent", () => { const idSyncsPromise = Promise.resolve(); handleResponseForIdSyncs.and.returnValue(idSyncsPromise); const onResolved = jasmine.createSpy("onResolved"); + const onResolved2 = jasmine.createSpy("onResolved2"); response.getEdge.and.returnValue({ regionId: 42 }); component.commands.getIdentity .run({ namespaces: ["ECID"] }) @@ -166,7 +165,11 @@ describe("Identity::createComponent", () => { }) .then(() => { expect(getIdentity).toHaveBeenCalled(); - getEcidFromResponse.and.returnValue("user@adobe"); + // ECID and CORE are requested regardless of the namespaces passed in. + getNamespacesFromResponse.and.returnValue({ + ECID: "user@adobe", + CORE: "mycoreid", + }); component.lifecycle.onResponse({ response }); getIdentityDeferred.resolve(); return flushPromiseChains(); @@ -180,17 +183,38 @@ describe("Identity::createComponent", () => { regionId: 42, }, }); + getNamespacesFromResponse.and.returnValue({ CORE: "mycoreid" }); + component.commands.getIdentity + .run({ namespaces: ["CORE"] }) + .then(onResolved2); + return flushPromiseChains(); + }) + .then(() => { + expect(getIdentity).toHaveBeenCalledTimes(1); + return flushPromiseChains(); + }) + .then(() => { + expect(onResolved2).toHaveBeenCalledWith({ + identity: { + CORE: "mycoreid", + }, + edge: { + regionId: 42, + }, + }); }); }); it("getIdentity command should not make a request when ecid is available", () => { const idSyncsPromise = Promise.resolve(); handleResponseForIdSyncs.and.returnValue(idSyncsPromise); - getEcidFromResponse.and.returnValue("user@adobe"); + getNamespacesFromResponse.and.returnValue({ ECID: "user@adobe" }); response.getEdge.and.returnValue({ regionId: 7 }); component.lifecycle.onResponse({ response }); const onResolved = jasmine.createSpy("onResolved"); - component.commands.getIdentity.run().then(onResolved); + component.commands.getIdentity + .run({ namespaces: ["ECID"] }) + .then(onResolved); return flushPromiseChains() .then(() => { expect(getIdentity).not.toHaveBeenCalled(); @@ -211,6 +235,39 @@ describe("Identity::createComponent", () => { }); }); + it("getIdentity command should not make a request when CORE is available", () => { + const idSyncsPromise = Promise.resolve(); + handleResponseForIdSyncs.and.returnValue(idSyncsPromise); + getNamespacesFromResponse.and.returnValue({ + ECID: "user@adobe", + CORE: "mycoreid", + }); + response.getEdge.and.returnValue({ regionId: 7 }); + component.lifecycle.onResponse({ response }); + const onResolved = jasmine.createSpy("onResolved"); + component.commands.getIdentity + .run({ namespaces: ["CORE"] }) + .then(onResolved); + return flushPromiseChains() + .then(() => { + expect(getIdentity).not.toHaveBeenCalled(); + expect(onResolved).not.toHaveBeenCalled(); + awaitConsentDeferred.resolve(); + return flushPromiseChains(); + }) + .then(() => { + expect(getIdentity).not.toHaveBeenCalled(); + expect(onResolved).toHaveBeenCalledWith({ + identity: { + CORE: "mycoreid", + }, + edge: { + regionId: 7, + }, + }); + }); + }); + it("getIdentity command is called with configuration overrides, when provided", () => { const idSyncsPromise = Promise.resolve(); handleResponseForIdSyncs.and.returnValue(idSyncsPromise); @@ -235,7 +292,7 @@ describe("Identity::createComponent", () => { }) .then(() => { expect(getIdentity).toHaveBeenCalledWith(getIdentityOptions); - getEcidFromResponse.and.returnValue("user@adobe"); + getNamespacesFromResponse.and.returnValue({ ECID: "user@adobe" }); component.lifecycle.onResponse({ response }); getIdentityDeferred.resolve(); return flushPromiseChains(); @@ -305,7 +362,7 @@ describe("Identity::createComponent", () => { expect(getIdentity).toHaveBeenCalledWith( jasmine.objectContaining({ namespaces: ["ECID"] }), ); - getEcidFromResponse.and.returnValue("user@adobe"); + getNamespacesFromResponse.and.returnValue({ ECID: "user@adobe" }); component.lifecycle.onResponse({ response }); getIdentityDeferred.resolve(); return flushPromiseChains(); @@ -323,7 +380,7 @@ describe("Identity::createComponent", () => { // set the ECID const idSyncsPromise = Promise.resolve(); handleResponseForIdSyncs.and.returnValue(idSyncsPromise); - getEcidFromResponse.and.returnValue("user@adobe"); + getNamespacesFromResponse.and.returnValue({ ECID: "user@adobe" }); response.getEdge.and.returnValue({ regionId: 7 }); component.lifecycle.onResponse({ response }); @@ -365,7 +422,7 @@ describe("Identity::createComponent", () => { edgeConfigOverrides, }), ); - getEcidFromResponse.and.returnValue("user@adobe"); + getNamespacesFromResponse.and.returnValue({ ECID: "user@adobe" }); component.lifecycle.onResponse({ response }); getIdentityDeferred.resolve(); return flushPromiseChains(); diff --git a/test/unit/specs/components/Identity/getIdentity/getIdentityOptionsValidator.spec.js b/test/unit/specs/components/Identity/getIdentity/createGetIdentityOptionsValidator.spec.js similarity index 57% rename from test/unit/specs/components/Identity/getIdentity/getIdentityOptionsValidator.spec.js rename to test/unit/specs/components/Identity/getIdentity/createGetIdentityOptionsValidator.spec.js index d16f0bd45..4407814a5 100644 --- a/test/unit/specs/components/Identity/getIdentity/getIdentityOptionsValidator.spec.js +++ b/test/unit/specs/components/Identity/getIdentity/createGetIdentityOptionsValidator.spec.js @@ -9,29 +9,37 @@ the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTA OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import getIdentityOptionsValidator from "../../../../../../src/components/Identity/getIdentity/getIdentityOptionsValidator.js"; + +import createGetIdentityOptionsValidator from "../../../../../../src/components/Identity/getIdentity/createGetIdentityOptionsValidator.js"; describe("Identity::getIdentityOptionsValidator", () => { + const thirdPartyValidator = createGetIdentityOptionsValidator({ + thirdPartyCookiesEnabled: true, + }); + const firstPartyValidator = createGetIdentityOptionsValidator({ + thirdPartyCookiesEnabled: false, + }); + it("should throw an error when invalid options are passed", () => { expect(() => { - getIdentityOptionsValidator({ key: ["item1", "item2"] }); + thirdPartyValidator({ key: ["item1", "item2"] }); }).toThrow(new Error("'key': Unknown field.")); expect(() => { - getIdentityOptionsValidator({ + thirdPartyValidator({ key1: ["item1", "item2"], key2: ["item1", "item2"], }); }).toThrow(new Error("'key1': Unknown field.\n'key2': Unknown field.")); expect(() => { - getIdentityOptionsValidator({ namespaces: [] }); + thirdPartyValidator({ namespaces: [] }); }).toThrow( new Error("'namespaces': Expected a non-empty array, but got []."), ); expect(() => { - getIdentityOptionsValidator({ namespaces: ["ECID", "ECID"] }); + thirdPartyValidator({ namespaces: ["ECID", "ECID"] }); }).toThrow( new Error( `'namespaces': Expected array values to be unique, but got ["ECID","ECID"].`, @@ -39,28 +47,32 @@ describe("Identity::getIdentityOptionsValidator", () => { ); expect(() => { - getIdentityOptionsValidator({ namespaces: ["ACD"] }); - }).toThrow(new Error(`'namespaces[0]': Expected ECID, but got "ACD".`)); + thirdPartyValidator({ namespaces: ["ACD"] }); + }).toThrow( + new Error( + `'namespaces[0]': Expected one of these values: ["ECID","CORE"], but got "ACD".`, + ), + ); }); it("should return valid options when no options are passed", () => { expect(() => { - getIdentityOptionsValidator(); + thirdPartyValidator(); }).not.toThrow(); - const validatedIdentityOptions = getIdentityOptionsValidator(); + const validatedIdentityOptions = thirdPartyValidator(); expect(validatedIdentityOptions).toEqual({ namespaces: ["ECID"] }); }); it("should not throw when supported namespace options are passed", () => { const ECID = "ECID"; expect(() => { - getIdentityOptionsValidator({ namespaces: [ECID] }); + thirdPartyValidator({ namespaces: [ECID] }); }).not.toThrow(); }); it("should return valid options when configuration is passed", () => { expect(() => { - getIdentityOptionsValidator({ + thirdPartyValidator({ edgeConfigOverrides: { identity: { idSyncContainerId: "123" } }, }); }).not.toThrow(); @@ -68,9 +80,25 @@ describe("Identity::getIdentityOptionsValidator", () => { it("should return valid options when an empty configuration is passed", () => { expect(() => { - getIdentityOptionsValidator({ + thirdPartyValidator({ edgeConfigOverrides: {}, }); }).not.toThrow(); }); + + it("should throw an error when CORE is passed with third party cookies disabled", () => { + expect(() => { + firstPartyValidator({ namespaces: ["CORE"] }); + }).toThrow( + new Error( + `namespaces: The CORE namespace cannot be requested when third-party cookies are disabled.`, + ), + ); + }); + + it("should not throw when CORE is passed with third party cookies enabled", () => { + expect(() => { + thirdPartyValidator({ namespaces: ["CORE"] }); + }).not.toThrow(); + }); }); diff --git a/test/unit/specs/components/Identity/getEcidFromResponse.spec.js b/test/unit/specs/components/Identity/getNamespacesFromResponse.spec.js similarity index 82% rename from test/unit/specs/components/Identity/getEcidFromResponse.spec.js rename to test/unit/specs/components/Identity/getNamespacesFromResponse.spec.js index 2de00b629..b0ba97354 100644 --- a/test/unit/specs/components/Identity/getEcidFromResponse.spec.js +++ b/test/unit/specs/components/Identity/getNamespacesFromResponse.spec.js @@ -10,7 +10,7 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ -import getEcidFromResponse from "../../../../../src/components/Identity/getEcidFromResponse.js"; +import getNamespacesFromResponse from "../../../../../src/components/Identity/getNamespacesFromResponse.js"; describe("Identity::getEcidFromResponse", () => { it("does not return ECID if ECID does not exist in response", () => { @@ -25,7 +25,7 @@ describe("Identity::getEcidFromResponse", () => { ], }); - expect(getEcidFromResponse(response)).toBeUndefined(); + expect(getNamespacesFromResponse(response)).toEqual({ other: "user123" }); expect(response.getPayloadsByType).toHaveBeenCalledWith("identity:result"); }); @@ -47,7 +47,10 @@ describe("Identity::getEcidFromResponse", () => { ], }); - expect(getEcidFromResponse(response)).toBe("user@adobe"); + expect(getNamespacesFromResponse(response)).toEqual({ + other: "user123", + ECID: "user@adobe", + }); expect(response.getPayloadsByType).toHaveBeenCalledWith("identity:result"); }); }); diff --git a/test/unit/specs/components/Identity/injectAddEcidQueryToPayload.spec.js b/test/unit/specs/components/Identity/injectAddEcidQueryToPayload.spec.js new file mode 100644 index 000000000..4daaa8358 --- /dev/null +++ b/test/unit/specs/components/Identity/injectAddEcidQueryToPayload.spec.js @@ -0,0 +1,55 @@ +/* +Copyright 2020 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 injectAddEcidQueryToPayload from "../../../../../src/components/Identity/injectAddEcidQueryToPayload.js"; + +describe("Identity::addEcidQueryToPayload", () => { + it("adds an ECID & CORE query to the event when third party cookies are enabled on Chrome", () => { + const addEcidQueryToPayload = injectAddEcidQueryToPayload({ + thirdPartyCookiesEnabled: true, + areThirdPartyCookiesSupportedByDefault: () => true, + }); + const payload = jasmine.createSpyObj("payload", ["mergeQuery"]); + addEcidQueryToPayload(payload); + expect(payload.mergeQuery).toHaveBeenCalledWith({ + identity: { + fetch: ["ECID", "CORE"], + }, + }); + }); + it("adds only ECID query to the event when third party cookies are enabled on Safari", () => { + const addEcidQueryToPayload = injectAddEcidQueryToPayload({ + thirdPartyCookiesEnabled: true, + areThirdPartyCookiesSupportedByDefault: () => false, + }); + const payload = jasmine.createSpyObj("payload", ["mergeQuery"]); + addEcidQueryToPayload(payload); + expect(payload.mergeQuery).toHaveBeenCalledWith({ + identity: { + fetch: ["ECID"], + }, + }); + }); + it("adds an ECID query to the event when third party cookies are disabled on Chrome", () => { + const addEcidQueryToPayload = injectAddEcidQueryToPayload({ + thirdPartyCookiesEnabled: false, + areThirdPartyCookiesSupportedByDefault: () => true, + }); + const payload = jasmine.createSpyObj("payload", ["mergeQuery"]); + addEcidQueryToPayload(payload); + expect(payload.mergeQuery).toHaveBeenCalledWith({ + identity: { + fetch: ["ECID"], + }, + }); + }); +}); diff --git a/test/unit/specs/utils/validation/createUniqueItemsValidator.spec.js b/test/unit/specs/utils/validation/createUniqueItemsValidator.spec.js index c769ae1f5..b7c240d71 100644 --- a/test/unit/specs/utils/validation/createUniqueItemsValidator.spec.js +++ b/test/unit/specs/utils/validation/createUniqueItemsValidator.spec.js @@ -18,12 +18,12 @@ import { describe("validation::createUniqueItems", () => { it(`validates an empty array`, () => { const validator = arrayOf(string()).uniqueItems(); - validator([]); + expect(validator([])).toEqual([]); }); it(`validates an array of one item`, () => { const validator = arrayOf(string()).uniqueItems(); - validator(["a"]); + expect(validator(["a"])).toEqual(["a"]); }); it(`throws an error on an array with duplicate (string) items`, () => { @@ -38,16 +38,16 @@ describe("validation::createUniqueItems", () => { it(`validates an array of enums`, () => { const validator = arrayOf(number()).uniqueItems(); - validator([]); + expect(validator([])).toEqual([]); }); it(`validates an array of null or undefined`, () => { const validator = arrayOf(string()).uniqueItems(); - validator([null, undefined]); + expect(validator([null, undefined])).toEqual([null, undefined]); }); it(`complains about required when null or undefined`, () => { - const validator = arrayOf(string()).uniqueItems().required(); + const validator = arrayOf(string().required()).uniqueItems(); expect(() => validator([null, undefined])).toThrowError(); }); });