158 lines
5.2 KiB
TypeScript
158 lines
5.2 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { eq } from 'drizzle-orm'
|
|
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
|
import { type AppDatabase, openDatabase } from '../storage/database.server'
|
|
import { connectionCredentials, connections } from '../storage/schema'
|
|
import { listConnections, requireTwitterConnection } from './repository.server'
|
|
|
|
let database: AppDatabase
|
|
beforeEach(() => {
|
|
database = openDatabase(':memory:')
|
|
vi.stubEnv('TWITTER_RELAY_BASE_URL', 'https://relay.invalid/')
|
|
})
|
|
afterEach(() => {
|
|
database.$client.close()
|
|
vi.unstubAllEnvs()
|
|
})
|
|
|
|
it('retains stable distinct IDs for multiple relay profiles across repeated discovery', async () => {
|
|
const fetchProfiles = vi.fn().mockResolvedValue(['first', 'second', 'first'])
|
|
const { connections: first } = await listConnections(database, fetchProfiles)
|
|
const { connections: second } = await listConnections(database, fetchProfiles)
|
|
expect(first).toHaveLength(2)
|
|
expect(new Set(first.map((connection) => connection.id)).size).toBe(2)
|
|
expect(second).toEqual(first)
|
|
expect(
|
|
await requireTwitterConnection(first[0]?.id ?? '', database, fetchProfiles),
|
|
).toBe(first[0]?.displayName)
|
|
expect(fetchProfiles).toHaveBeenCalledWith('https://relay.invalid')
|
|
})
|
|
|
|
it('preserves unavailable bindings and reconnects the original ID when a profile returns', async () => {
|
|
const { connections: original } = await listConnections(
|
|
database,
|
|
async () => ['first'],
|
|
)
|
|
const id = original[0]?.id ?? ''
|
|
expect((await listConnections(database, async () => [])).connections).toEqual(
|
|
[expect.objectContaining({ id, status: 'disconnected' })],
|
|
)
|
|
await expect(
|
|
requireTwitterConnection(id, database, async () => []),
|
|
).rejects.toThrow('この接続は利用できません')
|
|
expect(
|
|
(await listConnections(database, async () => ['first'])).connections,
|
|
).toEqual(original)
|
|
})
|
|
|
|
it('does not reuse an old relay binding for the same profile at another origin', async () => {
|
|
const { connections: original } = await listConnections(
|
|
database,
|
|
async () => ['first'],
|
|
)
|
|
vi.stubEnv('TWITTER_RELAY_BASE_URL', 'https://another-relay.invalid')
|
|
const { connections: current } = await listConnections(database, async () => [
|
|
'first',
|
|
])
|
|
expect(current).toHaveLength(2)
|
|
expect(current).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ id: original[0]?.id, status: 'disconnected' }),
|
|
expect.objectContaining({
|
|
origin: 'https://another-relay.invalid',
|
|
status: 'connected',
|
|
}),
|
|
]),
|
|
)
|
|
await expect(
|
|
requireTwitterConnection(original[0]?.id ?? '', database, async () => [
|
|
'first',
|
|
]),
|
|
).rejects.toThrow('この接続は利用できません')
|
|
})
|
|
|
|
it('does not convert a relay outage into persisted account removal', async () => {
|
|
const original = await listConnections(database, async () => ['first'])
|
|
const unavailable = vi
|
|
.fn()
|
|
.mockRejectedValue(new Error('private network detail'))
|
|
const failed = await listConnections(database, unavailable)
|
|
expect(failed.connections).toEqual(original.connections)
|
|
expect(failed.relayError).toContain(
|
|
'Relay への接続を確認して再試行してください。',
|
|
)
|
|
expect(database.select().from(connections).get()?.status).toBe('connected')
|
|
expect(await listConnections(database, async () => ['first'])).toEqual(
|
|
original,
|
|
)
|
|
})
|
|
|
|
it('returns public metadata for all platforms without joining or exposing credentials', async () => {
|
|
database
|
|
.insert(connections)
|
|
.values({
|
|
id: 'mastodon-account',
|
|
platform: 'mastodon',
|
|
origin: 'https://mastodon.invalid',
|
|
accountId: '42',
|
|
displayName: '@owner',
|
|
status: 'connected',
|
|
createdAt: 1,
|
|
updatedAt: 1,
|
|
})
|
|
.run()
|
|
database
|
|
.insert(connectionCredentials)
|
|
.values({
|
|
connectionId: 'mastodon-account',
|
|
encryptedToken: 'stored-secret-envelope',
|
|
updatedAt: 1,
|
|
})
|
|
.run()
|
|
const { connections: discovered } = await listConnections(
|
|
database,
|
|
async () => ['first'],
|
|
)
|
|
expect(discovered[0]).toEqual({
|
|
id: 'mastodon-account',
|
|
platform: 'mastodon',
|
|
origin: 'https://mastodon.invalid',
|
|
accountId: '42',
|
|
displayName: '@owner',
|
|
status: 'connected',
|
|
})
|
|
expect(Object.keys(discovered[1] ?? {}).sort()).toEqual([
|
|
'accountId',
|
|
'displayName',
|
|
'id',
|
|
'origin',
|
|
'platform',
|
|
'status',
|
|
])
|
|
expect(JSON.stringify(discovered)).not.toContain('stored-secret-envelope')
|
|
const outage = await listConnections(database, async () => {
|
|
throw new Error('offline')
|
|
})
|
|
expect(outage.connections).toEqual(discovered)
|
|
expect(outage.relayError).toBeDefined()
|
|
await expect(
|
|
requireTwitterConnection('mastodon-account', database, async () => [
|
|
'first',
|
|
]),
|
|
).rejects.toThrow('この接続は利用できません')
|
|
expect(
|
|
database
|
|
.select()
|
|
.from(connections)
|
|
.where(eq(connections.id, 'mastodon-account'))
|
|
.get()?.status,
|
|
).toBe('connected')
|
|
})
|
|
|
|
it('rejects an unknown connection without treating its ID as a relay profile', async () => {
|
|
await expect(
|
|
requireTwitterConnection('first', database, async () => ['first']),
|
|
).rejects.toThrow('この接続は利用できません')
|
|
})
|