aaedafc6ba
* Replace `tap`'s event validation from "zod" to "@atproto/lex" * Expose `record` data as parsed atproto data (including CIDs and Uint8Arrays) * Minor change to validation of integers in lex data * tidy * tidy * test using vitest * add missing `tap` from root `tsconfig.json`
210 lines
5.7 KiB
TypeScript
210 lines
5.7 KiB
TypeScript
import { once } from 'node:events'
|
|
import * as http from 'node:http'
|
|
import { AddressInfo } from 'node:net'
|
|
import { default as express } from 'express'
|
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'
|
|
import { Tap } from '../src/client'
|
|
|
|
describe('Tap client', () => {
|
|
describe('constructor', () => {
|
|
it('accepts http URL', () => {
|
|
const tap = new Tap('http://localhost:8080')
|
|
expect(tap.url).toBe('http://localhost:8080')
|
|
})
|
|
|
|
it('accepts https URL', () => {
|
|
const tap = new Tap('https://example.com')
|
|
expect(tap.url).toBe('https://example.com')
|
|
})
|
|
|
|
it('throws on invalid URL', () => {
|
|
expect(() => new Tap('ws://localhost:8080')).toThrow(
|
|
'Invalid URL, expected http:// or https://',
|
|
)
|
|
expect(() => new Tap('localhost:8080')).toThrow(
|
|
'Invalid URL, expected http:// or https://',
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('HTTP methods', () => {
|
|
let server: http.Server<
|
|
typeof http.IncomingMessage,
|
|
typeof http.ServerResponse
|
|
>
|
|
let tap: Tap
|
|
let requests: {
|
|
path: string
|
|
method: string
|
|
body?: unknown
|
|
headers: http.IncomingHttpHeaders
|
|
}[]
|
|
|
|
beforeAll(async () => {
|
|
const app = express()
|
|
app.use(express.json())
|
|
|
|
requests = []
|
|
|
|
app.post('/repos/add', (req, res) => {
|
|
requests.push({
|
|
path: req.path,
|
|
method: req.method,
|
|
body: req.body,
|
|
headers: req.headers,
|
|
})
|
|
res.sendStatus(200)
|
|
})
|
|
|
|
app.post('/repos/remove', (req, res) => {
|
|
requests.push({
|
|
path: req.path,
|
|
method: req.method,
|
|
body: req.body,
|
|
headers: req.headers,
|
|
})
|
|
res.sendStatus(200)
|
|
})
|
|
|
|
app.get('/resolve/:did', (req, res) => {
|
|
requests.push({
|
|
path: req.path,
|
|
method: req.method,
|
|
headers: req.headers,
|
|
})
|
|
if (req.params.did === 'did:example:notfound') {
|
|
res.sendStatus(404)
|
|
return
|
|
}
|
|
res.json({
|
|
id: req.params.did,
|
|
alsoKnownAs: ['at://alice.test'],
|
|
verificationMethod: [],
|
|
service: [],
|
|
})
|
|
})
|
|
|
|
app.get('/info/:did', (req, res) => {
|
|
requests.push({
|
|
path: req.path,
|
|
method: req.method,
|
|
headers: req.headers,
|
|
})
|
|
res.json({
|
|
did: req.params.did,
|
|
handle: 'alice.test',
|
|
state: 'active',
|
|
rev: '3abc123',
|
|
records: 42,
|
|
})
|
|
})
|
|
|
|
server = app.listen()
|
|
await once(server, 'listening')
|
|
const { port } = server.address() as AddressInfo
|
|
tap = new Tap(`http://localhost:${port}`, { adminPassword: 'secret' })
|
|
})
|
|
|
|
afterAll(async () => {
|
|
await new Promise((resolve) => server.close(resolve))
|
|
})
|
|
|
|
beforeEach(() => {
|
|
requests = []
|
|
})
|
|
|
|
describe('addRepos', () => {
|
|
it('sends POST to /repos/add with dids', async () => {
|
|
await tap.addRepos(['did:example:alice', 'did:example:bob'])
|
|
expect(requests).toHaveLength(1)
|
|
expect(requests[0].path).toBe('/repos/add')
|
|
expect(requests[0].method).toBe('POST')
|
|
expect(requests[0].body).toEqual({
|
|
dids: ['did:example:alice', 'did:example:bob'],
|
|
})
|
|
})
|
|
|
|
it('includes auth header', async () => {
|
|
await tap.addRepos(['did:example:alice'])
|
|
expect(requests[0].headers.authorization).toBe('Basic YWRtaW46c2VjcmV0')
|
|
})
|
|
})
|
|
|
|
describe('removeRepos', () => {
|
|
it('sends POST to /repos/remove with dids', async () => {
|
|
await tap.removeRepos(['did:example:alice'])
|
|
expect(requests).toHaveLength(1)
|
|
expect(requests[0].path).toBe('/repos/remove')
|
|
expect(requests[0].method).toBe('POST')
|
|
expect(requests[0].body).toEqual({ dids: ['did:example:alice'] })
|
|
})
|
|
})
|
|
|
|
describe('resolveDid', () => {
|
|
it('fetches and parses DID document', async () => {
|
|
const doc = await tap.resolveDid('did:example:alice')
|
|
expect(doc).not.toBeNull()
|
|
expect(doc?.id).toBe('did:example:alice')
|
|
expect(doc?.alsoKnownAs).toEqual(['at://alice.test'])
|
|
})
|
|
|
|
it('returns null for 404', async () => {
|
|
const doc = await tap.resolveDid('did:example:notfound')
|
|
expect(doc).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('getRepoInfo', () => {
|
|
it('fetches and parses repo info', async () => {
|
|
const info = await tap.getRepoInfo('did:example:alice')
|
|
expect(info.did).toBe('did:example:alice')
|
|
expect(info.handle).toBe('alice.test')
|
|
expect(info.state).toBe('active')
|
|
expect(info.records).toBe(42)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('HTTP error handling', () => {
|
|
let server: http.Server<
|
|
typeof http.IncomingMessage,
|
|
typeof http.ServerResponse
|
|
>
|
|
let tap: Tap
|
|
|
|
beforeAll(async () => {
|
|
const app = express()
|
|
app.use(express.json())
|
|
|
|
app.post('/repos/add', (_req, res) => {
|
|
res.status(500).send('Internal Server Error')
|
|
})
|
|
|
|
app.get('/info/:did', (_req, res) => {
|
|
res.status(500).send('Internal Server Error')
|
|
})
|
|
|
|
server = app.listen()
|
|
await once(server, 'listening')
|
|
const { port } = server.address() as AddressInfo
|
|
tap = new Tap(`http://localhost:${port}`)
|
|
})
|
|
|
|
afterAll(async () => {
|
|
await new Promise((resolve) => server.close(resolve))
|
|
})
|
|
|
|
it('throws on addRepos failure', async () => {
|
|
await expect(tap.addRepos(['did:example:alice'])).rejects.toThrow(
|
|
'Failed to add repos',
|
|
)
|
|
})
|
|
|
|
it('throws on getRepoInfo failure', async () => {
|
|
await expect(tap.getRepoInfo('did:example:alice')).rejects.toThrow(
|
|
'Failed to get repo info',
|
|
)
|
|
})
|
|
})
|
|
})
|