diff --git a/live-connect-js-6.0.3-alpha-8f0b28c.0.tgz b/live-connect-js-6.0.3-alpha-8f0b28c.0.tgz deleted file mode 100644 index 136fbb99..00000000 Binary files a/live-connect-js-6.0.3-alpha-8f0b28c.0.tgz and /dev/null differ diff --git a/src/cache.ts b/src/cache.ts index eaf245b5..11350f4f 100644 --- a/src/cache.ts +++ b/src/cache.ts @@ -1,6 +1,6 @@ import { strEqualsIgnoreCase, expiresInHours } from 'live-connect-common' import { WrappedStorageHandler } from './handlers/storage-handler' -import { StorageStrategies, StorageStrategy } from './model/storage-strategy' +import { StorageStrategies } from './model/storage-strategy' export type CacheRecord = { data: string @@ -19,20 +19,6 @@ export type StorageHandlerBackedCacheOpts = { defaultExpirationHours?: number } -// export type MakeCacheOpts = StorageHandlerBackedCacheOpts & { -// strategy: StorageStrategy, -// } - -// export function makeCache(opts: MakeCacheOpts): DurableCache { -// if (!strEqualsIgnoreCase(opts.strategy, StorageStrategies.cookie) && strEqualsIgnoreCase(opts.strategy, StorageStrategies.none)) { -// return NoOpCache -// } else { -// // TODO: Remove once we validate config properly -// const strategyWithDefault = opts.strategy ?? StorageStrategies.cookie -// return new StorageHandlerBackedCache({ ...opts, strategy: strategyWithDefault }) -// } -// } - export class StorageHandlerBackedCache implements DurableCache { private handler private storageStrategy diff --git a/src/enrichers/cache.ts b/src/enrichers/cache.ts index a0612046..23e25514 100644 --- a/src/enrichers/cache.ts +++ b/src/enrichers/cache.ts @@ -1,8 +1,8 @@ -import { strEqualsIgnoreCase } from "live-connect-common"; -import { DurableCache, NoOpCache, StorageHandlerBackedCache } from "../cache"; -import { WrappedStorageHandler } from "../handlers/storage-handler"; -import { StorageStrategies, StorageStrategy } from "../model/storage-strategy"; -import { Enricher } from "../types"; +import { strEqualsIgnoreCase } from 'live-connect-common' +import { DurableCache, NoOpCache, StorageHandlerBackedCache } from '../cache' +import { WrappedStorageHandler } from '../handlers/storage-handler' +import { StorageStrategies, StorageStrategy } from '../model/storage-strategy' +import { Enricher } from '../types' type Input = { domain: string, storageHandler: WrappedStorageHandler, storageStrategy: StorageStrategy } type Output = { cache: DurableCache } @@ -16,13 +16,13 @@ export const enrichCache: Enricher = state => { cache = new StorageHandlerBackedCache({ strategy: 'ls', storageHandler: state.storageHandler, - domain: state.domain, + domain: state.domain }) } else { cache = new StorageHandlerBackedCache({ strategy: 'cookie', storageHandler: state.storageHandler, - domain: state.domain, + domain: state.domain }) } return { ...state, cache } diff --git a/src/enrichers/call-handler.ts b/src/enrichers/call-handler.ts index 14ccf0a5..d5de5a98 100644 --- a/src/enrichers/call-handler.ts +++ b/src/enrichers/call-handler.ts @@ -1,6 +1,6 @@ -import { CallHandler, EventBus } from "live-connect-common" -import { WrappedCallHandler } from "../handlers/call-handler" -import { Enricher } from "../types" +import { CallHandler, EventBus } from 'live-connect-common' +import { WrappedCallHandler } from '../handlers/call-handler' +import { Enricher } from '../types' type Input = { callHandler: CallHandler, eventBus: EventBus } type Output = { callHandler: WrappedCallHandler } diff --git a/src/enrichers/decisions.ts b/src/enrichers/decisions.ts index 4a6c3eb2..dd7ba1e8 100644 --- a/src/enrichers/decisions.ts +++ b/src/enrichers/decisions.ts @@ -1,70 +1,11 @@ -import { getQueryParameter, ParsedParam } from '../utils/url' -import { trim, isUUID, expiresInDays } from 'live-connect-common' -import { Enricher, EventBus, State } from '../types' +import { Enricher, EventBus } from '../types' import { WrappedStorageHandler } from '../handlers/storage-handler' - -const DEFAULT_DECISION_ID_COOKIE_EXPIRES = expiresInDays(30) -const DECISION_ID_QUERY_PARAM_NAME = 'li_did' -const DECISION_ID_COOKIE_NAMESPACE = 'lidids.' - -const _onlyUnique = (value: string, index: number, self: string[]) => self.indexOf(value) === index -const _nonEmpty = (value: string) => value && trim(value).length > 0 +import { resolve } from '../manager/decisions' type Input = { storageHandler: WrappedStorageHandler, eventBus: EventBus, pageUrl?: string, domain: string } type Output = { decisionIds: string[] } export const enrichDecisionIds: Enricher = state => { - const { storageHandler, eventBus, pageUrl, domain } = state - function _addDecisionId(key: string, cookieDomain?: string) { - if (key) { - storageHandler.setCookie( - `${DECISION_ID_COOKIE_NAMESPACE}${key}`, - key, - DEFAULT_DECISION_ID_COOKIE_EXPIRES, - 'Lax', - cookieDomain) - } - } - - function _orElseEmtpy(errorDescription: string, f: () => A[]): A[] { - try { - return f() - } catch (e) { - eventBus.emitErrorWithMessage('DecisionsResolve', errorDescription, e) - return [] - } - } - - const freshDecisions = _orElseEmtpy( - 'Error while extracting new decision ids', - () => { - const extractedFreshDecisions = ([] as ParsedParam[]).concat((pageUrl && getQueryParameter(pageUrl, DECISION_ID_QUERY_PARAM_NAME)) || []) - return extractedFreshDecisions - .map(trim) - .filter(_nonEmpty) - .filter(isUUID) - .filter(_onlyUnique) - } - ) - - const storedDecisions = _orElseEmtpy( - 'Error while retrieving stored decision ids', - () => { - const extractedStoredDecisions = storageHandler.findSimilarCookies(DECISION_ID_COOKIE_NAMESPACE) - return extractedStoredDecisions.map(trim) - .filter(_nonEmpty) - .filter(isUUID) - .filter(_onlyUnique) - } - ) - - freshDecisions.forEach(decision => { - try { - _addDecisionId(decision, domain) - } catch (e) { - eventBus.emitErrorWithMessage('DecisionsResolve', 'Error while storing new decision id', e) - } - }) - - return { ...state, decisionIds: freshDecisions.concat(storedDecisions).filter(_onlyUnique) } + const { storageHandler, eventBus } = state + return { ...state, ...resolve(state, storageHandler, eventBus) } } diff --git a/src/enrichers/domain.ts b/src/enrichers/domain.ts index 1fbe3875..6430092d 100644 --- a/src/enrichers/domain.ts +++ b/src/enrichers/domain.ts @@ -1,6 +1,6 @@ -import { WrappedStorageHandler } from "../handlers/storage-handler" -import { Enricher } from "../types" -import { determineHighestAccessibleDomain } from "../utils/domain" +import { WrappedStorageHandler } from '../handlers/storage-handler' +import { Enricher } from '../types' +import { determineHighestAccessibleDomain } from '../utils/domain' type Input = { storageHandler: WrappedStorageHandler } type Output = { domain: string } diff --git a/src/enrichers/error-pixel.ts b/src/enrichers/error-pixel.ts deleted file mode 100644 index b3bb6d41..00000000 --- a/src/enrichers/error-pixel.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { EventBus } from "live-connect-common"; -import { Enricher } from "../types"; -import { register } from "../events/error-pixel"; -import { WrappedCallHandler } from "../handlers/call-handler"; - -type Input = { eventBus: EventBus, domain: string, callHandler: WrappedCallHandler } - -export const enrichErrorPixel: Enricher = ({ eventBus, domain, callHandler }) => { - register(eventBus, domain, callHandler) -} diff --git a/src/enrichers/identifiers-nohash.ts b/src/enrichers/identifiers-nohash.ts index df5b44ff..34b32879 100644 --- a/src/enrichers/identifiers-nohash.ts +++ b/src/enrichers/identifiers-nohash.ts @@ -1,10 +1,10 @@ import { containsEmailField, isEmail } from '../utils/email' import { safeToString, isArray, trim } from 'live-connect-common' -import { Enricher, EventBus, RetrievedIdentifier, State } from '../types' +import { Enricher, EventBus, RetrievedIdentifier } from '../types' import { WrappedReadOnlyStorageHandler } from '../handlers/storage-handler' -type Input = {identifiersToResolve: string | string[], storageHandler: WrappedReadOnlyStorageHandler, eventBus: EventBus} -type Output = {retrievedIdentifiers: RetrievedIdentifier[]} +type Input = { identifiersToResolve: string | string[], storageHandler: WrappedReadOnlyStorageHandler, eventBus: EventBus } +type Output = { retrievedIdentifiers: RetrievedIdentifier[] } export const enrichIdentifiers: Enricher = state => { try { diff --git a/src/enrichers/identifiers.ts b/src/enrichers/identifiers.ts index 637e308c..d785e98c 100644 --- a/src/enrichers/identifiers.ts +++ b/src/enrichers/identifiers.ts @@ -1,6 +1,6 @@ import { replaceEmailsWithHashes } from '../utils/email' import { safeToString, isString, isArray } from 'live-connect-common' -import { EventBus, HashedEmail, State, RetrievedIdentifier, Enricher } from '../types' +import { EventBus, HashedEmail, RetrievedIdentifier, Enricher } from '../types' import { WrappedReadOnlyStorageHandler } from '../handlers/storage-handler' type Input = { identifiersToResolve: string | string[], storageHandler: WrappedReadOnlyStorageHandler, eventBus: EventBus } diff --git a/src/enrichers/live-connect-id.ts b/src/enrichers/live-connect-id.ts index d90df1f5..ae96916e 100644 --- a/src/enrichers/live-connect-id.ts +++ b/src/enrichers/live-connect-id.ts @@ -2,7 +2,7 @@ import { ulid } from '../utils/ulid' import { domainHash } from '../utils/hash' import { expiresInDays } from 'live-connect-common' import { PEOPLE_VERIFIED_LS_ENTRY } from '../utils/consts' -import { Enricher, EventBus } from '../types' +import { Enricher } from '../types' import { WrappedStorageHandler } from '../handlers/storage-handler' import { DurableCache } from '../cache' @@ -13,7 +13,7 @@ type Input = { expirationDays?: number, domain: string, cache: DurableCache, sto type Output = { liveConnectId?: string, peopleVerifiedId?: string } export const enrichLiveConnectId: Enricher = state => { - const {expirationDays, domain, storageHandler, cache } = state + const { expirationDays, domain, storageHandler, cache } = state const expiry = expirationDays || DEFAULT_EXPIRATION_DAYS const oldValue = cache.get(NEXT_GEN_FP_NAME)?.data @@ -21,8 +21,7 @@ export const enrichLiveConnectId: Enricher = state => { if (oldValue) { cache.set(NEXT_GEN_FP_NAME, oldValue, expiresInDays(expiry)) } else { - const newValue = `${domainHash(domain)}--${ulid()}` - + const newValue = `${domainHash(domain)}--${ulid()}`.toLocaleLowerCase() cache.set(NEXT_GEN_FP_NAME, newValue, expiresInDays(expiry)) } diff --git a/src/enrichers/page.ts b/src/enrichers/page.ts index aaf6aae7..c5b39e24 100644 --- a/src/enrichers/page.ts +++ b/src/enrichers/page.ts @@ -12,4 +12,3 @@ export const enrichPage: Enricher = state => { contextElements: getContextElements(state.privacyMode, state.contextSelectors, state.contextElementsLength) } } - diff --git a/src/enrichers/people-verified-id.ts b/src/enrichers/people-verified-id.ts index 556916c6..199edeb7 100644 --- a/src/enrichers/people-verified-id.ts +++ b/src/enrichers/people-verified-id.ts @@ -16,4 +16,3 @@ export const enrichPeopleVerifiedId: Enricher = state => { return state } } - diff --git a/src/enrichers/storage-handler.ts b/src/enrichers/storage-handler.ts index 01142bd9..00e1a0a6 100644 --- a/src/enrichers/storage-handler.ts +++ b/src/enrichers/storage-handler.ts @@ -1,9 +1,9 @@ -import { EventBus, ReadOnlyStorageHandler, StorageHandler } from "live-connect-common"; -import { StorageStrategy } from "../model/storage-strategy"; -import { Enricher } from "../types"; -import { WrappedReadOnlyStorageHandler, WrappedStorageHandler } from "../handlers/storage-handler"; +import { EventBus, ReadOnlyStorageHandler, StorageHandler } from 'live-connect-common' +import { StorageStrategy } from '../model/storage-strategy' +import { Enricher } from '../types' +import { WrappedReadOnlyStorageHandler, WrappedStorageHandler } from '../handlers/storage-handler' -type InputReadOnly = { storageStrategy: StorageStrategy, storageHandler: ReadOnlyStorageHandler, eventBus: EventBus } +type InputReadOnly = { storageStrategy: StorageStrategy, storageHandler: ReadOnlyStorageHandler, eventBus: EventBus } type OutputReadOnly = { storageHandler: WrappedReadOnlyStorageHandler } export const enrichReadOnlyStorageHandler: Enricher = state => { @@ -11,7 +11,7 @@ export const enrichReadOnlyStorageHandler: Enricher = state => { diff --git a/src/enrichers/storage-strategy.ts b/src/enrichers/storage-strategy.ts index eba6863a..e15fc569 100644 --- a/src/enrichers/storage-strategy.ts +++ b/src/enrichers/storage-strategy.ts @@ -1,5 +1,5 @@ -import { StorageStrategies, StorageStrategy } from "../model/storage-strategy"; -import { Enricher } from "../types"; +import { StorageStrategies, StorageStrategy } from '../model/storage-strategy' +import { Enricher } from '../types' type Input = { privacyMode: boolean, storageStrategy?: StorageStrategy } type Output = { storageStrategy: StorageStrategy } diff --git a/src/events/error-pixel.ts b/src/events/error-pixel.ts index 67ec486b..f0aa2264 100644 --- a/src/events/error-pixel.ts +++ b/src/events/error-pixel.ts @@ -1,9 +1,7 @@ import { PixelSender } from '../pixel/sender' import { StateWrapper } from '../pixel/state' -import * as page from '../enrichers/page' import { ERRORS_CHANNEL, isRecord, isString } from 'live-connect-common' import { EventBus, State } from '../types' -import { WrappedCallHandler } from '../handlers/call-handler' const MAX_ERROR_FIELD_LENGTH = 120 @@ -51,12 +49,10 @@ export function asErrorDetails(e: unknown): State { } } -export function register(state: {}, callHandler: WrappedCallHandler, eventBus: EventBus): void { +export function register(state: State, sender: PixelSender, eventBus: EventBus): void { try { - const pixelSender = new PixelSender(state, callHandler, eventBus) - eventBus.on(ERRORS_CHANNEL, (error) => { - pixelSender.sendPixel(new StateWrapper(asErrorDetails(error), eventBus).combineWith(state || {})) + sender.sendPixel(new StateWrapper(asErrorDetails(error), eventBus).combineWith(state || {})) }) } catch (e) { console.error('handlers.error.register', e) diff --git a/src/idex.ts b/src/idex.ts index ec6c71bc..8adfc4a8 100644 --- a/src/idex.ts +++ b/src/idex.ts @@ -4,66 +4,27 @@ import { expiresInHours, isFunction, isObject } from 'live-connect-common' import { asParamOrEmpty, asStringParamWhen, asStringParam, mapAsParams } from './utils/params' import { DEFAULT_IDEX_AJAX_TIMEOUT, DEFAULT_IDEX_EXPIRATION_HOURS, DEFAULT_IDEX_URL, DEFAULT_REQUESTED_ATTRIBUTES } from './utils/consts' import { IdentityResolutionConfig, State, ResolutionParams, EventBus, RetrievedIdentifier } from './types' -import { WrappedStorageHandler } from './handlers/storage-handler' import { WrappedCallHandler } from './handlers/call-handler' +import { DurableCache, NoOpCache } from './cache' -export type CacheRecord = { - data: unknown - expiresAt?: Date -} - -interface Cache { - get: (key: unknown) => CacheRecord | null // null will be used to signal missing value - set: (key: unknown, value: unknown, expiration?: Date) => void -} - -function storageHandlerBackedCache(defaultExpirationHours: number, domain: string | undefined, storageHandler: WrappedStorageHandler, eventBus: EventBus): Cache { - const IDEX_STORAGE_KEY = '__li_idex_cache' - - function _cacheKey(rawKey: unknown) { - if (rawKey) { - const suffix = base64UrlEncode(JSON.stringify(rawKey)) - return `${IDEX_STORAGE_KEY}_${suffix}` - } else { - return IDEX_STORAGE_KEY - } - } +const IDEX_STORAGE_KEY = '__li_idex_cache' - return { - get: (key) => { - const cachedValue = storageHandler.get(_cacheKey(key)) - if (cachedValue) { - return { data: JSON.parse(cachedValue.data), expiresAt: cachedValue.expiresAt } - } else { - return null - } - }, - set: (key, value, expiresAt) => { - try { - storageHandler.set( - _cacheKey(key), - JSON.stringify(value), - expiresAt || expiresInHours(defaultExpirationHours), - domain - ) - } catch (ex) { - eventBus.emitError('IdentityResolverStorage', ex) - } - } +function _cacheKey(rawKey: unknown) { + if (rawKey) { + const suffix = base64UrlEncode(JSON.stringify(rawKey)) + return `${IDEX_STORAGE_KEY}_${suffix}` + } else { + return IDEX_STORAGE_KEY } } -const noopCache: Cache = { - get: () => null, - set: () => undefined -} - export class IdentityResolver { eventBus: EventBus calls: WrappedCallHandler - cache: Cache + cache: DurableCache idexConfig: IdentityResolutionConfig externalIds: RetrievedIdentifier[] + defaultExpirationHours: number source: string publisherId: number | string url: string @@ -71,12 +32,18 @@ export class IdentityResolver { requestedAttributes: string[] tuples: [string, string][] - private constructor (config: State, calls: WrappedCallHandler, cache: Cache, eventBus: EventBus) { + private constructor ( + config: State, + calls: WrappedCallHandler, + cache: DurableCache, + eventBus: EventBus + ) { this.eventBus = eventBus this.calls = calls this.cache = cache this.idexConfig = config.identityResolutionConfig || {} this.externalIds = config.retrievedIdentifiers || [] + this.defaultExpirationHours = this.idexConfig.expirationHours || DEFAULT_IDEX_EXPIRATION_HOURS this.source = this.idexConfig.source || 'unknown' this.publisherId = this.idexConfig.publisherId || 'any' this.url = this.idexConfig.url || DEFAULT_IDEX_URL @@ -108,18 +75,31 @@ export class IdentityResolver { }) } - static make(config: State, storageHandler: WrappedStorageHandler, calls: WrappedCallHandler, eventBus: EventBus): IdentityResolver { - const nonNullConfig = config || {} - const idexConfig = nonNullConfig.identityResolutionConfig || {} - const expirationHours = idexConfig.expirationHours || DEFAULT_IDEX_EXPIRATION_HOURS - const domain = nonNullConfig.domain - - const cache = storageHandlerBackedCache(expirationHours, domain, storageHandler, eventBus) + static make( + config: State, + cache: DurableCache, + calls: WrappedCallHandler, + eventBus: EventBus + ): IdentityResolver { + const nonNullConfig = config || { identityResolutionConfig: {} } return new IdentityResolver(nonNullConfig, calls, cache, eventBus) } static makeNoCache(config: State, calls: WrappedCallHandler, eventBus: EventBus): IdentityResolver { - return new IdentityResolver(config || {}, calls, noopCache, eventBus) + return IdentityResolver.make(config || {}, NoOpCache, calls, eventBus) + } + + private getCached(key: unknown) { + const cachedValue = this.cache.get(_cacheKey(key)) + if (cachedValue) { + return { data: JSON.parse(cachedValue.data), expiresAt: cachedValue.expiresAt } + } else { + return null + } + } + + private setCached(key: unknown, value: unknown, expiresAt?: Date) { + this.cache.set(_cacheKey(key), JSON.stringify(value), expiresAt || expiresInHours(this.defaultExpirationHours)) } private responseReceived( @@ -139,13 +119,13 @@ export class IdentityResolver { const expiresAt = responseExpires(response) - this.cache.set(additionalParams, responseObj, expiresAt) + this.setCached(additionalParams, responseObj, expiresAt) successCallback(responseObj, expiresAt) } } unsafeResolve(successCallback: (result: unknown, expiresAt?: Date) => void, errorCallback: () => void, additionalParams: ResolutionParams): void { - const cachedValue = this.cache.get(additionalParams) + const cachedValue = this.getCached(additionalParams) if (cachedValue) { successCallback(cachedValue.data, cachedValue.expiresAt) } else { @@ -164,12 +144,14 @@ export class IdentityResolver { return `${this.url}/${this.source}/${this.publisherId}${params}` } - resolve(successCallback: (result: unknown, expiresAt?: Date) => void, errorCallback: () => void, additionalParams?: ResolutionParams): void { + resolve(successCallback: (result: unknown, expiresAt?: Date) => void, errorCallback?: () => void, additionalParams?: ResolutionParams): void { try { - this.unsafeResolve(successCallback, errorCallback, additionalParams || {}) + this.unsafeResolve(successCallback, errorCallback || (() => {}), additionalParams || {}) } catch (e) { console.error('IdentityResolve', e) - errorCallback() + if (errorCallback && isFunction(errorCallback)) { + errorCallback() + } if (this.eventBus) { this.eventBus.emitError('IdentityResolve', e) } diff --git a/src/manager/decisions.ts b/src/manager/decisions.ts new file mode 100644 index 00000000..a37cad6a --- /dev/null +++ b/src/manager/decisions.ts @@ -0,0 +1,66 @@ +import { getQueryParameter, ParsedParam } from '../utils/url' +import { trim, isUUID, expiresInDays } from 'live-connect-common' +import { EventBus } from '../types' +import { WrappedStorageHandler } from '../handlers/storage-handler' + +const DEFAULT_DECISION_ID_COOKIE_EXPIRES = expiresInDays(30) +const DECISION_ID_QUERY_PARAM_NAME = 'li_did' +const DECISION_ID_COOKIE_NAMESPACE = 'lidids.' + +const _onlyUnique = (value: string, index: number, self: string[]) => self.indexOf(value) === index +const _nonEmpty = (value: string) => value && trim(value).length > 0 + +export function resolve(state: { pageUrl?: string, domain?: string }, storageHandler: WrappedStorageHandler, eventBus: EventBus): { decisionIds: string[] } { + function _addDecisionId(key: string, cookieDomain?: string) { + if (key) { + storageHandler.setCookie( + `${DECISION_ID_COOKIE_NAMESPACE}${key}`, + key, + DEFAULT_DECISION_ID_COOKIE_EXPIRES, + 'Lax', + cookieDomain) + } + } + + function _orElseEmtpy(errorDescription: string, f: () => A[]): A[] { + try { + return f() + } catch (e) { + eventBus.emitErrorWithMessage('DecisionsResolve', errorDescription, e) + return [] + } + } + + const freshDecisions = _orElseEmtpy( + 'Error while extracting new decision ids', + () => { + const extractedFreshDecisions = ([] as ParsedParam[]).concat((state.pageUrl && getQueryParameter(state.pageUrl, DECISION_ID_QUERY_PARAM_NAME)) || []) + return extractedFreshDecisions + .map(trim) + .filter(_nonEmpty) + .filter(isUUID) + .filter(_onlyUnique) + } + ) + + const storedDecisions = _orElseEmtpy( + 'Error while retrieving stored decision ids', + () => { + const extractedStoredDecisions = storageHandler.findSimilarCookies(DECISION_ID_COOKIE_NAMESPACE) + return extractedStoredDecisions.map(trim) + .filter(_nonEmpty) + .filter(isUUID) + .filter(_onlyUnique) + } + ) + + freshDecisions.forEach(decision => { + try { + _addDecisionId(decision, state.domain) + } catch (e) { + eventBus.emitErrorWithMessage('DecisionsResolve', 'Error while storing new decision id', e) + } + }) + + return { decisionIds: freshDecisions.concat(storedDecisions).filter(_onlyUnique) } +} diff --git a/src/pixel/fiddler.ts b/src/pixel/fiddler.ts index 4b28b930..b1f2d5c3 100644 --- a/src/pixel/fiddler.ts +++ b/src/pixel/fiddler.ts @@ -79,11 +79,11 @@ export function mergeObjects(obj1: A, obj2: export class EnrichmentContext { data: A - constructor(state: A) { + constructor (state: A) { this.data = state } via(enricher: Enricher): EnrichmentContext { - return new EnrichmentContext({...this.data, ...enricher(this.data)}) + return new EnrichmentContext({ ...this.data, ...enricher(this.data) }) } } diff --git a/src/pixel/sender.ts b/src/pixel/sender.ts index 0c711b9e..e317582d 100644 --- a/src/pixel/sender.ts +++ b/src/pixel/sender.ts @@ -1,6 +1,6 @@ import { isArray, isFunction } from 'live-connect-common' import { asStringParam } from '../utils/params' -import { LiveConnectConfig, EventBus, Enricher } from '../types' +import { LiveConnectConfig, EventBus } from '../types' import { StateWrapper } from './state' import { WrappedCallHandler } from '../handlers/call-handler' diff --git a/src/pixel/state.ts b/src/pixel/state.ts index 6b3b7672..0b6ee90e 100644 --- a/src/pixel/state.ts +++ b/src/pixel/state.ts @@ -4,7 +4,7 @@ import { fiddle, mergeObjects } from './fiddler' import { isObject, trim, isArray, nonNull } from 'live-connect-common' import { asStringParam, asParamOrEmpty, asStringParamWhen, asStringParamTransform } from '../utils/params' import { toParams } from '../utils/url' -import { Enricher, EventBus, State } from '../types' +import { EventBus, State } from '../types' import { collectUrl } from './url-collector' const noOpEvents = ['setemail', 'setemailhash', 'sethashedemail'] diff --git a/src/standard-live-connect.ts b/src/standard-live-connect.ts index 6dc25f2d..cd579585 100644 --- a/src/standard-live-connect.ts +++ b/src/standard-live-connect.ts @@ -19,6 +19,7 @@ import { enrichDecisionIds } from './enrichers/decisions' import { enrichLiveConnectId } from './enrichers/live-connect-id' import { enrichCache } from './enrichers/cache' import { enrichCallHandler } from './enrichers/call-handler' +import { register as registerErrorPixel } from './events/error-pixel' const hemStore: State = {} function _pushSingleEvent (event: any, pixelClient: PixelSender, enrichedState: StateWrapper, eventBus: EventBus) { @@ -114,6 +115,8 @@ function _standardInitialization (liveConnectConfig: LiveConnectConfig, external const onPixelPreload = () => eventBus.emit(C.PRELOAD_PIXEL, '0') const pixelClient = new PixelSender(enrichedState, enrichedState.callHandler, eventBus, onPixelLoad, onPixelPreload) + registerErrorPixel(enrichedState, pixelClient, eventBus) + const resolver = IdentityResolver.make(enrichedState, enrichedState.storageHandler, enrichedState.callHandler, eventBus) const _push = (...args: any[]) => _processArgs(args, pixelClient, new StateWrapper(enrichedState, enrichedState.eventBus), eventBus) diff --git a/src/utils/domain.ts b/src/utils/domain.ts index 0e9b5cdd..07f389dc 100644 --- a/src/utils/domain.ts +++ b/src/utils/domain.ts @@ -1,5 +1,5 @@ -import { WrappedStorageHandler } from "../handlers/storage-handler" -import { loadedDomain } from "./page" +import { WrappedStorageHandler } from '../handlers/storage-handler' +import { loadedDomain } from './page' const TLD_CACHE_KEY = '_li_dcdm_c' diff --git a/src/utils/wrapping.ts b/src/utils/wrapping.ts index d8015cf5..6bf8cbd0 100644 --- a/src/utils/wrapping.ts +++ b/src/utils/wrapping.ts @@ -22,14 +22,14 @@ export class WrappingContext { if (isObject(this.obj)) { const member = this.obj[functionName] if (isFunction(member)) { - return ((...args) => { + return (...args) => { try { // eslint-disable-next-line @typescript-eslint/no-explicit-any return (member as any).call(this.obj, ...args) } catch (e) { this.eventBus.emitErrorWithMessage(this.name, `Failed calling ${functionName}`, e) } - }) + } } } this.errors.push(functionName) diff --git a/test/unit/cache.spec.ts b/test/unit/cache.spec.ts new file mode 100644 index 00000000..78892a3d --- /dev/null +++ b/test/unit/cache.spec.ts @@ -0,0 +1,186 @@ +import jsdom from 'global-jsdom' +import { expect, use } from 'chai' +import { WrappedStorageHandler } from '../../src/handlers/storage-handler' +import { DefaultStorageHandler } from 'live-connect-handlers' +import sinon, { SinonStub } from 'sinon' +import { EventBus, expiresInDays } from 'live-connect-common' +import dirtyChai from 'dirty-chai' +import { LocalEventBus } from '../../src/events/event-bus' +import { StorageHandlerBackedCache } from '../../src/cache' +import { StorageStrategy } from '../../src/model/storage-strategy' + +use(dirtyChai) + +type RecordedError = { + name: string + message: string + exception?: unknown +} + +describe('StorageHandler', () => { + let errors: RecordedError[] = [] + let eventBusStub: SinonStub<[string, string, unknown?], EventBus> + const eventBus = LocalEventBus() + const storage = new DefaultStorageHandler(eventBus) + + const sandbox = sinon.createSandbox() + beforeEach(() => { + errors = [] + jsdom('', { + url: 'http://www.something.example.com' + }) + + eventBusStub = sandbox.stub(eventBus, 'emitErrorWithMessage').callsFake((name, message, e) => { + errors.push({ + name, + message, + exception: e + }) + return eventBus + }) + }) + + afterEach(() => { + eventBusStub.restore() + }) + + it('should use local storage', () => { + const storageHandler = WrappedStorageHandler.make('ls', storage, eventBus) + const cache = new StorageHandlerBackedCache({ + strategy: 'ls', + storageHandler, + domain: 'example.com' + }) + + cache.set('key', 'value', expiresInDays(1)) + expect(cache.get('key')?.data).to.be.eq('value') + expect(storageHandler.getDataFromLocalStorage('key')).to.be.eq('value') + expect(cache.get('unknownKey')).to.be.null() + }) + + it('should use cookies', () => { + const storageHandler = WrappedStorageHandler.make('cookie', storage, eventBus) + const cache = new StorageHandlerBackedCache({ + strategy: 'cookie', + storageHandler, + domain: 'example.com' + }) + + cache.set('key', 'value', expiresInDays(1)) + expect(cache.get('key')?.data).to.be.eq('value') + expect(storage.getCookie('key')).to.be.eq('value') + expect(cache.get('unknownKey')).to.be.null() + }) + + it('should use cookies when the strategy is not defined', () => { + const storageHandler = WrappedStorageHandler.make(null as unknown as StorageStrategy, storage, eventBus) + const cache = new StorageHandlerBackedCache({ + strategy: null as unknown as 'cookie', + storageHandler, + domain: 'example.com' + }) + + cache.set('key', 'value', expiresInDays(1)) + expect(cache.get('key')?.data).to.be.eq('value') + expect(storage.getCookie('key')).to.be.eq('value') + expect(cache.get('unknownKey')).to.be.null() + }) + + it('should return nothing when the underlying handler\'s strategy is none', () => { + const storageHandler = WrappedStorageHandler.make('none', storage, eventBus) + const cache = new StorageHandlerBackedCache({ + strategy: 'cookie', + storageHandler, + domain: 'example.com' + }) + + cache.set('key', 'value', expiresInDays(1)) + expect(cache.get('key')).to.be.null() + expect(storage.getCookie('key')).to.be.null() + expect(storage.getDataFromLocalStorage('key')).to.be.null() + expect(errors.length).to.be.eq(0) + }) + + it('should return nothing when the underlying handler\'s the strategy is disabled', () => { + const storageHandler = WrappedStorageHandler.make('disabled', storage, eventBus) + const cache = new StorageHandlerBackedCache({ + strategy: 'cookie', + storageHandler, + domain: 'example.com' + }) + + cache.set('key_any', 'value_any', expiresInDays(1)) + storageHandler.setDataInLocalStorage('key_ls', 'value_any') + storageHandler.setCookie('key_cookie', 'value_cookie', expiresInDays(1), 'Lax', 'example.com') + + storageHandler.removeDataFromLocalStorage('key_ls') + + expect(cache.get('key_any')).to.be.null() + expect(storageHandler.getDataFromLocalStorage('key_ls')).to.be.null() + expect(storageHandler.getCookie('key_cookie')).to.be.null() + + expect(storageHandler.findSimilarCookies('key_cookie')).to.be.empty() + expect(storageHandler.localStorageIsEnabled()).to.be.false() + expect(errors.length).to.be.eq(0) + }) + + it('should return nothing when the strategy is ls and the time is in the past', () => { + const storageHandler = WrappedStorageHandler.make('ls', storage, eventBus) + const cache = new StorageHandlerBackedCache({ + strategy: 'ls', + storageHandler, + domain: 'example.com' + }) + + cache.set('key', 'value', expiresInDays(-1)) + expect(cache.get('key')).to.be.null() + expect(storage.getDataFromLocalStorage('key')).to.be.null() + }) + + it('should return nothing when the strategy is cookie and the time is in the past', () => { + const storageHandler = WrappedStorageHandler.make('cookie', storage, eventBus) + const cache = new StorageHandlerBackedCache({ + strategy: 'cookie', + storageHandler, + domain: 'example.com' + }) + + cache.set('key', 'value', expiresInDays(-1)) + expect(cache.get('key')).to.be.null() + expect(storage.getCookie('key')).to.be.null() + }) + + it('should update expiration when overwriting localstorage with expiration with one without', () => { + const storageHandler = WrappedStorageHandler.make('ls', storage, eventBus) + const cache = new StorageHandlerBackedCache({ + strategy: 'ls', + storageHandler, + domain: 'example.com' + }) + + cache.set('key', 'value', expiresInDays(5)) + cache.set('key', 'value', undefined) + + const result = cache.get('key') + + expect(result?.data).to.be.eq('value') + expect(result?.expiresAt).to.be.undefined() + }) + + it('should update expiration when overwriting a cookie with expiration with one without', () => { + const storageHandler = WrappedStorageHandler.make('cookie', storage, eventBus) + const cache = new StorageHandlerBackedCache({ + strategy: 'cookie', + storageHandler, + domain: 'example.com' + }) + + cache.set('key', 'value', expiresInDays(5)) + cache.set('key', 'value', undefined) + + const result = cache.get('key') + + expect(result?.data).to.be.eq('value') + expect(result?.expiresAt).to.be.undefined() + }) +}) diff --git a/test/unit/enricher/domain.spec.ts b/test/unit/enricher/domain.spec.ts new file mode 100644 index 00000000..eeb7b763 --- /dev/null +++ b/test/unit/enricher/domain.spec.ts @@ -0,0 +1,42 @@ +import { expect, use } from 'chai' +import { enrichDomain } from '../../../src/enrichers/domain' +import { DefaultStorageHandler } from 'live-connect-handlers' +import jsdom from 'global-jsdom' +import dirtyChai from 'dirty-chai' +import { WrappedStorageHandler } from '../../../src/handlers/storage-handler' +import { LocalEventBus } from '../../../src/events/event-bus' + +use(dirtyChai) + +const eventBus = LocalEventBus() +const storage = new DefaultStorageHandler(eventBus) +const storageHandler = WrappedStorageHandler.make('cookie', storage, eventBus) +const input = { storageHandler } + +describe('TLD checker', () => { + beforeEach(() => jsdom('', { + url: 'http://subdomain.tests.example.com' + })) + + it('should determine correct tld', () => { + const resolved = enrichDomain(input) + expect(resolved.domain).to.eq('.example.com') + }) + + it('should reuse the cached correct tld', () => { + storageHandler.setCookie('_li_dcdm_c', '.example.com') + const resolved = enrichDomain(input) + expect(resolved.domain).to.eq('.example.com') + }) +}) + +describe('TLD on sub-domain', () => { + beforeEach(() => jsdom('', { + url: 'http://example.co.uk' + })) + + it('should use the full domain', () => { + const resolved = enrichDomain(input) + expect(resolved.domain).to.eq('.example.co.uk') + }) +}) diff --git a/test/unit/enricher/identifiers-nohash.spec.ts b/test/unit/enricher/identifiers-nohash.spec.ts index e9dfb5be..25f81d8f 100644 --- a/test/unit/enricher/identifiers-nohash.spec.ts +++ b/test/unit/enricher/identifiers-nohash.spec.ts @@ -1,15 +1,16 @@ import { expect, use } from 'chai' -import * as identifiersEnricher from '../../../src/enrichers/identifiers-nohash' +import { enrichIdentifiers } from '../../../src/enrichers/identifiers-nohash' import jsdom from 'global-jsdom' import { DefaultStorageHandler } from 'live-connect-handlers' import sinon from 'sinon' import dirtyChai from 'dirty-chai' import { LocalEventBus } from '../../../src/events/event-bus' +import { WrappedStorageHandler } from '../../../src/handlers/storage-handler' use(dirtyChai) const eventBus = LocalEventBus() -const storage = new DefaultStorageHandler(eventBus) +const storageHandler = WrappedStorageHandler.make('cookie', new DefaultStorageHandler(eventBus), eventBus) const COOKIE_NAME = 'sample_cookie' const SIMPLE_COOKIE1 = 'sample_value1' const SIMPLE_COOKIE2 = 'sample_value2' @@ -23,133 +24,106 @@ describe('IdentifiersNoHashEnricher', () => { })) afterEach(() => { - storage.setCookie(COOKIE_NAME, '') - storage.removeDataFromLocalStorage(COOKIE_NAME) + storageHandler.setCookie(COOKIE_NAME, '') + storageHandler.removeDataFromLocalStorage(COOKIE_NAME) }) it('should return an empty result when the collecting identifiers config is not set', () => { - const state = {} - // @ts-expect-error - const result = identifiersEnricher.enrich(state, storage) - expect(result).to.eql({ - retrievedIdentifiers: [] - }) + const result = enrichIdentifiers({ identifiersToResolve: [], storageHandler, eventBus }) + expect(result.retrievedIdentifiers).to.eql([]) }) it('should return an empty result when the collecting identifiers config is set but there are no cookies', () => { - const state = { identifiersToResolve: [COOKIE_NAME] } - // @ts-expect-error - const result = identifiersEnricher.enrich(state, storage) - expect(result).to.eql({ - retrievedIdentifiers: [] - }) + const state = { identifiersToResolve: [COOKIE_NAME], storageHandler, eventBus } + const result = enrichIdentifiers(state) + expect(result.retrievedIdentifiers).to.eql([]) }) it('should return the collected cookies when the identifiers config is a string', () => { - storage.setCookie(COOKIE_NAME, SIMPLE_COOKIE1) - const state = { identifiersToResolve: `random_name, ${COOKIE_NAME} ` } - - // @ts-expect-error - const result = identifiersEnricher.enrich(state, storage) - - expect(result).to.eql({ - retrievedIdentifiers: [{ - name: COOKIE_NAME, - value: SIMPLE_COOKIE1 - }] - }) + storageHandler.setCookie(COOKIE_NAME, SIMPLE_COOKIE1) + const state = { identifiersToResolve: `random_name, ${COOKIE_NAME} `, storageHandler, eventBus } + + const result = enrichIdentifiers(state) + + expect(result.retrievedIdentifiers).to.eql([{ + name: COOKIE_NAME, + value: SIMPLE_COOKIE1 + }]) }) it('should return the collected cookies', () => { - storage.setCookie(COOKIE_NAME, SIMPLE_COOKIE1) - const state = { identifiersToResolve: [COOKIE_NAME] } - - // @ts-expect-error - const result = identifiersEnricher.enrich(state, storage) - - expect(result).to.eql({ - retrievedIdentifiers: [{ - name: COOKIE_NAME, - value: SIMPLE_COOKIE1 - }] - }) + storageHandler.setCookie(COOKIE_NAME, SIMPLE_COOKIE1) + const state = { identifiersToResolve: [COOKIE_NAME], storageHandler, eventBus } + + const result = enrichIdentifiers(state) + + expect(result.retrievedIdentifiers).to.eql([{ + name: COOKIE_NAME, + value: SIMPLE_COOKIE1 + }]) }) it('should return the collected identifiers from local storage ', () => { - storage.setDataInLocalStorage(COOKIE_NAME, SIMPLE_COOKIE2) - const state = { identifiersToResolve: [COOKIE_NAME] } - - // @ts-expect-error - const result = identifiersEnricher.enrich(state, storage) - - expect(result).to.eql({ - retrievedIdentifiers: [{ - name: COOKIE_NAME, - value: SIMPLE_COOKIE2 - }] - }) + storageHandler.setDataInLocalStorage(COOKIE_NAME, SIMPLE_COOKIE2) + const state = { identifiersToResolve: [COOKIE_NAME], storageHandler, eventBus } + + const result = enrichIdentifiers(state) + + expect(result.retrievedIdentifiers).to.eql([{ + name: COOKIE_NAME, + value: SIMPLE_COOKIE2 + }]) }) it('should prefer the cookie storage to the local storage', () => { - storage.setCookie(COOKIE_NAME, SIMPLE_COOKIE1) - storage.setDataInLocalStorage(COOKIE_NAME, SIMPLE_COOKIE2) - const state = { identifiersToResolve: [COOKIE_NAME] } - - // @ts-expect-error - const result = identifiersEnricher.enrich(state, storage) - - expect(result).to.eql({ - retrievedIdentifiers: [{ - name: COOKIE_NAME, - value: SIMPLE_COOKIE1 - }] - }) + storageHandler.setCookie(COOKIE_NAME, SIMPLE_COOKIE1) + storageHandler.setDataInLocalStorage(COOKIE_NAME, SIMPLE_COOKIE2) + const state = { identifiersToResolve: [COOKIE_NAME], storageHandler, eventBus } + + const result = enrichIdentifiers(state) + + expect(result.retrievedIdentifiers).to.eql([{ + name: COOKIE_NAME, + value: SIMPLE_COOKIE1 + }]) }) it('should not return hashes when the cookie is an email', () => { - storage.setCookie(COOKIE_NAME, EMAIL) - const state = { identifiersToResolve: [COOKIE_NAME] } + storageHandler.setCookie(COOKIE_NAME, EMAIL) + const state = { identifiersToResolve: [COOKIE_NAME], storageHandler, eventBus } - // @ts-expect-error - const result = identifiersEnricher.enrich(state, storage) + const result = enrichIdentifiers(state) - expect(result).to.eql({ - retrievedIdentifiers: [] - }) + expect(result.retrievedIdentifiers).to.eql([]) }) it('should not return hashes when the cookie is a json with an email', () => { - storage.setCookie(COOKIE_NAME, `"username":"${EMAIL}"`) - const state = { identifiersToResolve: [COOKIE_NAME] } + storageHandler.setCookie(COOKIE_NAME, `"username":"${EMAIL}"`) + const state = { identifiersToResolve: [COOKIE_NAME], storageHandler, eventBus } - // @ts-expect-error - const result = identifiersEnricher.enrich(state, storage) + const result = enrichIdentifiers(state) - expect(result).to.eql({ - retrievedIdentifiers: [] - }) + expect(result.retrievedIdentifiers).to.eql([]) }) it('should not return multiple hashes when the cookie is a json with an email', () => { - storage.setCookie(COOKIE_NAME, `"username":"${EMAIL}","username2":"${EMAIL2}"`) - const state = { identifiersToResolve: [COOKIE_NAME] } + storageHandler.setCookie(COOKIE_NAME, `"username":"${EMAIL}","username2":"${EMAIL2}"`) + const state = { identifiersToResolve: [COOKIE_NAME], storageHandler, eventBus } - // @ts-expect-error - const result = identifiersEnricher.enrich(state, storage) + const result = enrichIdentifiers(state) - expect(result).to.eql({ - retrievedIdentifiers: [] - }) + expect(result.retrievedIdentifiers).to.eql([]) }) it('should emit an error and emit an empty result if cookies enrichment fails', () => { - const getCookieStub = sandbox.stub(storage, 'getCookie').throws() - storage.setCookie(COOKIE_NAME, SIMPLE_COOKIE1) - const state = { identifiersToResolve: [COOKIE_NAME] } - // @ts-expect-error - const resolutionResult = identifiersEnricher.enrich(state, storage, eventBus) + const getCookieStub = sandbox.stub(storageHandler, 'getCookie').throws() + storageHandler.setCookie(COOKIE_NAME, SIMPLE_COOKIE1) + const state = { identifiersToResolve: [COOKIE_NAME], storageHandler, eventBus } + + const resolutionResult = enrichIdentifiers(state) + + expect(resolutionResult.retrievedIdentifiers).to.eql([]) - expect(resolutionResult).to.eql({}) getCookieStub.restore() }) }) diff --git a/test/unit/enricher/identifiers.spec.ts b/test/unit/enricher/identifiers.spec.ts index 601ab928..c139d280 100644 --- a/test/unit/enricher/identifiers.spec.ts +++ b/test/unit/enricher/identifiers.spec.ts @@ -1,5 +1,5 @@ import { expect, use } from 'chai' -import * as identifiersEnricher from '../../../src/enrichers/identifiers' +import { enrichIdentifiers } from '../../../src/enrichers/identifiers' import jsdom from 'global-jsdom' import { DefaultStorageHandler } from 'live-connect-handlers' import sinon from 'sinon' @@ -10,7 +10,7 @@ import { LocalEventBus } from '../../../src/events/event-bus' use(dirtyChai) const eventBus = LocalEventBus() -const storage = WrappedStorageHandler.make('cookie', new DefaultStorageHandler(eventBus), eventBus) +const storageHandler = WrappedStorageHandler.make('cookie', new DefaultStorageHandler(eventBus), eventBus) const COOKIE_NAME = 'sample_cookie' const SIMPLE_COOKIE1 = 'sample_value1' @@ -35,174 +35,156 @@ describe('IdentifiersEnricher', () => { })) afterEach(() => { - storage.setCookie(COOKIE_NAME, '') - storage.removeDataFromLocalStorage(COOKIE_NAME) + storageHandler.setCookie(COOKIE_NAME, '') + storageHandler.removeDataFromLocalStorage(COOKIE_NAME) }) it('should return an empty result when the collecting identifiers config is not set', () => { - const state = {} - // @ts-expect-error - const result = identifiersEnricher.enrich(state, storage) - expect(result).to.eql({ - retrievedIdentifiers: [], - hashesFromIdentifiers: [] - }) + const result = enrichIdentifiers({ identifiersToResolve: [], storageHandler, eventBus }) + + expect(result.retrievedIdentifiers).to.eql([]) + expect(result.hashesFromIdentifiers).to.eql([]) }) it('should return an empty result when the collecting identifiers config is set but there are no cookies', () => { - const state = { identifiersToResolve: [COOKIE_NAME] } - // @ts-expect-error - const result = identifiersEnricher.enrich(state, storage) - expect(result).to.eql({ - retrievedIdentifiers: [], - hashesFromIdentifiers: [] - }) + const state = { identifiersToResolve: [COOKIE_NAME], storageHandler, eventBus } + const result = enrichIdentifiers(state) + + expect(result.retrievedIdentifiers).to.eql([]) + expect(result.hashesFromIdentifiers).to.eql([]) }) it('should return the collected cookies when the identifiers config is a string', () => { - storage.setCookie(COOKIE_NAME, SIMPLE_COOKIE1) - const state = { identifiersToResolve: `random_name, ${COOKIE_NAME} ` } + storageHandler.setCookie(COOKIE_NAME, SIMPLE_COOKIE1) + const state = { identifiersToResolve: `random_name, ${COOKIE_NAME} `, storageHandler, eventBus } - // @ts-expect-error - const result = identifiersEnricher.enrich(state, storage) + const result = enrichIdentifiers(state) - expect(result).to.eql({ - retrievedIdentifiers: [{ - name: COOKIE_NAME, - value: SIMPLE_COOKIE1 - }], - hashesFromIdentifiers: [] - }) + expect(result.retrievedIdentifiers).to.eql([{ + name: COOKIE_NAME, + value: SIMPLE_COOKIE1 + }]) + + expect(result.hashesFromIdentifiers).to.eql([]) }) it('should return the collected cookies', () => { - storage.setCookie(COOKIE_NAME, SIMPLE_COOKIE1) - const state = { identifiersToResolve: [COOKIE_NAME] } + storageHandler.setCookie(COOKIE_NAME, SIMPLE_COOKIE1) + const state = { identifiersToResolve: [COOKIE_NAME], storageHandler, eventBus } - // @ts-expect-error - const result = identifiersEnricher.enrich(state, storage) + const result = enrichIdentifiers(state) - expect(result).to.eql({ - retrievedIdentifiers: [{ - name: COOKIE_NAME, - value: SIMPLE_COOKIE1 - }], - hashesFromIdentifiers: [] - }) + expect(result.retrievedIdentifiers).to.eql([{ + name: COOKIE_NAME, + value: SIMPLE_COOKIE1 + }]) + + expect(result.hashesFromIdentifiers).to.eql([]) }) it('should return the collected identifiers from local storage ', () => { - storage.setDataInLocalStorage(COOKIE_NAME, SIMPLE_COOKIE2) - const state = { identifiersToResolve: [COOKIE_NAME] } + storageHandler.setDataInLocalStorage(COOKIE_NAME, SIMPLE_COOKIE2) + const state = { identifiersToResolve: [COOKIE_NAME], storageHandler, eventBus } - // @ts-expect-error - const result = identifiersEnricher.enrich(state, storage) + const result = enrichIdentifiers(state) - expect(result).to.eql({ - retrievedIdentifiers: [{ - name: COOKIE_NAME, - value: SIMPLE_COOKIE2 - }], - hashesFromIdentifiers: [] - }) + expect(result.retrievedIdentifiers).to.eql([{ + name: COOKIE_NAME, + value: SIMPLE_COOKIE2 + }]) + + expect(result.hashesFromIdentifiers).to.eql([]) }) it('should prefer the cookie storage to the local storage', () => { - storage.setCookie(COOKIE_NAME, SIMPLE_COOKIE1) - storage.setDataInLocalStorage(COOKIE_NAME, SIMPLE_COOKIE2) - const state = { identifiersToResolve: [COOKIE_NAME] } + storageHandler.setCookie(COOKIE_NAME, SIMPLE_COOKIE1) + storageHandler.setDataInLocalStorage(COOKIE_NAME, SIMPLE_COOKIE2) + const state = { identifiersToResolve: [COOKIE_NAME], storageHandler, eventBus } - // @ts-expect-error - const result = identifiersEnricher.enrich(state, storage) + const result = enrichIdentifiers(state) - expect(result).to.eql({ - retrievedIdentifiers: [{ - name: COOKIE_NAME, - value: SIMPLE_COOKIE1 - }], - hashesFromIdentifiers: [] - }) + expect(result.retrievedIdentifiers).to.eql([{ + name: COOKIE_NAME, + value: SIMPLE_COOKIE1 + }]) + + expect(result.hashesFromIdentifiers).to.eql([]) }) it('should return hashes when the cookie is an email', () => { - storage.setCookie(COOKIE_NAME, EMAIL) - const state = { identifiersToResolve: [COOKIE_NAME] } + storageHandler.setCookie(COOKIE_NAME, EMAIL) + const state = { identifiersToResolve: [COOKIE_NAME], storageHandler, eventBus } - // @ts-expect-error - const result = identifiersEnricher.enrich(state, storage) + const result = enrichIdentifiers(state) - expect(result).to.eql({ - retrievedIdentifiers: [{ - name: COOKIE_NAME, - value: EMAIL_HASHES.md5 - }], - hashesFromIdentifiers: [EMAIL_HASHES] - }) + expect(result.retrievedIdentifiers).to.eql([{ + name: COOKIE_NAME, + value: EMAIL_HASHES.md5 + }]) + + expect(result.hashesFromIdentifiers).to.eql([EMAIL_HASHES]) }) it('should return hashes when the cookie is a json with an email', () => { - storage.setCookie(COOKIE_NAME, `"username":"${EMAIL}"`) - const state = { identifiersToResolve: [COOKIE_NAME] } + storageHandler.setCookie(COOKIE_NAME, `"username":"${EMAIL}"`) + const state = { identifiersToResolve: [COOKIE_NAME], storageHandler, eventBus } - // @ts-expect-error - const result = identifiersEnricher.enrich(state, storage) + const result = enrichIdentifiers(state) - expect(result).to.eql({ - retrievedIdentifiers: [{ - name: COOKIE_NAME, - value: `"username":"${EMAIL_HASHES.md5}"` - }], - hashesFromIdentifiers: [EMAIL_HASHES] - }) + expect(result.retrievedIdentifiers).to.eql([{ + name: COOKIE_NAME, + value: `"username":"${EMAIL_HASHES.md5}"` + }]) + + expect(result.hashesFromIdentifiers).to.eql([EMAIL_HASHES]) }) it('should return multiple hashes when the cookie is a json with an email', () => { - storage.setCookie(COOKIE_NAME, `"username":"${EMAIL}","username2":"${EMAIL2}"`) - const state = { identifiersToResolve: [COOKIE_NAME] } + storageHandler.setCookie(COOKIE_NAME, `"username":"${EMAIL}","username2":"${EMAIL2}"`) + const state = { identifiersToResolve: [COOKIE_NAME], storageHandler, eventBus } - // @ts-expect-error - const result = identifiersEnricher.enrich(state, storage) + const result = enrichIdentifiers(state) - expect(result).to.eql({ - retrievedIdentifiers: [{ - name: COOKIE_NAME, - value: `"username":"${EMAIL_HASHES.md5}","username2":"${EMAIL2_HASHES.md5}"` - }], - hashesFromIdentifiers: [EMAIL_HASHES, EMAIL2_HASHES] - }) + expect(result.retrievedIdentifiers).to.eql([{ + name: COOKIE_NAME, + value: `"username":"${EMAIL_HASHES.md5}","username2":"${EMAIL2_HASHES.md5}"` + }]) + + expect(result.hashesFromIdentifiers).to.eql([EMAIL_HASHES, EMAIL2_HASHES]) }) it('should return cookies and deduplicated hashes', () => { const COOKIE2_NAME = `${COOKIE_NAME}2` - storage.setCookie(COOKIE_NAME, `"username":"${EMAIL}"`) - storage.setDataInLocalStorage(COOKIE2_NAME, EMAIL) - const state = { identifiersToResolve: [COOKIE_NAME, COOKIE2_NAME] } - - // @ts-expect-error - const result = identifiersEnricher.enrich(state, storage) - - expect(result).to.eql({ - retrievedIdentifiers: [ - { - name: COOKIE_NAME, - value: `"username":"${EMAIL_HASHES.md5}"` - }, - { - name: COOKIE2_NAME, - value: EMAIL_HASHES.md5 - } - ], - hashesFromIdentifiers: [EMAIL_HASHES] - }) + storageHandler.setCookie(COOKIE_NAME, `"username":"${EMAIL}"`) + storageHandler.setDataInLocalStorage(COOKIE2_NAME, EMAIL) + + const state = { identifiersToResolve: [COOKIE_NAME, COOKIE2_NAME], storageHandler, eventBus } + + const result = enrichIdentifiers(state) + + expect(result.retrievedIdentifiers).to.eql([ + { + name: COOKIE_NAME, + value: `"username":"${EMAIL_HASHES.md5}"` + }, + { + name: COOKIE2_NAME, + value: EMAIL_HASHES.md5 + } + ]) + + expect(result.hashesFromIdentifiers).to.eql([EMAIL_HASHES]) }) it('should emit an error and emit an empty result if cookies enrichment fails', () => { - const getCookieStub = sandbox.stub(storage, 'getCookie').throws() - storage.setCookie(COOKIE_NAME, SIMPLE_COOKIE1) - const state = { identifiersToResolve: [COOKIE_NAME] } - const resolutionResult = identifiersEnricher.enrich(state, storage, eventBus) + const getCookieStub = sandbox.stub(storageHandler, 'getCookie').throws() + storageHandler.setCookie(COOKIE_NAME, SIMPLE_COOKIE1) + const state = { identifiersToResolve: [COOKIE_NAME], storageHandler, eventBus } + + const result = enrichIdentifiers(state) - expect(resolutionResult).to.eql({}) + expect(result.retrievedIdentifiers).to.eql([]) + expect(result.hashesFromIdentifiers).to.eql([]) getCookieStub.restore() }) }) diff --git a/test/unit/enricher/live-connect-id.spec.ts b/test/unit/enricher/live-connect-id.spec.ts new file mode 100644 index 00000000..000f68d2 --- /dev/null +++ b/test/unit/enricher/live-connect-id.spec.ts @@ -0,0 +1,113 @@ +import { expect, use } from 'chai' +import { enrichLiveConnectId } from '../../../src/enrichers/live-connect-id' +import { DefaultStorageHandler } from 'live-connect-handlers' +import sinon from 'sinon' +import jsdom from 'global-jsdom' +import dirtyChai from 'dirty-chai' +import { WrappedStorageHandler } from '../../../src/handlers/storage-handler' +import { LocalEventBus } from '../../../src/events/event-bus' +import { NoOpCache, StorageHandlerBackedCache } from '../../../src/cache' +import { StorageStrategies, StorageStrategy } from '../../../src/model/storage-strategy' +import { expiresInHours } from 'live-connect-common' + +use(dirtyChai) + +function makeDeps(strategy: StorageStrategy = StorageStrategies.cookie) { + const domain = '.example.com' + const eventBus = LocalEventBus() + const storage = new DefaultStorageHandler(eventBus) + const storageHandler = WrappedStorageHandler.make(strategy, storage, eventBus) + + let cache + if (strategy === 'cookie' || strategy === 'ls') { + cache = new StorageHandlerBackedCache({ strategy, storageHandler, domain }) + } else { + cache = NoOpCache + } + return { storageHandler, cache, eventBus, domain } +} + +describe('LiveConnectIdEnricher', () => { + sinon.createSandbox() + + beforeEach(() => { + jsdom('', { + url: 'http://www.example.com' + }) + }) + + it('should create a first party cookie if it doesn\'t exist', () => { + const deps = makeDeps('cookie') + const { storageHandler } = deps + + expect(storageHandler.getCookie('_lc2_fpi')).to.eql(null) + + const resolutionResult = enrichLiveConnectId(deps) + + expect(storageHandler.getCookie('_lc2_fpi')).to.eql(resolutionResult.liveConnectId) + expect(storageHandler.getDataFromLocalStorage('_li_duid')).to.eql(resolutionResult.liveConnectId) + }) + + it('should create a first party cookie if it doesn\'t exist, and storage strategy is cookie', () => { + const deps = makeDeps('cookie') + const { storageHandler } = deps + + expect(storageHandler.getCookie('_lc2_fpi')).to.eql(null) + const resolutionResult = enrichLiveConnectId(deps) + expect(storageHandler.getCookie('_lc2_fpi')).to.eql(resolutionResult.liveConnectId) + expect(storageHandler.getDataFromLocalStorage('_li_duid')).to.eql(resolutionResult.liveConnectId) + }) + + it('should create a first party identifier in local storage if it doesn\'t exist, and storage strategy is ls', () => { + const deps = makeDeps('ls') + const { storageHandler } = deps + + expect(storageHandler.getDataFromLocalStorage('_lc2_fpi')).to.eql(null) + + const resolutionResult = enrichLiveConnectId(deps) + + expect(storageHandler.getDataFromLocalStorage('_lc2_fpi')).to.eql(resolutionResult.liveConnectId) + expect(storageHandler.getDataFromLocalStorage('_lc2_fpi_exp')).to.be.not.null() + expect(storageHandler.getDataFromLocalStorage('_li_duid')).to.eql(resolutionResult.liveConnectId) + }) + + it('should not create or return a first party identifier if the StorageStrategy is set to "none"', () => { + const deps = makeDeps('none') + + const resolutionResult = enrichLiveConnectId(deps) + + expect(resolutionResult.liveConnectId).to.be.undefined() + expect(resolutionResult.peopleVerifiedId).to.be.undefined() + }) + + it('should re-use a first party cookie if it exist', () => { + const deps = makeDeps('cookie') + const { storageHandler } = deps + + const id = 'xxxxx' + storageHandler.setCookie('_lc2_fpi', id, expiresInHours(4), undefined, '.example.com') + + const resolutionResult = enrichLiveConnectId(deps) + expect(storageHandler.getCookie('_lc2_fpi')).to.eql(id) + expect(resolutionResult.liveConnectId).to.eql(id) + }) + + it('should create a first party cookie that starts with apex domain hash', () => { + const deps = makeDeps('cookie') + const { storageHandler } = deps + + enrichLiveConnectId(deps) + + // apexOfExampleCom = '0caaf24ab1a0' + expect(storageHandler.getCookie('_lc2_fpi')).to.match(/0caaf24ab1a0--.*/) + }) + + it('should create a first party cookie that is lowercased', () => { + const deps = makeDeps('cookie') + const { storageHandler } = deps + + enrichLiveConnectId(deps) + + expect(storageHandler.getCookie('_lc2_fpi')).to.satisfy((cookie: string) => cookie === cookie.toLowerCase()) + }) +}) diff --git a/test/unit/enricher/page.spec.ts b/test/unit/enricher/page.spec.ts index 00284bb2..2f0e1f95 100644 --- a/test/unit/enricher/page.spec.ts +++ b/test/unit/enricher/page.spec.ts @@ -1,6 +1,6 @@ import jsdom from 'global-jsdom' import { expect, use } from 'chai' -import * as pageEnricher from '../../../src/enrichers/page' +import { enrichPage } from '../../../src/enrichers/page' import dirtyChai from 'dirty-chai' use(dirtyChai) @@ -26,15 +26,15 @@ describe('PageEnricher', () => { document.body.appendChild(newP) const state = { + privacyMode: false, contextSelectors: 'h1,p', contextElementsLength: 1000 } const encodedContextElements = 'PGgxPlNvbWUgaGVhZGVyPC9oMT48cD5tYWlsdG86NTYzNGZmMTNmOTUzZWJjYjM3NGFjOGMzNDliY2ZjZmUsIGFsc28gZm91bmQ6IGYxMzdlM2QwOTg5ODc3ZWIzZjU3NWRjOWNkNmRmZDBkICE8L3A-' - const result = pageEnricher.enrich(state) - expect(result).to.eql({ - pageUrl: url, - referrer, - contextElements: encodedContextElements - }) + const result = enrichPage(state) + + expect(result.pageUrl).to.eql(url) + expect(result.referrer).to.eql(referrer) + expect(result.contextElements).to.eql(encodedContextElements) }) }) diff --git a/test/unit/enricher/privacy-config.spec.ts b/test/unit/enricher/privacy-config.spec.ts index c09a2122..0fd633c8 100644 --- a/test/unit/enricher/privacy-config.spec.ts +++ b/test/unit/enricher/privacy-config.spec.ts @@ -1,7 +1,6 @@ import { expect, use } from 'chai' -import * as pageEnricher from '../../../src/enrichers/privacy-config' +import { enrichPrivacyMode } from '../../../src/enrichers/privacy-config' import dirtyChai from 'dirty-chai' -import { State } from '../../../src/types' use(dirtyChai) @@ -10,42 +9,35 @@ describe('PrivacyConfigEnricher', () => { const state = { gdprApplies: true } - const result = pageEnricher.enrich(state) + const result = enrichPrivacyMode(state) - expect(result).to.eql({ - privacyMode: true - }) + expect(result.privacyMode).to.be.true() }) it('should return empty enrichment when gdprApplies is undefined', () => { - const state = { - someTestField: 'a' - } as State - const result = pageEnricher.enrich(state) + const state = { someTestField: 'a', gdprApplies: undefined } + + const result = enrichPrivacyMode(state) - expect(result).to.eql({}) + expect(result.privacyMode).to.be.false() }) it('should return privacyMode set to true when gdprApplies is defined but is not a boolean value', () => { const state = { - gdprApplies: 'a' + gdprApplies: 'a' as unknown as boolean } - // @ts-expect-error - const result = pageEnricher.enrich(state) - expect(result).to.eql({ - privacyMode: true - }) + const result = enrichPrivacyMode(state) + + expect(result.privacyMode).to.be.true() }) it('should return privacyMode set to false when gdprApplies is false', () => { const state = { gdprApplies: false } - const result = pageEnricher.enrich(state) + const result = enrichPrivacyMode(state) - expect(result).to.eql({ - privacyMode: false - }) + expect(result.privacyMode).to.be.false() }) }) diff --git a/test/unit/events/error-pixel.spec.ts b/test/unit/events/error-pixel.spec.ts index 094f2d4e..7bc4a27d 100644 --- a/test/unit/events/error-pixel.spec.ts +++ b/test/unit/events/error-pixel.spec.ts @@ -6,7 +6,6 @@ import { PixelSender } from '../../../src/pixel/sender' import { LocalEventBus } from '../../../src/events/event-bus' import { ERRORS_CHANNEL, EventBus } from 'live-connect-common' import dirtyChai from 'dirty-chai' -import { WrappedCallHandler } from '../../../src/handlers/call-handler' import { StateWrapper } from '../../../src/pixel/state' use(dirtyChai) @@ -29,7 +28,9 @@ describe('ErrorPixel', () => { }) it('should register itself on the global bus', () => { - errorPixel.register({ collectorUrl: 'http://localhost' }, {} as WrappedCallHandler, eventBus) + const pixelSender = {} as PixelSender + + errorPixel.register({ collectorUrl: 'http://localhost' }, pixelSender, eventBus) // @ts-expect-error const errorHandler = eventBus.data.h expect(errorHandler).to.have.key(ERRORS_CHANNEL) @@ -37,9 +38,14 @@ describe('ErrorPixel', () => { }) it('should call the pixel once registered', () => { - sandbox.stub(PixelSender.prototype, 'sendPixel').callsFake((data: StateWrapper) => errors.push(data)) + const pixelSender = { + sendPixel: (data: StateWrapper) => errors.push(data) + } as unknown as PixelSender - errorPixel.register({ collectorUrl: 'http://localhost' }, {} as WrappedCallHandler, eventBus) + errorPixel.register({ + collectorUrl: 'http://localhost', + pageUrl: 'https://www.example.com/?sad=0&dsad=iou' + }, pixelSender, eventBus) eventBus.emitErrorWithMessage('Error', 'some other message') expect(errors.length).to.eql(1) const errorDetails = errors[0].data.errorDetails diff --git a/test/unit/handlers/storage-handler.spec.ts b/test/unit/handlers/storage-handler.spec.ts index ee7d7efe..7d3295d1 100644 --- a/test/unit/handlers/storage-handler.spec.ts +++ b/test/unit/handlers/storage-handler.spec.ts @@ -1,9 +1,8 @@ import jsdom from 'global-jsdom' import { expect, use } from 'chai' import { WrappedStorageHandler } from '../../../src/handlers/storage-handler' -import { DefaultStorageHandler } from 'live-connect-handlers' import sinon, { SinonStub } from 'sinon' -import { EventBus, expiresInDays } from 'live-connect-common' +import { EventBus } from 'live-connect-common' import dirtyChai from 'dirty-chai' import { LocalEventBus } from '../../../src/events/event-bus' @@ -19,7 +18,6 @@ describe('StorageHandler', () => { let errors: RecordedError[] = [] let eventBusStub: SinonStub<[string, string, unknown?], EventBus> const eventBus = LocalEventBus() - const storage = new DefaultStorageHandler(eventBus) const sandbox = sinon.createSandbox() beforeEach(() => { @@ -62,100 +60,4 @@ describe('StorageHandler', () => { WrappedStorageHandler.make('disabled', {}, eventBus) expect(errors.length).to.be.eq(0) }) - - it('should use local storage', () => { - const storageHandler = WrappedStorageHandler.make('ls', storage, eventBus) - storageHandler.set('key', 'value', expiresInDays(1), 'example.com') - expect(storageHandler.get('key')?.data).to.be.eq('value') - expect(storage.getDataFromLocalStorage('key')).to.be.eq('value') - expect(storageHandler.get('unknownKey')).to.be.null() - }) - - it('should use cookies', () => { - const storageHandler = WrappedStorageHandler.make('cookie', storage, eventBus) - storageHandler.set('key', 'value', expiresInDays(1), 'example.com') - expect(storageHandler.get('key')?.data).to.be.eq('value') - expect(storage.getCookie('key')).to.be.eq('value') - expect(storageHandler.get('unknownKey')).to.be.null() - }) - - it('should use cookies when the strategy is not defined', () => { - // @ts-expect-error - const storageHandler = WrappedStorageHandler.make(null, storage, eventBus) - storageHandler.set('key', 'value', expiresInDays(1), 'example.com') - expect(storageHandler.get('key')?.data).to.be.eq('value') - expect(storage.getCookie('key')).to.be.eq('value') - expect(storageHandler.get('unknownKey')).to.be.null() - }) - - it('should return nothing when the strategy is none', () => { - const storageHandler = WrappedStorageHandler.make('none', storage, eventBus) - storageHandler.set('key', 'value', expiresInDays(1), 'example.com') - expect(storageHandler.get('key')).to.be.null() - expect(storage.getCookie('key')).to.be.null() - expect(storage.getDataFromLocalStorage('key')).to.be.null() - expect(errors.length).to.be.eq(0) - }) - - it('should return nothing when the strategy is disabled', () => { - const storageHandler = WrappedStorageHandler.make('disabled', storage, eventBus) - - storageHandler.set('key_any', 'value_any', expiresInDays(1), 'example.com') - storageHandler.setDataInLocalStorage('key_ls', 'value_any') - storageHandler.setCookie('key_cookie', 'value_cookie', expiresInDays(1), 'Lax', 'example.com') - - storageHandler.removeDataFromLocalStorage('key_ls') - - expect(storageHandler.get('key_any')).to.be.null() - expect(storageHandler.getDataFromLocalStorage('key_ls')).to.be.null() - expect(storageHandler.getCookie('key_cookie')).to.be.null() - - expect(storageHandler.findSimilarCookies('key_cookie')).to.be.empty() - expect(storageHandler.localStorageIsEnabled()).to.be.false() - expect(errors.length).to.be.eq(0) - }) - - it('should return nothing when the strategy is ls and the time is in the past', () => { - const storageHandler = WrappedStorageHandler.make('ls', storage, eventBus) - storageHandler.set('key', 'value', expiresInDays(-1), 'example.com') - expect(storageHandler.get('key')).to.be.null() - expect(storage.getDataFromLocalStorage('key')).to.be.null() - }) - - it('should return nothing when the strategy is cookie and the time is in the past', () => { - const storageHandler = WrappedStorageHandler.make('cookie', storage, eventBus) - storageHandler.set('key', 'value', expiresInDays(-1), 'example.com') - expect(storageHandler.get('key')).to.be.null() - expect(storage.getCookie('key')).to.be.null() - }) - - it('should return nothing when the strategy is undefined and the time is in the past', () => { - // @ts-expect-error - const storageHandler = WrappedStorageHandler.make(null, storage, eventBus) - storageHandler.set('key', 'value', expiresInDays(-1), 'example.com') - expect(storageHandler.get('key')).to.be.null() - expect(storage.getCookie('key')).to.be.null() - }) - - it('should update expiration when overwriting localstorage with expiration with one without', () => { - const storageHandler = WrappedStorageHandler.make('ls', storage, eventBus) - storageHandler.set('key', 'value', expiresInDays(5), 'example.com') - storageHandler.set('key', 'value', undefined, 'example.com') - - const result = storageHandler.get('key') - - expect(result?.data).to.be.eq('value') - expect(result?.expiresAt).to.be.undefined() - }) - - it('should update expiration when overwriting a cookie with expiration with one without', () => { - const storageHandler = WrappedStorageHandler.make('cookie', storage, eventBus) - storageHandler.set('key', 'value', expiresInDays(5), 'example.com') - storageHandler.set('key', 'value', undefined, 'example.com') - - const result = storageHandler.get('key') - - expect(result?.data).to.be.eq('value') - expect(result?.expiresAt).to.be.undefined() - }) }) diff --git a/test/unit/idex/identity-resolver.spec.ts b/test/unit/idex/identity-resolver.spec.ts index 15d10449..771491b2 100644 --- a/test/unit/idex/identity-resolver.spec.ts +++ b/test/unit/idex/identity-resolver.spec.ts @@ -1,5 +1,4 @@ // @ts-nocheck - import jsdom from 'global-jsdom' import sinon from 'sinon' import { expect, use } from 'chai' @@ -9,6 +8,7 @@ import { LocalEventBus } from '../../../src/events/event-bus' import dirtyChai from 'dirty-chai' import { WrappedStorageHandler } from '../../../src/handlers/storage-handler' import { WrappedCallHandler } from '../../../src/handlers/call-handler' +import { StorageHandlerBackedCache } from '../../../src/cache' use(dirtyChai) @@ -20,6 +20,11 @@ describe('IdentityResolver', () => { let errors = [] let callCount = 0 const storageHandler = WrappedStorageHandler.make('cookie', new DefaultStorageHandler(eventBus), eventBus) + const cache = new StorageHandlerBackedCache({ + strategy: 'cookie', + storageHandler, + domain: 'example.com' + }) beforeEach(() => { jsdom('', { @@ -39,7 +44,7 @@ describe('IdentityResolver', () => { it('should invoke callback on success, store the result in a cookie', (done) => { const response = { id: 112233 } - const identityResolver = IdentityResolver.make({}, storageHandler, calls, eventBus) + const identityResolver = IdentityResolver.make({}, cache, calls, eventBus) const successCallback = (responseAsJson) => { expect(callCount).to.be.eql(1) expect(errors).to.be.empty() @@ -57,7 +62,12 @@ describe('IdentityResolver', () => { it('should invoke callback on success, if storing the result in a cookie fails', () => { const setCookieStub = sinon.createSandbox().stub(storage, 'setCookie').throws() const failedStorage = WrappedStorageHandler.make('cookie', storage, eventBus) - const identityResolver = IdentityResolver.make({}, failedStorage, calls, eventBus) + const cache = new StorageHandlerBackedCache({ + strategy: 'cookie', + storageHandler: failedStorage, + domain: 'example.com' + }) + const identityResolver = IdentityResolver.make({}, cache, calls, eventBus) let jsonResponse = null const successCallback = (responseAsJson) => { jsonResponse = responseAsJson @@ -77,7 +87,7 @@ describe('IdentityResolver', () => { it('should attach the duid', (done) => { const response = { id: 112233 } - const identityResolver = IdentityResolver.make({ peopleVerifiedId: '987' }, storageHandler, calls, eventBus) + const identityResolver = IdentityResolver.make({ peopleVerifiedId: '987' }, cache, calls, eventBus) const successCallback = (responseAsJson) => { expect(requestToComplete.url).to.eq('https://idx.liadm.com/idex/unknown/any?duid=987') expect(errors).to.be.empty() @@ -90,7 +100,7 @@ describe('IdentityResolver', () => { it('should attach additional params', (done) => { const response = { id: 112233 } - const identityResolver = IdentityResolver.make({ peopleVerifiedId: '987' }, storageHandler, calls, eventBus) + const identityResolver = IdentityResolver.make({ peopleVerifiedId: '987' }, cache, calls, eventBus) const successCallback = (responseAsJson) => { expect(requestToComplete.url).to.eq('https://idx.liadm.com/idex/unknown/any?duid=987&key=value') expect(errors).to.be.empty() @@ -103,7 +113,7 @@ describe('IdentityResolver', () => { it('should attach additional params with an array that should be serialized as repeated query', (done) => { const response = { id: 112233 } - const identityResolver = IdentityResolver.make({ peopleVerifiedId: '987' }, storageHandler, calls, eventBus) + const identityResolver = IdentityResolver.make({ peopleVerifiedId: '987' }, cache, calls, eventBus) const successCallback = (responseAsJson) => { expect(requestToComplete.url).to.eq('https://idx.liadm.com/idex/unknown/any?duid=987&qf=0.1&resolve=age&resolve=gender') expect(errors).to.be.empty() @@ -116,7 +126,7 @@ describe('IdentityResolver', () => { it('should attach publisher id', (done) => { const response = { id: 112233 } - const identityResolver = IdentityResolver.make({ peopleVerifiedId: '987', identityResolutionConfig: { publisherId: 123 } }, storageHandler, calls, eventBus) + const identityResolver = IdentityResolver.make({ peopleVerifiedId: '987', identityResolutionConfig: { publisherId: 123 } }, cache, calls, eventBus) const successCallback = (responseAsJson) => { expect(requestToComplete.url).to.eq('https://idx.liadm.com/idex/unknown/123?duid=987&key=value') expect(errors).to.be.empty() @@ -129,7 +139,7 @@ describe('IdentityResolver', () => { it('should attach the did', (done) => { const response = { id: 112233 } - const identityResolver = IdentityResolver.make({ distributorId: 'did-01er' }, storageHandler, calls, eventBus) + const identityResolver = IdentityResolver.make({ distributorId: 'did-01er' }, cache, calls, eventBus) const successCallback = (responseAsJson) => { expect(requestToComplete.url).to.eq('https://idx.liadm.com/idex/unknown/any?did=did-01er') expect(errors).to.be.empty() @@ -141,7 +151,7 @@ describe('IdentityResolver', () => { }) it('should not attach an empty tuple', (done) => { - const identityResolver = IdentityResolver.make({ peopleVerifiedId: null }, storageHandler, calls, eventBus) + const identityResolver = IdentityResolver.make({ peopleVerifiedId: null }, cache, calls, eventBus) const successCallback = (responseAsJson) => { expect(requestToComplete.url).to.eq('https://idx.liadm.com/idex/unknown/any') expect(errors).to.be.empty() @@ -166,7 +176,7 @@ describe('IdentityResolver', () => { value: 'AnotherId' } ] - }, storageHandler, calls, eventBus) + }, cache, calls, eventBus) const successCallback = (responseAsJson) => { expect(requestToComplete.url).to.eq('https://idx.liadm.com/idex/unknown/any?duid=987&pubcid=exexex&some-id=AnotherId') expect(errors).to.be.empty() @@ -184,7 +194,7 @@ describe('IdentityResolver', () => { privacyMode: false, gdprConsent: 'gdprConsent', usPrivacyString: 'usPrivacyString' - }, storageHandler, calls, eventBus) + }, cache, calls, eventBus) const successCallback = (responseAsJson) => { expect(requestToComplete.url).to.eq('https://idx.liadm.com/idex/unknown/any?us_privacy=usPrivacyString&gdpr=0&gdpr_consent=gdprConsent') expect(errors).to.be.empty() @@ -202,7 +212,7 @@ describe('IdentityResolver', () => { privacyMode: true, gdprConsent: 'gdprConsent', usPrivacyString: 'usPrivacyString' - }, storageHandler, calls, eventBus) + }, cache, calls, eventBus) const successCallback = (responseAsJson) => { expect(requestToComplete.url).to.eq('https://idx.liadm.com/idex/unknown/any?us_privacy=usPrivacyString&gdpr=1&n3pc=1&gdpr_consent=gdprConsent') expect(errors).to.be.empty() @@ -214,7 +224,7 @@ describe('IdentityResolver', () => { }) it('should return the default empty response and emit error if response is 500', (done) => { - const identityResolver = IdentityResolver.make({}, storageHandler, calls, eventBus) + const identityResolver = IdentityResolver.make({}, cache, calls, eventBus) const errorCallback = (error) => { expect(error.message).to.match(/^Error during XHR call: 500, url/) done() @@ -227,7 +237,7 @@ describe('IdentityResolver', () => { const responseMd5 = { id: 123 } const responseSha1 = { id: 125 } - const identityResolver = IdentityResolver.make({}, storageHandler, calls, eventBus) + const identityResolver = IdentityResolver.make({}, cache, calls, eventBus) let jsonResponse = null const successCallback = (responseAsJson) => { jsonResponse = responseAsJson @@ -261,7 +271,7 @@ describe('IdentityResolver', () => { requestedAttributes: ['uid2', 'md5'] } }, - storageHandler, + cache, calls, eventBus ) @@ -287,7 +297,7 @@ describe('IdentityResolver', () => { }, privacyMode: true }, - storageHandler, + cache, calls, eventBus ) @@ -308,13 +318,17 @@ describe('IdentityResolver', () => { const response = { id: 112233 } let recordedExpiresAt: Date - const customStorage = WrappedStorageHandler.make('cookie', storage, eventBus) - customStorage.set = (key, value, expiresAt, sameSite, domain) => { + const customCache = new StorageHandlerBackedCache({ + strategy: 'cookie', + storageHandler, + domain: 'example.com' + }) + customCache.set = (key, value, expiresAt) => { recordedExpiresAt = expiresAt - storageHandler.set(key, value, expiresAt, sameSite, domain) + cache.set(key, value, expiresAt) } - const identityResolver = IdentityResolver.make({}, customStorage, calls, eventBus) + const identityResolver = IdentityResolver.make({}, customCache, calls, eventBus) const expiresAt = new Date() expiresAt.setHours(expiresAt.getHours() + 12) diff --git a/test/unit/manager/decisions.spec.ts b/test/unit/manager/decisions.spec.ts index f8f3088d..4efd9bf5 100644 --- a/test/unit/manager/decisions.spec.ts +++ b/test/unit/manager/decisions.spec.ts @@ -49,7 +49,6 @@ describe('DecisionsManager for stored decisions', () => { it('should not return empty values', () => { storage.setCookie('lidids.', '') const resolutionResult = decisions.resolve({}, storage, eventBus) - // @ts-expect-error expect(resolutionResult.decisionIds.length).to.eql(0) }) diff --git a/test/unit/manager/identifiers.spec.ts b/test/unit/manager/identifiers.spec.ts deleted file mode 100644 index 041c0195..00000000 --- a/test/unit/manager/identifiers.spec.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { expect, use } from 'chai' -import * as identifiers from '../../../src/manager/identifiers' -import { DefaultStorageHandler } from 'live-connect-handlers' -import sinon from 'sinon' -import jsdom from 'global-jsdom' -import dirtyChai from 'dirty-chai' -import { WrappedStorageHandler } from '../../../src/handlers/storage-handler' -import { LocalEventBus } from '../../../src/events/event-bus' - -use(dirtyChai) - -const eventBus = LocalEventBus() -const storage = new DefaultStorageHandler(eventBus) -const storageHandler = WrappedStorageHandler.make('cookie', storage, eventBus) - -describe('IdentifiersManager', () => { - const sandbox = sinon.createSandbox() - - beforeEach(() => { - jsdom('', { - url: 'http://www.example.com' - }) - }) - - it('should create a first party cookie if it doesn\'t exist', () => { - expect(storageHandler.getCookie('_lc2_fpi')).to.eql(null) - const resolutionResult = identifiers.resolve({}, storageHandler, eventBus) - expect(storageHandler.getCookie('_lc2_fpi')).to.eql(resolutionResult.liveConnectId) - expect(storageHandler.getDataFromLocalStorage('_li_duid')).to.eql(resolutionResult.liveConnectId) - }) - - it('should create a first party cookie if it doesn\'t exist, and storage strategy is cookie', () => { - expect(storageHandler.getCookie('_lc2_fpi')).to.eql(null) - const resolutionResult = identifiers.resolve({}, storageHandler, eventBus) - expect(storageHandler.getCookie('_lc2_fpi')).to.eql(resolutionResult.liveConnectId) - expect(storageHandler.getDataFromLocalStorage('_li_duid')).to.eql(resolutionResult.liveConnectId) - }) - - it('should create a first party identifier in local storage if it doesn\'t exist, and storage strategy is ls', () => { - expect(storageHandler.getDataFromLocalStorage('_lc2_fpi')).to.eql(null) - const localStorage = WrappedStorageHandler.make('ls', storage, eventBus) - const resolutionResult = identifiers.resolve({}, localStorage, eventBus) - expect(storageHandler.getDataFromLocalStorage('_lc2_fpi')).to.eql(resolutionResult.liveConnectId) - expect(storageHandler.getDataFromLocalStorage('_lc2_fpi_exp')).to.be.not.null() - expect(storageHandler.getDataFromLocalStorage('_li_duid')).to.eql(resolutionResult.liveConnectId) - }) - - it('should not create or return a first party identifier if the StorageStrategy is set to "none"', () => { - const storageNone = WrappedStorageHandler.make('none', storage, eventBus) - const resolutionResult = identifiers.resolve({}, storageNone, eventBus) - expect(resolutionResult).to.include({ domain: '.www.example.com' }) - }) - - it('should return the domain', () => { - const resolutionResult = identifiers.resolve({}, storageHandler, eventBus) - expect(resolutionResult.domain).to.eql('.example.com') - }) - - it('should re-use a first party cookie if it exist', () => { - const id = 'xxxxx' - // @ts-expect-error - storageHandler.setCookie('_lc2_fpi', id, 400, undefined, '.example.com') - const resolutionResult = identifiers.resolve({}, storageHandler, eventBus) - expect(storageHandler.getCookie('_lc2_fpi')).to.eql(id) - expect(resolutionResult.liveConnectId).to.eql(id) - }) - - it('should emit an error if identifiers.resolve fails for some reason, return an empty object', () => { - const stub = sandbox.stub(storage, 'getCookie').throws() - const failedStorage = WrappedStorageHandler.make('cookie', storage, eventBus) - const resolutionResult = identifiers.resolve({}, failedStorage, eventBus) - expect(resolutionResult).to.eql({}) - stub.restore() - }) - - it('should create a first party cookie that starts with apex domain hash', () => { - identifiers.resolve({}, storageHandler, eventBus) - // apexOfExampleCom = '0caaf24ab1a0' - expect(storageHandler.getCookie('_lc2_fpi')).to.match(/0caaf24ab1a0--.*/) - }) - - it('should create a first party cookie that is lowercased', () => { - identifiers.resolve({}, storageHandler, eventBus) - // @ts-expect-error - expect(storageHandler.getCookie('_lc2_fpi')).to.satisfy(cookie => cookie === cookie.toLowerCase()) - }) -}) - -describe('TLD checker', () => { - beforeEach(() => jsdom('', { - url: 'http://subdomain.tests.example.com' - })) - - it('should determine correct tld', () => { - const resolved = identifiers.resolve({}, storageHandler, eventBus) - expect(resolved.domain).to.eq('.example.com') - }) - - it('should reuse the cached correct tld', () => { - storageHandler.setCookie('_li_dcdm_c', '.example.com') - const resolved = identifiers.resolve({}, storageHandler, eventBus) - expect(resolved.domain).to.eq('.example.com') - }) -}) - -describe('TLD on sub-domain', () => { - beforeEach(() => jsdom('', { - url: 'http://example.co.uk' - })) - - it('should use the full domain', () => { - const resolved = identifiers.resolve({}, storageHandler, eventBus) - expect(resolved.domain).to.eq('.example.co.uk') - }) -}) diff --git a/test/unit/pixel/state.spec.ts b/test/unit/pixel/state.spec.ts index 3d7143c7..d7a4875f 100644 --- a/test/unit/pixel/state.spec.ts +++ b/test/unit/pixel/state.spec.ts @@ -1,7 +1,7 @@ // @ts-nocheck import { assert, expect, use } from 'chai' import { hashEmail } from '../../../src/utils/hash' -import { enrich as privacyConfig } from '../../../src/enrichers/privacy-config' +import { enrichPrivacyMode } from '../../../src/enrichers/privacy-config' import { StateWrapper } from '../../../src/pixel/state' import { mergeObjects } from '../../../src/pixel/fiddler' import dirtyChai from 'dirty-chai' @@ -62,7 +62,7 @@ describe('EventComposition', () => { gdprConsent: 'test-consent-string', referrer: 'https://some.test.referrer.com' } - const event = new StateWrapper(mergeObjects(pixelData, privacyConfig(pixelData))) + const event = new StateWrapper(mergeObjects(pixelData, enrichPrivacyMode(pixelData))) const expectedPairs = [ 'aid=9898', // appId @@ -114,7 +114,7 @@ describe('EventComposition', () => { gdprConsent: 'test-consent-string', referrer: 'https://some.test.referrer.com' } - const event = new StateWrapper(mergeObjects(pixelData, privacyConfig(pixelData))) + const event = new StateWrapper(mergeObjects(pixelData, enrichPrivacyMode(pixelData))) const expectedPairs = [ 'aid=9898', // appId @@ -176,7 +176,7 @@ describe('EventComposition', () => { gdprApplies: true, gdprConsent: 'some-string' } - const event = new StateWrapper(mergeObjects(pixelData, privacyConfig(pixelData))) + const event = new StateWrapper(mergeObjects(pixelData, enrichPrivacyMode(pixelData))) const b64EncodedEventSource = 'eyJldmVudE5hbWUiOiJ2aWV3Q29udGVudCJ9' expect(event.asQuery().toQueryString()).to.eql(`?se=${b64EncodedEventSource}&gdpr=1&n3pc=1&n3pct=1&nb=1&gdpr_consent=some-string`) assert.includeDeepMembers(event.asTuples(), [['se', b64EncodedEventSource], ['gdpr', '1'], ['n3pc', '1'], ['n3pct', '1'], ['nb', '1'], ['gdpr_consent', 'some-string']]) @@ -188,7 +188,7 @@ describe('EventComposition', () => { gdprApplies: false, gdprConsent: 'some-string' } - const event = new StateWrapper(mergeObjects(pixelData, privacyConfig(pixelData))) + const event = new StateWrapper(mergeObjects(pixelData, enrichPrivacyMode(pixelData))) const b64EncodedEventSource = 'eyJldmVudE5hbWUiOiJ2aWV3Q29udGVudCJ9' expect(event.asQuery().toQueryString()).to.eql(`?se=${b64EncodedEventSource}&gdpr=0&gdpr_consent=some-string`) assert.includeDeepMembers(event.asTuples(), [['se', b64EncodedEventSource], ['gdpr', '0'], ['gdpr_consent', 'some-string']]) @@ -211,7 +211,7 @@ describe('EventComposition', () => { gdprConsent: undefined, wrapperName: undefined } - const event = new StateWrapper(mergeObjects(pixelData, privacyConfig(pixelData))) + const event = new StateWrapper(mergeObjects(pixelData, enrichPrivacyMode(pixelData))) expect(event.asQuery().toQueryString()).to.eql(`?tna=${trackerName}`) assert.includeDeepMembers(event.asTuples(), [['tna', trackerName]]) })