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
This commit is contained in:
parent
439305b57e
commit
9027882fb4
23 changed files with 2406 additions and 658 deletions
|
@ -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(),
|
||||
|
|
199
jest/test-pds.ts
Normal file
199
jest/test-pds.ts
Normal file
|
@ -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)
|
||||
}
|
|
@ -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>,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue