Achieve 100% coverage on analytics.js
- Add test for navigator.language falsy branch - Add test for retry getOrCreateVisitor failing on second attempt - Mark unreachable defensive guards (encrypt/decrypt/send with null encKey) with c8 ignore since they cannot be triggered via public API Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ async function importKey(b64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function encrypt(data) {
|
async function encrypt(data) {
|
||||||
if (!encKey) return null
|
/* c8 ignore next */ if (!encKey) return null
|
||||||
const json = new globalThis.TextEncoder().encode(JSON.stringify(data))
|
const json = new globalThis.TextEncoder().encode(JSON.stringify(data))
|
||||||
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12))
|
const iv = globalThis.crypto.getRandomValues(new Uint8Array(12))
|
||||||
const encrypted = await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv, tagLength: 128 }, encKey, json)
|
const encrypted = await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv, tagLength: 128 }, encKey, json)
|
||||||
@@ -24,7 +24,7 @@ async function encrypt(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function decrypt(b64) {
|
async function decrypt(b64) {
|
||||||
if (!encKey) return null
|
/* c8 ignore next */ if (!encKey) return null
|
||||||
const raw = Uint8Array.from(globalThis.atob(b64), c => c.codePointAt(0))
|
const raw = Uint8Array.from(globalThis.atob(b64), c => c.codePointAt(0))
|
||||||
const iv = raw.slice(0, 12)
|
const iv = raw.slice(0, 12)
|
||||||
const data = raw.slice(12)
|
const data = raw.slice(12)
|
||||||
@@ -43,7 +43,7 @@ function clearSession() {
|
|||||||
|
|
||||||
async function send(data, expectResponse = false) {
|
async function send(data, expectResponse = false) {
|
||||||
const d = await encrypt(data)
|
const d = await encrypt(data)
|
||||||
if (!d) return null
|
/* c8 ignore next */ if (!d) return null
|
||||||
try {
|
try {
|
||||||
if (!expectResponse && navigator.sendBeacon) {
|
if (!expectResponse && navigator.sendBeacon) {
|
||||||
navigator.sendBeacon(ENDPOINT, JSON.stringify({ d }))
|
navigator.sendBeacon(ENDPOINT, JSON.stringify({ d }))
|
||||||
|
|||||||
@@ -293,6 +293,56 @@ describe('analytics.js', () => {
|
|||||||
expect(beaconMock).not.toHaveBeenCalled()
|
expect(beaconMock).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('covers navigator.language being undefined', async () => {
|
||||||
|
const key = await realImportKey(TEST_KEY_B64)
|
||||||
|
const visitorResp = await realEncrypt({ uid: 'vl', h: 'hashl' }, key)
|
||||||
|
|
||||||
|
document.body.dataset.k = TEST_KEY_B64
|
||||||
|
document.body.dataset.e = '/t'
|
||||||
|
|
||||||
|
const origLang = navigator.language
|
||||||
|
Object.defineProperty(navigator, 'language', { value: '', configurable: true })
|
||||||
|
|
||||||
|
fetchMock.mockResolvedValue({
|
||||||
|
ok: true, status: 200, json: async () => ({ d: visitorResp }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const { initAnalytics } = await loadModule()
|
||||||
|
await initAnalytics()
|
||||||
|
|
||||||
|
Object.defineProperty(navigator, 'language', { value: origLang, configurable: true })
|
||||||
|
|
||||||
|
expect(fetchMock).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('retry getOrCreateVisitor fails on second attempt after 403', async () => {
|
||||||
|
const key = await realImportKey(TEST_KEY_B64)
|
||||||
|
const visitorResp = await realEncrypt({ uid: 'v6', h: 'hash6' }, key)
|
||||||
|
|
||||||
|
document.body.dataset.k = TEST_KEY_B64
|
||||||
|
document.body.dataset.e = '/t'
|
||||||
|
|
||||||
|
navigator.sendBeacon = undefined
|
||||||
|
|
||||||
|
let callCount = 0
|
||||||
|
fetchMock.mockImplementation(async () => {
|
||||||
|
callCount++
|
||||||
|
// 1st: visitor creation succeeds
|
||||||
|
if (callCount === 1) return { ok: true, status: 200, json: async () => ({ d: visitorResp }) }
|
||||||
|
// 2nd: trackPageView gets 403 -> clears session
|
||||||
|
if (callCount === 2) return { ok: false, status: 403 }
|
||||||
|
// 3rd: second getOrCreateVisitor also fails (204 = no body)
|
||||||
|
return { ok: true, status: 204 }
|
||||||
|
})
|
||||||
|
|
||||||
|
const { initAnalytics } = await loadModule()
|
||||||
|
await initAnalytics()
|
||||||
|
|
||||||
|
// After 403, retry creates visitor (call 3) but gets 204 -> null visitor -> early return
|
||||||
|
expect(callCount).toBe(3)
|
||||||
|
expect(sessionStorage.getItem('_u')).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
it('setAuth sends when session and key exist', async () => {
|
it('setAuth sends when session and key exist', async () => {
|
||||||
const key = await realImportKey(TEST_KEY_B64)
|
const key = await realImportKey(TEST_KEY_B64)
|
||||||
const visitorResp = await realEncrypt({ uid: 'v5', h: 'hash5' }, key)
|
const visitorResp = await realEncrypt({ uid: 'v5', h: 'hash5' }, key)
|
||||||
|
|||||||
Reference in New Issue
Block a user