about summary refs log tree commit diff
path: root/jest
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2023-01-24 09:06:27 -0600
committerGitHub <noreply@github.com>2023-01-24 09:06:27 -0600
commit9027882fb401df2a9df6a89facb2bdb94b8b731b (patch)
treedc60ca1a2cc1be0838229f06b588f56871f2b91e /jest
parent439305b57e0c20799d87baf92c067ec8e262ea13 (diff)
downloadvoidsky-9027882fb401df2a9df6a89facb2bdb94b8b731b.tar.zst
Account switcher (#85)
* Update the account-create and signin views to use the design system.

Also:
- Add borderDark to the theme
- Start to an account selector in the signin flow

* Dark mode fixes in signin ui

* Track multiple active accounts and provide account-switching UI

* Add test tooling for an in-memory pds

* Add complete integration tests for login and the account switcher
Diffstat (limited to 'jest')
-rw-r--r--jest/jestSetup.js13
-rw-r--r--jest/test-pds.ts199
-rw-r--r--jest/test-utils.tsx13
3 files changed, 205 insertions, 20 deletions
diff --git a/jest/jestSetup.js b/jest/jestSetup.js
index b1f110a53..8f95062a9 100644
--- a/jest/jestSetup.js
+++ b/jest/jestSetup.js
@@ -25,19 +25,6 @@ jest.mock('react-native-safe-area-context', () => {
   }
 })
 
-jest.mock('@gorhom/bottom-sheet', () => {
-  const react = require('react-native')
-  return {
-    __esModule: true,
-    default: react.View,
-    namedExport: {
-      ...require('react-native-reanimated/mock'),
-      ...jest.requireActual('@gorhom/bottom-sheet'),
-      BottomSheetFlatList: react.FlatList,
-    },
-  }
-})
-
 jest.mock('rn-fetch-blob', () => ({
   config: jest.fn().mockReturnThis(),
   cancel: jest.fn(),
diff --git a/jest/test-pds.ts b/jest/test-pds.ts
new file mode 100644
index 000000000..4915b58e3
--- /dev/null
+++ b/jest/test-pds.ts
@@ -0,0 +1,199 @@
+import {AddressInfo} from 'net'
+import os from 'os'
+import path from 'path'
+import * as crypto from '@atproto/crypto'
+import PDSServer, {
+  Database as PDSDatabase,
+  MemoryBlobStore,
+  ServerConfig as PDSServerConfig,
+} from '@atproto/pds'
+import * as plc from '@atproto/plc'
+import AtpApi, {ServiceClient} from '@atproto/api'
+
+export interface TestUser {
+  email: string
+  did: string
+  declarationCid: string
+  handle: string
+  password: string
+  api: ServiceClient
+}
+
+export interface TestUsers {
+  alice: TestUser
+  bob: TestUser
+  carla: TestUser
+}
+
+export interface TestPDS {
+  pdsUrl: string
+  users: TestUsers
+  close: () => Promise<void>
+}
+
+// NOTE
+// deterministic date generator
+// we use this to ensure the mock dataset is always the same
+// which is very useful when testing
+function* dateGen() {
+  let start = 1657846031914
+  while (true) {
+    yield new Date(start).toISOString()
+    start += 1e3
+  }
+  return ''
+}
+
+export async function createServer(): Promise<TestPDS> {
+  const keypair = await crypto.EcdsaKeypair.create()
+
+  // run plc server
+  const plcDb = plc.Database.memory()
+  await plcDb.migrateToLatestOrThrow()
+  const plcServer = plc.PlcServer.create({db: plcDb})
+  const plcListener = await plcServer.start()
+  const plcPort = (plcListener.address() as AddressInfo).port
+  const plcUrl = `http://localhost:${plcPort}`
+
+  const recoveryKey = (await crypto.EcdsaKeypair.create()).did()
+
+  const plcClient = new plc.PlcClient(plcUrl)
+  const serverDid = await plcClient.createDid(
+    keypair,
+    recoveryKey,
+    'localhost',
+    'https://pds.public.url',
+  )
+
+  const blobstoreLoc = path.join(os.tmpdir(), crypto.randomStr(5, 'base32'))
+
+  const cfg = new PDSServerConfig({
+    debugMode: true,
+    version: '0.0.0',
+    scheme: 'http',
+    hostname: 'localhost',
+    serverDid,
+    recoveryKey,
+    adminPassword: 'admin-pass',
+    inviteRequired: false,
+    didPlcUrl: plcUrl,
+    jwtSecret: 'jwt-secret',
+    availableUserDomains: ['.test'],
+    appUrlPasswordReset: 'app://forgot-password',
+    emailNoReplyAddress: 'noreply@blueskyweb.xyz',
+    publicUrl: 'https://pds.public.url',
+    imgUriSalt: '9dd04221f5755bce5f55f47464c27e1e',
+    imgUriKey:
+      'f23ecd142835025f42c3db2cf25dd813956c178392760256211f9d315f8ab4d8',
+    dbPostgresUrl: process.env.DB_POSTGRES_URL,
+    blobstoreLocation: `${blobstoreLoc}/blobs`,
+    blobstoreTmp: `${blobstoreLoc}/tmp`,
+  })
+
+  const db = PDSDatabase.memory()
+  await db.migrateToLatestOrThrow()
+  const blobstore = new MemoryBlobStore()
+
+  const pds = PDSServer.create({db, blobstore, keypair, config: cfg})
+  const pdsServer = await pds.start()
+  const pdsPort = (pdsServer.address() as AddressInfo).port
+  const pdsUrl = `http://localhost:${pdsPort}`
+  const testUsers = await genMockData(pdsUrl)
+
+  return {
+    pdsUrl,
+    users: testUsers,
+    async close() {
+      await pds.destroy()
+      await plcServer.destroy()
+    },
+  }
+}
+
+async function genMockData(pdsUrl: string): Promise<TestUsers> {
+  const date = dateGen()
+
+  const clients = {
+    loggedout: AtpApi.service(pdsUrl),
+    alice: AtpApi.service(pdsUrl),
+    bob: AtpApi.service(pdsUrl),
+    carla: AtpApi.service(pdsUrl),
+  }
+  const users: TestUser[] = [
+    {
+      email: 'alice@test.com',
+      did: '',
+      declarationCid: '',
+      handle: 'alice.test',
+      password: 'hunter2',
+      api: clients.alice,
+    },
+    {
+      email: 'bob@test.com',
+      did: '',
+      declarationCid: '',
+      handle: 'bob.test',
+      password: 'hunter2',
+      api: clients.bob,
+    },
+    {
+      email: 'carla@test.com',
+      did: '',
+      declarationCid: '',
+      handle: 'carla.test',
+      password: 'hunter2',
+      api: clients.carla,
+    },
+  ]
+  const alice = users[0]
+  const bob = users[1]
+  const carla = users[2]
+
+  let _i = 1
+  for (const user of users) {
+    const res = await clients.loggedout.com.atproto.account.create({
+      email: user.email,
+      handle: user.handle,
+      password: user.password,
+    })
+    user.api.setHeader('Authorization', `Bearer ${res.data.accessJwt}`)
+    const {data: profile} = await user.api.app.bsky.actor.getProfile({
+      actor: user.handle,
+    })
+    user.did = res.data.did
+    user.declarationCid = profile.declaration.cid
+    await user.api.app.bsky.actor.profile.create(
+      {did: user.did},
+      {
+        displayName: ucfirst(user.handle).slice(0, -5),
+        description: `Test user ${_i++}`,
+      },
+    )
+  }
+
+  // everybody follows everybody
+  const follow = async (author: TestUser, subject: TestUser) => {
+    await author.api.app.bsky.graph.follow.create(
+      {did: author.did},
+      {
+        subject: {
+          did: subject.did,
+          declarationCid: subject.declarationCid,
+        },
+        createdAt: date.next().value,
+      },
+    )
+  }
+  await follow(alice, bob)
+  await follow(alice, carla)
+  await follow(bob, alice)
+  await follow(bob, carla)
+  await follow(carla, alice)
+  await follow(carla, bob)
+
+  return {alice, bob, carla}
+}
+
+function ucfirst(str: string): string {
+  return str.at(0)?.toUpperCase() + str.slice(1)
+}
diff --git a/jest/test-utils.tsx b/jest/test-utils.tsx
index c84ee637e..5a74a6ef6 100644
--- a/jest/test-utils.tsx
+++ b/jest/test-utils.tsx
@@ -4,20 +4,19 @@ import {GestureHandlerRootView} from 'react-native-gesture-handler'
 import {RootSiblingParent} from 'react-native-root-siblings'
 import {SafeAreaProvider} from 'react-native-safe-area-context'
 import {RootStoreProvider} from '../src/state'
+import {ThemeProvider} from '../src/view/lib/ThemeContext'
 import {mockedRootStore} from '../__mocks__/state-mock'
 
-const customRender = (ui: any, storeMock?: any) =>
+const customRender = (ui: any, rootStore?: any) =>
   render(
     // eslint-disable-next-line react-native/no-inline-styles
     <GestureHandlerRootView style={{flex: 1}}>
       <RootSiblingParent>
         <RootStoreProvider
-          value={
-            storeMock != null
-              ? {...mockedRootStore, ...storeMock}
-              : mockedRootStore
-          }>
-          <SafeAreaProvider>{ui}</SafeAreaProvider>
+          value={rootStore != null ? rootStore : mockedRootStore}>
+          <ThemeProvider theme="light">
+            <SafeAreaProvider>{ui}</SafeAreaProvider>
+          </ThemeProvider>
         </RootStoreProvider>
       </RootSiblingParent>
     </GestureHandlerRootView>,