fix(tracker): fix for dying tests (added tabid to writer, refactored other tests)

This commit is contained in:
nick-delirium 2023-05-31 17:54:54 +02:00
parent 3da035188e
commit 00ee001539
8 changed files with 452 additions and 16 deletions

View file

@ -50,7 +50,7 @@ jobs:
- name: Jest tests
run: |
cd tracker/tracker
yarn test
yarn test:ci
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v3
with:

View file

@ -22,7 +22,8 @@
"build": "npm run clean && npm run tscRun && npm run rollup && npm run compile",
"prepare": "cd ../../ && husky install tracker/.husky/",
"lint-front": "lint-staged",
"test": "jest"
"test": "jest --coverage=false",
"test:ci": "jest --coverage=true"
},
"devDependencies": {
"@babel/core": "^7.10.2",

View file

@ -14,6 +14,11 @@ export interface Options {
domSanitizer?: (node: Element) => SanitizeLevel
}
export const stringWiper = (input: string) =>
input
.trim()
.replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/g, '█')
export default class Sanitizer {
private readonly obscured: Set<number> = new Set()
private readonly hidden: Set<number> = new Set()
@ -59,10 +64,9 @@ export default class Sanitizer {
sanitize(id: number, data: string): string {
if (this.obscured.has(id)) {
// TODO: is it the best place to put trim() ? Might trimmed spaces be considered in layout in certain cases?
return data
.trim()
.replace(/[^\f\n\r\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]/g, '█')
return stringWiper(data)
}
if (this.options.obscureTextNumbers) {
data = data.replace(/\d/g, '0')
}

View file

@ -82,14 +82,6 @@ export function hasOpenreplayAttribute(e: Element, attr: string): boolean {
return false
}
export function isIframeCrossdomain(e: HTMLIFrameElement): boolean {
try {
return e.contentWindow?.location.href !== window.location.href
} catch (e) {
return true
}
}
/**
* checks if iframe is accessible
**/
@ -105,7 +97,7 @@ function dec2hex(dec: number) {
return dec.toString(16).padStart(2, '0')
}
export function generateRandomId(len: number) {
export function generateRandomId(len?: number) {
const arr: Uint8Array = new Uint8Array((len || 40) / 2)
// msCrypto = IE11
// @ts-ignore

View file

@ -0,0 +1,113 @@
import { describe, expect, test } from '@jest/globals'
import {
isNode,
isSVGElement,
isElementNode,
isCommentNode,
isTextNode,
isDocument,
isRootNode,
hasTag,
} from '../main/app/guards.js'
describe('isNode', () => {
test('returns true for a valid Node object', () => {
const node = document.createElement('div')
expect(isNode(node)).toBe(true)
})
test('returns false for a non-Node object', () => {
const obj = { foo: 'bar' }
expect(isNode(obj)).toBe(false)
})
})
describe('isSVGElement', () => {
test('returns true for an SVGElement object', () => {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
expect(isSVGElement(svg)).toBe(true)
})
test('returns false for a non-SVGElement object', () => {
const div = document.createElement('div')
expect(isSVGElement(div)).toBe(false)
})
})
describe('isElementNode', () => {
test('returns true for an Element object', () => {
const element = document.createElement('div')
expect(isElementNode(element)).toBe(true)
})
test('returns false for a non-Element object', () => {
const textNode = document.createTextNode('Hello')
expect(isElementNode(textNode)).toBe(false)
})
})
describe('isCommentNode', () => {
test('returns true for a Comment object', () => {
const comment = document.createComment('This is a comment')
expect(isCommentNode(comment)).toBe(true)
})
test('returns false for a non-Comment object', () => {
const div = document.createElement('div')
expect(isCommentNode(div)).toBe(false)
})
})
describe('isTextNode', () => {
test('returns true for a Text object', () => {
const textNode = document.createTextNode('Hello')
expect(isTextNode(textNode)).toBe(true)
})
test('returns false for a non-Text object', () => {
const div = document.createElement('div')
expect(isTextNode(div)).toBe(false)
})
})
describe('isDocument', () => {
test('returns true for a Document object', () => {
const documentObj = document.implementation.createHTMLDocument('Test')
expect(isDocument(documentObj)).toBe(true)
})
test('returns false for a non-Document object', () => {
const div = document.createElement('div')
expect(isDocument(div)).toBe(false)
})
})
describe('isRootNode', () => {
test('returns true for a Document object', () => {
const documentObj = document.implementation.createHTMLDocument('Test')
expect(isRootNode(documentObj)).toBe(true)
})
test('returns true for a DocumentFragment object', () => {
const fragment = document.createDocumentFragment()
expect(isRootNode(fragment)).toBe(true)
})
test('returns false for a non-root Node object', () => {
const div = document.createElement('div')
expect(isRootNode(div)).toBe(false)
})
})
describe('hasTag', () => {
test('returns true if the element has the specified tag name', () => {
const element = document.createElement('input')
expect(hasTag(element, 'input')).toBe(true)
})
test('returns false if the element does not have the specified tag name', () => {
const element = document.createElement('div')
// @ts-expect-error
expect(hasTag(element, 'span')).toBe(false)
})
})

View file

@ -0,0 +1,135 @@
import { describe, expect, jest, afterEach, beforeEach, test } from '@jest/globals'
import Sanitizer, { SanitizeLevel, Options, stringWiper } from '../main/app/sanitizer.js'
describe('stringWiper', () => {
test('should replace all characters with █', () => {
expect(stringWiper('Sensitive Data')).toBe('██████████████')
})
})
describe('Sanitizer', () => {
let sanitizer: Sanitizer
beforeEach(() => {
const options: Options = {
obscureTextEmails: true,
obscureTextNumbers: false,
domSanitizer: undefined,
}
const app = {
nodes: {
getID: (el: { mockId: number }) => el.mockId,
},
}
// @ts-expect-error
sanitizer = new Sanitizer(app, options)
})
afterEach(() => {
sanitizer.clear()
})
test('should handle node and mark it as obscured if parent is obscured', () => {
sanitizer['obscured'].add(2)
sanitizer.handleNode(1, 2, document.createElement('div'))
expect(sanitizer.isObscured(1)).toBe(true)
})
test('should handle node and mark it as obscured if it has "masked" or "obscured" attribute', () => {
const node = document.createElement('div')
node.setAttribute('data-openreplay-obscured', '')
sanitizer.handleNode(1, 2, node)
expect(sanitizer.isObscured(1)).toBe(true)
})
test('should handle node and mark it as hidden if parent is hidden', () => {
sanitizer['hidden'].add(2)
sanitizer.handleNode(1, 2, document.createElement('div'))
expect(sanitizer.isHidden(1)).toBe(true)
})
test('should handle node and mark it as hidden if it has "htmlmasked" or "hidden" attribute', () => {
const node = document.createElement('div')
node.setAttribute('data-openreplay-hidden', '')
sanitizer.handleNode(1, 2, node)
expect(sanitizer.isHidden(1)).toBe(true)
})
test('should handle node and sanitize based on custom domSanitizer function', () => {
const domSanitizer = (node: Element): SanitizeLevel => {
if (node.tagName === 'SPAN') {
return SanitizeLevel.Obscured
}
if (node.tagName === 'DIV') {
return SanitizeLevel.Hidden
}
return SanitizeLevel.Plain
}
const options: Options = {
obscureTextEmails: true,
obscureTextNumbers: false,
domSanitizer,
}
const app = {
nodes: {
getID: jest.fn(),
},
}
// @ts-expect-error
sanitizer = new Sanitizer(app, options)
const spanNode = document.createElement('span')
const divNode = document.createElement('div')
const plainNode = document.createElement('p')
sanitizer.handleNode(1, 2, spanNode)
sanitizer.handleNode(3, 4, divNode)
sanitizer.handleNode(5, 6, plainNode)
expect(sanitizer.isObscured(1)).toBe(true)
expect(sanitizer.isHidden(3)).toBe(true)
expect(sanitizer.isObscured(5)).toBe(false)
expect(sanitizer.isHidden(5)).toBe(false)
})
test('should sanitize data as obscured if node is marked as obscured', () => {
sanitizer['obscured'].add(1)
const data = 'Sensitive Data'
const sanitizedData = sanitizer.sanitize(1, data)
expect(sanitizedData).toEqual(stringWiper(data))
})
test('should sanitize data by obscuring text numbers if enabled', () => {
sanitizer['options'].obscureTextNumbers = true
const data = 'Phone: 123-456-7890'
const sanitizedData = sanitizer.sanitize(1, data)
expect(sanitizedData).toEqual('Phone: 000-000-0000')
})
test('should sanitize data by obscuring text emails if enabled', () => {
sanitizer['options'].obscureTextEmails = true
const data = 'john.doe@example.com'
const sanitizedData = sanitizer.sanitize(1, data)
expect(sanitizedData).toEqual('********@*******.***')
})
test('should return inner text of an element securely by sanitizing it', () => {
const element = document.createElement('div')
sanitizer['obscured'].add(1)
// @ts-expect-error
element.mockId = 1
element.innerText = 'Sensitive Data'
const sanitizedText = sanitizer.getInnerTextSecure(element)
expect(sanitizedText).toEqual('██████████████')
})
test('should return empty string if node element does not exist', () => {
const element = document.createElement('div')
element.innerText = 'Sensitive Data'
const sanitizedText = sanitizer.getInnerTextSecure(element)
expect(sanitizedText).toEqual('')
})
})

View file

@ -0,0 +1,186 @@
import { describe, expect, test, jest, afterEach, beforeEach } from '@jest/globals'
import {
adjustTimeOrigin,
getTimeOrigin,
now,
stars,
normSpaces,
isURL,
deprecationWarn,
getLabelAttribute,
hasOpenreplayAttribute,
canAccessIframe,
generateRandomId,
} from '../main/utils.js'
describe('adjustTimeOrigin', () => {
test('adjusts the time origin based on performance.now', () => {
jest.spyOn(Date, 'now').mockReturnValue(1000)
jest.spyOn(performance, 'now').mockReturnValue(1000)
adjustTimeOrigin()
expect(getTimeOrigin()).toBe(0)
})
})
describe('now', () => {
test('returns the current timestamp in milliseconds', () => {
jest.spyOn(Date, 'now').mockReturnValue(2550)
jest.spyOn(performance, 'now').mockReturnValue(2550)
adjustTimeOrigin()
expect(now()).toBe(2550)
})
})
describe('stars', () => {
test('returns a string of asterisks with the same length as the input string', () => {
expect(stars('hello')).toBe('*****')
})
test('returns an empty string if the input string is empty', () => {
expect(stars('')).toBe('')
})
})
describe('normSpaces', () => {
test('trims the string and replaces multiple spaces with a single space', () => {
expect(normSpaces(' hello world ')).toBe('hello world')
})
test('returns an empty string if the input string is empty', () => {
expect(normSpaces('')).toBe('')
})
})
describe('isURL', () => {
test('returns true for a valid URL starting with "https://"', () => {
expect(isURL('https://example.com')).toBe(true)
})
test('returns true for a valid URL starting with "http://"', () => {
expect(isURL('http://example.com')).toBe(true)
})
test('returns false for a URL without a valid protocol', () => {
expect(isURL('example.com')).toBe(false)
})
test('returns false for an empty string', () => {
expect(isURL('')).toBe(false)
})
})
describe('deprecationWarn', () => {
let consoleWarnSpy: jest.SpiedFunction<(args: any) => void>
beforeEach(() => {
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation((args) => args)
})
afterEach(() => {
consoleWarnSpy.mockRestore()
})
test('prints a warning message for a deprecated feature', () => {
deprecationWarn('oldFeature', 'newFeature')
expect(consoleWarnSpy).toHaveBeenCalledWith(
'OpenReplay: oldFeature is deprecated. Please, use newFeature instead. Visit https://docs.openreplay.com/ for more information.',
)
})
test('does not print a warning message for a deprecated feature that has already been warned', () => {
deprecationWarn('oldFeature2', 'newFeature')
deprecationWarn('oldFeature2', 'newFeature')
expect(consoleWarnSpy).toHaveBeenCalledTimes(1)
})
})
describe('getLabelAttribute', () => {
test('returns the value of "data-openreplay-label" attribute if present', () => {
const element = document.createElement('div')
element.setAttribute('data-openreplay-label', 'Label')
expect(getLabelAttribute(element)).toBe('Label')
})
test('returns the value of "data-asayer-label" attribute if "data-openreplay-label" is not present (with deprecation warning)', () => {
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation((args) => args)
const element = document.createElement('div')
element.setAttribute('data-asayer-label', 'Label')
expect(getLabelAttribute(element)).toBe('Label')
expect(consoleWarnSpy).toHaveBeenCalledWith(
'OpenReplay: "data-asayer-label" attribute is deprecated. Please, use "data-openreplay-label" attribute instead. Visit https://docs.openreplay.com/ for more information.',
)
consoleWarnSpy.mockRestore()
})
test('returns null if neither "data-openreplay-label" nor "data-asayer-label" are present', () => {
const element = document.createElement('div')
expect(getLabelAttribute(element)).toBeNull()
})
})
describe('hasOpenreplayAttribute', () => {
let consoleWarnSpy: jest.SpiedFunction<(args: any) => void>
beforeEach(() => {
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation((args) => args)
})
afterEach(() => {
consoleWarnSpy.mockRestore()
})
test('returns true and prints a deprecation warning for a deprecated openreplay attribute', () => {
const element = document.createElement('div')
element.setAttribute('data-openreplay-htmlmasked', 'true')
const result = hasOpenreplayAttribute(element, 'htmlmasked')
expect(result).toBe(true)
expect(consoleWarnSpy).toHaveBeenCalledWith(
'OpenReplay: "data-openreplay-htmlmasked" attribute is deprecated. Please, use "hidden" attribute instead. Visit https://docs.openreplay.com/installation/sanitize-data for more information.',
)
})
test('returns false for a non-existent openreplay attribute', () => {
const element = document.createElement('div')
const result = hasOpenreplayAttribute(element, 'nonexistent')
expect(result).toBe(false)
expect(consoleWarnSpy).not.toHaveBeenCalled()
})
})
describe('canAccessIframe', () => {
test('returns true if the iframe has a contentDocument', () => {
const iframe = document.createElement('iframe')
Object.defineProperty(iframe, 'contentDocument', {
get: () => document.createElement('div'),
})
expect(canAccessIframe(iframe)).toBe(true)
})
test('returns false if the iframe does not have a contentDocument', () => {
const iframe = document.createElement('iframe')
// Mock iframe.contentDocument to throw an error
Object.defineProperty(iframe, 'contentDocument', {
get: () => {
throw new Error('securityError')
},
})
expect(canAccessIframe(iframe)).toBe(false)
})
})
describe('generateRandomId', () => {
test('generates a random ID with the specified length', () => {
const id = generateRandomId(10)
expect(id).toHaveLength(10)
expect(/^[0-9a-f]+$/.test(id)).toBe(true)
})
test('generates a random ID with the default length if no length is specified', () => {
const id = generateRandomId()
expect(id).toHaveLength(40)
expect(/^[0-9a-f]+$/.test(id)).toBe(true)
})
})

View file

@ -9,7 +9,7 @@ describe('BatchWriter', () => {
beforeEach(() => {
onBatchMock = jest.fn()
batchWriter = new BatchWriter(1, 123456789, 'example.com', onBatchMock)
batchWriter = new BatchWriter(1, 123456789, 'example.com', onBatchMock, '123')
})
afterEach(() => {
@ -21,7 +21,8 @@ describe('BatchWriter', () => {
expect(batchWriter['timestamp']).toBe(123456789)
expect(batchWriter['url']).toBe('example.com')
expect(batchWriter['onBatch']).toBe(onBatchMock)
expect(batchWriter['nextIndex']).toBe(0)
// we add tab id as first in the batch
expect(batchWriter['nextIndex']).toBe(1)
expect(batchWriter['beaconSize']).toBe(200000)
expect(batchWriter['encoder']).toBeDefined()
expect(batchWriter['strDict']).toBeDefined()
@ -30,12 +31,14 @@ describe('BatchWriter', () => {
})
test('writeType writes the type of the message', () => {
// @ts-ignore
const message = [Messages.Type.BatchMetadata, 1, 2, 3, 4, 'example.com']
const result = batchWriter['writeType'](message as Message)
expect(result).toBe(true)
})
test('writeFields encodes the message fields', () => {
// @ts-ignore
const message = [Messages.Type.BatchMetadata, 1, 2, 3, 4, 'example.com']
const result = batchWriter['writeFields'](message as Message)
expect(result).toBe(true)
@ -52,6 +55,7 @@ describe('BatchWriter', () => {
})
test('writeWithSize writes the message with its size', () => {
// @ts-ignore
const message = [Messages.Type.BatchMetadata, 1, 2, 3, 4, 'example.com']
const result = batchWriter['writeWithSize'](message as Message)
expect(result).toBe(true)
@ -72,6 +76,7 @@ describe('BatchWriter', () => {
})
test('writeMessage writes the given message', () => {
// @ts-ignore
const message = [Messages.Type.Timestamp, 987654321]
// @ts-ignore
batchWriter['writeWithSize'] = jest.fn().mockReturnValue(true)