about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2023-12-06 18:41:05 -0600
committerGitHub <noreply@github.com>2023-12-06 16:41:05 -0800
commit818c6ae87957767383e3c0ea0f759e278a2930c7 (patch)
treee104da955ef2888ece9534dded6d1cc5d19a99bb /src
parent07fe0585775f1c9d89399b00dc1849a6d0c5d71f (diff)
downloadvoidsky-818c6ae87957767383e3c0ea0f759e278a2930c7.tar.zst
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 test
Diffstat (limited to 'src')
-rw-r--r--src/state/persisted/__tests__/fixtures.ts67
-rw-r--r--src/state/persisted/__tests__/index.test.ts49
-rw-r--r--src/state/persisted/__tests__/migrate.test.ts93
-rw-r--r--src/state/persisted/__tests__/schema.test.ts (renamed from src/state/persisted/__tests__/legacy.test.ts)8
-rw-r--r--src/state/persisted/legacy.ts8
5 files changed, 222 insertions, 3 deletions
diff --git a/src/state/persisted/__tests__/fixtures.ts b/src/state/persisted/__tests__/fixtures.ts
new file mode 100644
index 000000000..ac8f7c8d1
--- /dev/null
+++ b/src/state/persisted/__tests__/fixtures.ts
@@ -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: {},
+}
diff --git a/src/state/persisted/__tests__/index.test.ts b/src/state/persisted/__tests__/index.test.ts
new file mode 100644
index 000000000..90c5e0e4e
--- /dev/null
+++ b/src/state/persisted/__tests__/index.test.ts
@@ -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')
+})
diff --git a/src/state/persisted/__tests__/migrate.test.ts b/src/state/persisted/__tests__/migrate.test.ts
new file mode 100644
index 000000000..d42580efd
--- /dev/null
+++ b/src/state/persisted/__tests__/migrate.test.ts
@@ -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},
+  )
+})
diff --git a/src/state/persisted/__tests__/legacy.test.ts b/src/state/persisted/__tests__/schema.test.ts
index 7f4b138a1..c78a2c27c 100644
--- a/src/state/persisted/__tests__/legacy.test.ts
+++ b/src/state/persisted/__tests__/schema.test.ts
@@ -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)
+})
diff --git a/src/state/persisted/legacy.ts b/src/state/persisted/legacy.ts
index c45b18322..d70263746 100644
--- a/src/state/persisted/legacy.ts
+++ b/src/state/persisted/legacy.ts
@@ -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')