Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 102 additions & 0 deletions src/app/_cache/forageStorage.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import localforage from 'localforage'
import { getAssignees, migrateAssignees, setAssignees } from './forageStorage'

jest.mock('localforage', () => ({
__esModule: true,
default: {
config: jest.fn(),
getItem: jest.fn(),
setItem: jest.fn(),
},
}))

const mockedLocalforage = jest.mocked(localforage)

describe('forageStorage', () => {
const originalWindow = global.window
const originalDocument = global.document
const originalLocalStorage = global.localStorage

const defineBrowserGlobals = ({
hasStorageAccess,
requestStorageAccess = jest.fn(),
}: {
hasStorageAccess: jest.Mock<Promise<boolean>, []>
requestStorageAccess?: jest.Mock<Promise<void>, []>
}) => {
Object.defineProperty(global, 'window', {
configurable: true,
value: {},
})
Object.defineProperty(global, 'document', {
configurable: true,
value: {
hasStorageAccess,
requestStorageAccess,
},
})
Object.defineProperty(global, 'localStorage', {
configurable: true,
value: {
getItem: jest.fn(),
removeItem: jest.fn(),
},
})
}

afterEach(() => {
jest.clearAllMocks()
Object.defineProperty(global, 'window', {
configurable: true,
value: originalWindow,
})
Object.defineProperty(global, 'document', {
configurable: true,
value: originalDocument,
})
Object.defineProperty(global, 'localStorage', {
configurable: true,
value: originalLocalStorage,
})
})

it('skips assignee reads when the browser denies storage access', async () => {
const requestStorageAccess = jest.fn<Promise<void>, []>()
defineBrowserGlobals({
hasStorageAccess: jest.fn().mockResolvedValue(false),
requestStorageAccess,
})

await expect(getAssignees('client.company')).resolves.toEqual([])

expect(mockedLocalforage.getItem).not.toHaveBeenCalled()
expect(requestStorageAccess).not.toHaveBeenCalled()
})

it('skips assignee writes when the browser denies storage access', async () => {
const requestStorageAccess = jest.fn<Promise<void>, []>()
defineBrowserGlobals({
hasStorageAccess: jest.fn().mockResolvedValue(false),
requestStorageAccess,
})

await setAssignees('client.company', [{ id: 'assignee-1' }])

expect(mockedLocalforage.setItem).not.toHaveBeenCalled()
expect(requestStorageAccess).not.toHaveBeenCalled()
})

it('does not touch localStorage migration when storage access is denied', async () => {
const requestStorageAccess = jest.fn<Promise<void>, []>()
defineBrowserGlobals({
hasStorageAccess: jest.fn().mockResolvedValue(false),
requestStorageAccess,
})

await migrateAssignees('client.company')

expect(global.localStorage.getItem).not.toHaveBeenCalled()
expect(mockedLocalforage.setItem).not.toHaveBeenCalled()
expect(requestStorageAccess).not.toHaveBeenCalled()
})
})
51 changes: 31 additions & 20 deletions src/app/_cache/forageStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,52 +8,63 @@ localforage.config({
storeName: 'assignees',
})

async function hasAssigneeStorageAccess() {
if (typeof window === 'undefined') return false

if (typeof document.hasStorageAccess !== 'function') {
return true
}

try {
const hasAccess = await document.hasStorageAccess()
if (!hasAccess) {
console.info('Browser has no storage access')
}
return hasAccess
} catch {
return false
}
}

export async function migrateAssignees(lookupKey: string) {
if (!(await hasAssigneeStorageAccess())) return

const lKey = `assignees.${lookupKey}`
const existing = localStorage.getItem(lKey)

if (existing) {
try {
try {
const existing = localStorage.getItem(lKey)

if (existing) {
const parsed = JSON.parse(existing)
await localforage.setItem(lKey, parsed)
localStorage.removeItem(lKey)
} catch (err) {
console.error('Migration failed', err)
}
} catch (err) {
console.error('Migration failed', err)
}
} //a utility function to migrate existing assignee data from localStorage to localForage
}

export async function getAssignees(lookupKey: string): Promise<IAssigneeCombined[]> {
if (typeof window === 'undefined') return []
if (!(await hasAssigneeStorageAccess())) return []

try {
if (!(await document.hasStorageAccess())) {
console.info('Browswer has no storage access')
await document.requestStorageAccess()
}

return (await localforage.getItem<IAssigneeCombined[]>(`assignees.${lookupKey}`)) ?? []
} catch (error: unknown) {
console.error(
"Storage access not granted. Under Chrome's Settings > Privacy and Security, make sure 'Third-party cookies' is allowed.",
"Storage access not granted. Under Chrome's Settings > Privacy and Security, make sure 'Third-party cookies' are allowed.",
)
return []
}
}

export async function setAssignees(lookupKey: string, value: any) {
if (typeof window === 'undefined') return
if (!(await hasAssigneeStorageAccess())) return

try {
if (!(await document.hasStorageAccess())) {
console.info('Browswer has no storage access')
await document.requestStorageAccess()
}

return await localforage.setItem(`assignees.${lookupKey}`, value)
} catch (error: unknown) {
console.error(
"Storage access not granted. Under Chrome's Settings > Privacy and Security, make sure 'Third-party cookies' is allowed.",
"Storage access not granted. Under Chrome's Settings > Privacy and Security, make sure 'Third-party cookies' are allowed.",
)
}
}
Loading