Refactor session management to use a new "Agent" API (#165)

* Add the atp-agent implementation (temporarily in this repo)

* Rewrite all session & API management to use the new atp-agent

* Update tests for the atp-agent refactor

* Refactor management of session-related state. Includes:
- More careful management of when state is cleared or fetched
- Debug logging to help trace future issues
- Clearer APIs overall

* Bubble session-expiration events to the user and display a toast to explain

* Switch to the new @atproto/api@0.1.0
This commit is contained in:
Paul Frazee
2023-02-07 16:04:08 -06:00
committed by GitHub
parent 0e160298c8
commit 759ec8011b
23 changed files with 464 additions and 471 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ import {cleanup, fireEvent, render, waitFor} from '../jest/test-utils'
import {createServer, TestPDS} from '../jest/test-pds'
import {RootStoreModel, setupState} from '../src/state'
const WAIT_OPTS = {timeout: 5e3}
const WAIT_OPTS = {timeout: 10e3}
describe('Account flows', () => {
let pds: TestPDS | undefined
@@ -2,7 +2,7 @@ import {RootStoreModel} from '../../../src/state/models/root-store'
import {LinkMetasViewModel} from '../../../src/state/models/link-metas-view'
import * as LinkMetaLib from '../../../src/lib/link-meta'
import {LikelyType} from './../../../src/lib/link-meta'
import {sessionClient, SessionServiceClient} from '@atproto/api'
import AtpAgent from '@atproto/api'
import {DEFAULT_SERVICE} from '../../../src/state'
describe('LinkMetasViewModel', () => {
@@ -17,8 +17,7 @@ describe('LinkMetasViewModel', () => {
}
beforeEach(() => {
const api = sessionClient.service(DEFAULT_SERVICE) as SessionServiceClient
rootStore = new RootStoreModel(api)
rootStore = new RootStoreModel(new AtpAgent({service: DEFAULT_SERVICE}))
viewModel = new LinkMetasViewModel(rootStore)
})
+91 -110
View File
@@ -1,106 +1,110 @@
import {RootStoreModel} from '../../../src/state/models/root-store'
import {MeModel} from '../../../src/state/models/me'
import {NotificationsViewModel} from './../../../src/state/models/notifications-view'
import {sessionClient, SessionServiceClient} from '@atproto/api'
import {DEFAULT_SERVICE} from './../../../src/state/index'
import {createServer, TestPDS} from '../../../jest/test-pds'
import {RootStoreModel, setupState} from '../../../src/state'
import {NotificationsViewModel} from '../../../src/state/models/notifications-view'
describe('MeModel', () => {
let pds: TestPDS | undefined
let rootStore: RootStoreModel
let meModel: MeModel
beforeEach(() => {
const api = sessionClient.service(DEFAULT_SERVICE) as SessionServiceClient
rootStore = new RootStoreModel(api)
meModel = new MeModel(rootStore)
beforeAll(async () => {
jest.useFakeTimers()
pds = await createServer()
rootStore = await setupState(pds.pdsUrl)
})
afterAll(() => {
afterAll(async () => {
jest.clearAllMocks()
await pds?.close()
})
it('should clear() correctly', () => {
meModel.did = '123'
meModel.handle = 'handle'
meModel.displayName = 'John Doe'
meModel.description = 'description'
meModel.avatar = 'avatar'
meModel.notificationCount = 1
meModel.clear()
expect(meModel.did).toEqual('')
expect(meModel.handle).toEqual('')
expect(meModel.displayName).toEqual('')
expect(meModel.description).toEqual('')
expect(meModel.avatar).toEqual('')
expect(meModel.notificationCount).toEqual(0)
rootStore.me.did = '123'
rootStore.me.handle = 'handle'
rootStore.me.displayName = 'John Doe'
rootStore.me.description = 'description'
rootStore.me.avatar = 'avatar'
rootStore.me.notificationCount = 1
rootStore.me.clear()
expect(rootStore.me.did).toEqual('')
expect(rootStore.me.handle).toEqual('')
expect(rootStore.me.displayName).toEqual('')
expect(rootStore.me.description).toEqual('')
expect(rootStore.me.avatar).toEqual('')
expect(rootStore.me.notificationCount).toEqual(0)
})
it('should hydrate() successfully with valid properties', () => {
meModel.hydrate({
rootStore.me.clear()
rootStore.me.hydrate({
did: '123',
handle: 'handle',
displayName: 'John Doe',
description: 'description',
avatar: 'avatar',
})
expect(meModel.did).toEqual('123')
expect(meModel.handle).toEqual('handle')
expect(meModel.displayName).toEqual('John Doe')
expect(meModel.description).toEqual('description')
expect(meModel.avatar).toEqual('avatar')
expect(rootStore.me.did).toEqual('123')
expect(rootStore.me.handle).toEqual('handle')
expect(rootStore.me.displayName).toEqual('John Doe')
expect(rootStore.me.description).toEqual('description')
expect(rootStore.me.avatar).toEqual('avatar')
})
it('should not hydrate() with invalid properties', () => {
meModel.hydrate({
rootStore.me.clear()
rootStore.me.hydrate({
did: '',
handle: 'handle',
displayName: 'John Doe',
description: 'description',
avatar: 'avatar',
})
expect(meModel.did).toEqual('')
expect(meModel.handle).toEqual('')
expect(meModel.displayName).toEqual('')
expect(meModel.description).toEqual('')
expect(meModel.avatar).toEqual('')
expect(rootStore.me.did).toEqual('')
expect(rootStore.me.handle).toEqual('')
expect(rootStore.me.displayName).toEqual('')
expect(rootStore.me.description).toEqual('')
expect(rootStore.me.avatar).toEqual('')
meModel.hydrate({
rootStore.me.hydrate({
did: '123',
displayName: 'John Doe',
description: 'description',
avatar: 'avatar',
})
expect(meModel.did).toEqual('')
expect(meModel.handle).toEqual('')
expect(meModel.displayName).toEqual('')
expect(meModel.description).toEqual('')
expect(meModel.avatar).toEqual('')
expect(rootStore.me.did).toEqual('')
expect(rootStore.me.handle).toEqual('')
expect(rootStore.me.displayName).toEqual('')
expect(rootStore.me.description).toEqual('')
expect(rootStore.me.avatar).toEqual('')
})
it('should serialize() key information', () => {
rootStore.me.did = '123'
rootStore.me.handle = 'handle'
rootStore.me.displayName = 'John Doe'
rootStore.me.description = 'description'
rootStore.me.avatar = 'avatar'
expect(rootStore.me.serialize()).toEqual({
did: '123',
handle: 'handle',
displayName: 'John Doe',
description: 'description',
avatar: 'avatar',
})
})
it('should load() successfully', async () => {
jest
.spyOn(rootStore.api.app.bsky.actor, 'getProfile')
.mockImplementationOnce((): Promise<any> => {
return Promise.resolve({
data: {
displayName: 'John Doe',
description: 'description',
avatar: 'avatar',
},
})
})
rootStore.session.data = {
did: '123',
handle: 'handle',
service: 'test service',
accessJwt: 'test token',
refreshJwt: 'test token',
}
await meModel.load()
expect(meModel.did).toEqual('123')
expect(meModel.handle).toEqual('handle')
expect(meModel.displayName).toEqual('John Doe')
expect(meModel.description).toEqual('description')
expect(meModel.avatar).toEqual('avatar')
await rootStore.session.login({
service: pds?.pdsUrl || '',
identifier: 'alice.test',
password: 'hunter2',
})
await rootStore.me.load()
expect(typeof rootStore.me.did).toEqual('string')
expect(rootStore.me.handle).toEqual('alice.test')
expect(rootStore.me.displayName).toEqual('Alice')
expect(rootStore.me.description).toEqual('Test user 1')
expect(rootStore.me.avatar).toEqual('')
})
it('should load() successfully without profile data', async () => {
@@ -111,55 +115,32 @@ describe('MeModel', () => {
data: null,
})
})
rootStore.session.data = {
did: '123',
handle: 'handle',
service: 'test service',
accessJwt: 'test token',
refreshJwt: 'test token',
}
await meModel.load()
expect(meModel.did).toEqual('123')
expect(meModel.handle).toEqual('handle')
expect(meModel.displayName).toEqual('')
expect(meModel.description).toEqual('')
expect(meModel.avatar).toEqual('')
await rootStore.me.load()
expect(typeof rootStore.me.did).toEqual('string')
expect(rootStore.me.handle).toEqual('alice.test')
expect(rootStore.me.displayName).toEqual('')
expect(rootStore.me.description).toEqual('')
expect(rootStore.me.avatar).toEqual('')
})
it('should load() to nothing when no session', async () => {
rootStore.session.data = null
await meModel.load()
expect(meModel.did).toEqual('')
expect(meModel.handle).toEqual('')
expect(meModel.displayName).toEqual('')
expect(meModel.description).toEqual('')
expect(meModel.avatar).toEqual('')
expect(meModel.notificationCount).toEqual(0)
})
it('should serialize() key information', () => {
meModel.did = '123'
meModel.handle = 'handle'
meModel.displayName = 'John Doe'
meModel.description = 'description'
meModel.avatar = 'avatar'
expect(meModel.serialize()).toEqual({
did: '123',
handle: 'handle',
displayName: 'John Doe',
description: 'description',
avatar: 'avatar',
})
await rootStore.session.logout()
await rootStore.me.load()
expect(rootStore.me.did).toEqual('')
expect(rootStore.me.handle).toEqual('')
expect(rootStore.me.displayName).toEqual('')
expect(rootStore.me.description).toEqual('')
expect(rootStore.me.avatar).toEqual('')
expect(rootStore.me.notificationCount).toEqual(0)
})
it('should clearNotificationCount() successfully', () => {
meModel.clearNotificationCount()
expect(meModel.notificationCount).toBe(0)
rootStore.me.clearNotificationCount()
expect(rootStore.me.notificationCount).toBe(0)
})
it('should update notifs count with fetchStateUpdate()', async () => {
meModel.notifications = {
rootStore.me.notifications = {
refresh: jest.fn().mockResolvedValue({}),
} as unknown as NotificationsViewModel
@@ -173,8 +154,8 @@ describe('MeModel', () => {
})
})
await meModel.fetchNotifications()
expect(meModel.notificationCount).toBe(1)
expect(meModel.notifications.refresh).toHaveBeenCalled()
await rootStore.me.fetchNotifications()
expect(rootStore.me.notificationCount).toBe(1)
expect(rootStore.me.notifications.refresh).toHaveBeenCalled()
})
})
+1 -1
View File
@@ -17,7 +17,7 @@ describe('rootStore', () => {
})
it('should call the clearAll() resets state correctly', () => {
rootStore.clearAll()
rootStore.clearAllSessionState()
expect(rootStore.session.data).toEqual(null)
expect(rootStore.nav.tabs).toEqual([