Add tests for migration and persisted state (#2118)
* Add tests for migrate * Add test for persisted.init * Add legacy transform test * Set NODE_ENV for testing * Mock logger * Set expo var to testzio/stable
parent
07fe058577
commit
818c6ae879
|
@ -48,4 +48,4 @@ jobs:
|
|||
run: yarn intl:build
|
||||
- name: Run tests
|
||||
run: |
|
||||
yarn test --forceExit
|
||||
NODE_ENV=test EXPO_PUBLIC_ENV=test yarn test --forceExit
|
||||
|
|
2
Makefile
2
Makefile
|
@ -14,7 +14,7 @@ build-web: ## Compile web bundle, copy to bskyweb directory
|
|||
|
||||
.PHONY: test
|
||||
test: ## Run all tests
|
||||
yarn test
|
||||
NODE_ENV=test EXPO_PUBLIC_ENV=test yarn test
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## Run style checks and verify syntax
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import type {LegacySchema} from '#/state/persisted/legacy'
|
||||
|
||||
export const ALICE_DID = 'did:plc:ALICE_DID'
|
||||
export const BOB_DID = 'did:plc:BOB_DID'
|
||||
|
||||
export const LEGACY_DATA_DUMP: LegacySchema = {
|
||||
session: {
|
||||
data: {
|
||||
service: 'https://bsky.social/',
|
||||
did: ALICE_DID,
|
||||
},
|
||||
accounts: [
|
||||
{
|
||||
service: 'https://bsky.social',
|
||||
did: ALICE_DID,
|
||||
refreshJwt: 'refreshJwt',
|
||||
accessJwt: 'accessJwt',
|
||||
handle: 'alice.test',
|
||||
email: 'alice@bsky.test',
|
||||
displayName: 'Alice',
|
||||
aviUrl: 'avi',
|
||||
emailConfirmed: true,
|
||||
},
|
||||
{
|
||||
service: 'https://bsky.social',
|
||||
did: BOB_DID,
|
||||
refreshJwt: 'refreshJwt',
|
||||
accessJwt: 'accessJwt',
|
||||
handle: 'bob.test',
|
||||
email: 'bob@bsky.test',
|
||||
displayName: 'Bob',
|
||||
aviUrl: 'avi',
|
||||
emailConfirmed: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
me: {
|
||||
did: ALICE_DID,
|
||||
handle: 'alice.test',
|
||||
displayName: 'Alice',
|
||||
description: '',
|
||||
avatar: 'avi',
|
||||
},
|
||||
onboarding: {step: 'Home'},
|
||||
shell: {colorMode: 'system'},
|
||||
preferences: {
|
||||
primaryLanguage: 'en',
|
||||
contentLanguages: ['en'],
|
||||
postLanguage: 'en',
|
||||
postLanguageHistory: ['en', 'en', 'ja', 'pt', 'de', 'en'],
|
||||
contentLabels: {
|
||||
nsfw: 'warn',
|
||||
nudity: 'warn',
|
||||
suggestive: 'warn',
|
||||
gore: 'warn',
|
||||
hate: 'hide',
|
||||
spam: 'hide',
|
||||
impersonation: 'warn',
|
||||
},
|
||||
savedFeeds: ['feed_a', 'feed_b', 'feed_c'],
|
||||
pinnedFeeds: ['feed_a', 'feed_b'],
|
||||
requireAltTextEnabled: false,
|
||||
},
|
||||
invitedUsers: {seenDids: [], copiedInvites: []},
|
||||
mutedThreads: {uris: []},
|
||||
reminders: {},
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
import {jest, expect, test, afterEach} from '@jest/globals'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
|
||||
import {defaults} from '#/state/persisted/schema'
|
||||
import {migrate} from '#/state/persisted/legacy'
|
||||
import * as store from '#/state/persisted/store'
|
||||
import * as persisted from '#/state/persisted'
|
||||
|
||||
const write = jest.mocked(store.write)
|
||||
const read = jest.mocked(store.read)
|
||||
|
||||
jest.mock('#/logger')
|
||||
jest.mock('#/state/persisted/legacy', () => ({
|
||||
migrate: jest.fn(),
|
||||
}))
|
||||
jest.mock('#/state/persisted/store', () => ({
|
||||
write: jest.fn(),
|
||||
read: jest.fn(),
|
||||
}))
|
||||
|
||||
afterEach(() => {
|
||||
jest.useFakeTimers()
|
||||
jest.clearAllMocks()
|
||||
AsyncStorage.clear()
|
||||
})
|
||||
|
||||
test('init: fresh install, no migration', async () => {
|
||||
await persisted.init()
|
||||
|
||||
expect(migrate).toHaveBeenCalledTimes(1)
|
||||
expect(read).toHaveBeenCalledTimes(1)
|
||||
expect(write).toHaveBeenCalledWith(defaults)
|
||||
|
||||
// default value
|
||||
expect(persisted.get('colorMode')).toBe('system')
|
||||
})
|
||||
|
||||
test('init: fresh install, migration ran', async () => {
|
||||
read.mockResolvedValueOnce(defaults)
|
||||
|
||||
await persisted.init()
|
||||
|
||||
expect(migrate).toHaveBeenCalledTimes(1)
|
||||
expect(read).toHaveBeenCalledTimes(1)
|
||||
expect(write).not.toHaveBeenCalled()
|
||||
|
||||
// default value
|
||||
expect(persisted.get('colorMode')).toBe('system')
|
||||
})
|
|
@ -0,0 +1,93 @@
|
|||
import {jest, expect, test, afterEach} from '@jest/globals'
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage'
|
||||
|
||||
import {defaults, schema} from '#/state/persisted/schema'
|
||||
import {transform, migrate} from '#/state/persisted/legacy'
|
||||
import * as store from '#/state/persisted/store'
|
||||
import {logger} from '#/logger'
|
||||
import * as fixtures from '#/state/persisted/__tests__/fixtures'
|
||||
|
||||
const write = jest.mocked(store.write)
|
||||
const read = jest.mocked(store.read)
|
||||
|
||||
jest.mock('#/logger')
|
||||
jest.mock('#/state/persisted/store', () => ({
|
||||
write: jest.fn(),
|
||||
read: jest.fn(),
|
||||
}))
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
AsyncStorage.clear()
|
||||
})
|
||||
|
||||
test('migrate: fresh install', async () => {
|
||||
await migrate()
|
||||
|
||||
expect(AsyncStorage.getItem).toHaveBeenCalledWith('root')
|
||||
expect(read).toHaveBeenCalledTimes(1)
|
||||
expect(logger.log).toHaveBeenCalledWith(
|
||||
'persisted state: no migration needed',
|
||||
)
|
||||
})
|
||||
|
||||
test('migrate: fresh install, existing new storage', async () => {
|
||||
read.mockResolvedValueOnce(defaults)
|
||||
|
||||
await migrate()
|
||||
|
||||
expect(AsyncStorage.getItem).toHaveBeenCalledWith('root')
|
||||
expect(read).toHaveBeenCalledTimes(1)
|
||||
expect(logger.log).toHaveBeenCalledWith(
|
||||
'persisted state: no migration needed',
|
||||
)
|
||||
})
|
||||
|
||||
test('migrate: fresh install, AsyncStorage error', async () => {
|
||||
const prevGetItem = AsyncStorage.getItem
|
||||
|
||||
const error = new Error('test error')
|
||||
|
||||
AsyncStorage.getItem = jest.fn(() => {
|
||||
throw error
|
||||
})
|
||||
|
||||
await migrate()
|
||||
|
||||
expect(AsyncStorage.getItem).toHaveBeenCalledWith('root')
|
||||
expect(logger.error).toHaveBeenCalledWith(error, {
|
||||
message: 'persisted state: error migrating legacy storage',
|
||||
})
|
||||
|
||||
AsyncStorage.getItem = prevGetItem
|
||||
})
|
||||
|
||||
test('migrate: has legacy data', async () => {
|
||||
await AsyncStorage.setItem('root', JSON.stringify(fixtures.LEGACY_DATA_DUMP))
|
||||
|
||||
await migrate()
|
||||
|
||||
expect(write).toHaveBeenCalledWith(transform(fixtures.LEGACY_DATA_DUMP))
|
||||
expect(logger.log).toHaveBeenCalledWith(
|
||||
'persisted state: migrated legacy storage',
|
||||
)
|
||||
})
|
||||
|
||||
test('migrate: has legacy data, fails validation', async () => {
|
||||
const legacy = fixtures.LEGACY_DATA_DUMP
|
||||
// @ts-ignore
|
||||
legacy.shell.colorMode = 'invalid'
|
||||
await AsyncStorage.setItem('root', JSON.stringify(legacy))
|
||||
|
||||
await migrate()
|
||||
|
||||
const transformed = transform(legacy)
|
||||
const validate = schema.safeParse(transformed)
|
||||
|
||||
expect(write).not.toHaveBeenCalled()
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
'persisted state: legacy data failed validation',
|
||||
// @ts-ignore
|
||||
{error: validate.error},
|
||||
)
|
||||
})
|
|
@ -2,6 +2,7 @@ import {expect, test} from '@jest/globals'
|
|||
|
||||
import {transform} from '#/state/persisted/legacy'
|
||||
import {defaults, schema} from '#/state/persisted/schema'
|
||||
import * as fixtures from '#/state/persisted/__tests__/fixtures'
|
||||
|
||||
test('defaults', () => {
|
||||
expect(() => schema.parse(defaults)).not.toThrow()
|
||||
|
@ -11,3 +12,10 @@ test('transform', () => {
|
|||
const data = transform({})
|
||||
expect(() => schema.parse(data)).not.toThrow()
|
||||
})
|
||||
|
||||
test('transform: legacy fixture', () => {
|
||||
const data = transform(fixtures.LEGACY_DATA_DUMP)
|
||||
expect(() => schema.parse(data)).not.toThrow()
|
||||
expect(data.session.currentAccount?.did).toEqual(fixtures.ALICE_DID)
|
||||
expect(data.session.accounts.length).toEqual(2)
|
||||
})
|
|
@ -7,7 +7,7 @@ import {write, read} from '#/state/persisted/store'
|
|||
/**
|
||||
* The shape of the serialized data from our legacy Mobx store.
|
||||
*/
|
||||
type LegacySchema = {
|
||||
export type LegacySchema = {
|
||||
shell: {
|
||||
colorMode: 'system' | 'light' | 'dark'
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ type LegacySchema = {
|
|||
data: {
|
||||
service: string
|
||||
did: `did:plc:${string}`
|
||||
}
|
||||
} | null
|
||||
accounts: {
|
||||
service: string
|
||||
did: `did:plc:${string}`
|
||||
|
@ -61,7 +61,7 @@ type LegacySchema = {
|
|||
copiedInvites: string[]
|
||||
}
|
||||
mutedThreads: {uris: string[]}
|
||||
reminders: {lastEmailConfirm: string}
|
||||
reminders: {lastEmailConfirm?: string}
|
||||
}
|
||||
|
||||
const DEPRECATED_ROOT_STATE_STORAGE_KEY = 'root'
|
||||
|
@ -124,6 +124,7 @@ export async function migrate() {
|
|||
const newData = await read()
|
||||
const alreadyMigrated = Boolean(newData)
|
||||
|
||||
/* TODO BEGIN DEBUG — remove this eventually */
|
||||
try {
|
||||
if (rawLegacyData) {
|
||||
const legacy = JSON.parse(rawLegacyData) as Partial<LegacySchema>
|
||||
|
@ -149,6 +150,7 @@ export async function migrate() {
|
|||
} catch (e: any) {
|
||||
logger.error(e, {message: `persisted state: legacy debugging failed`})
|
||||
}
|
||||
/* TODO END DEBUG */
|
||||
|
||||
if (!alreadyMigrated && rawLegacyData) {
|
||||
logger.info('persisted state: migrating legacy storage')
|
||||
|
|
Loading…
Reference in New Issue