Sifa professional network API (Fastify, AT Protocol, Jetstream)
sifa.id/
1import { describe, it, expect } from 'vitest';
2import type { FastifyRequest, FastifyReply } from 'fastify';
3import type { NodeOAuthClient } from '@atproto/oauth-client-node';
4import {
5 TokenInvalidError,
6 TokenRefreshError,
7 TokenRevokedError,
8} from '@atproto/oauth-client-node';
9import type { Database } from '../../src/db/index.js';
10import { createAuthMiddleware } from '../../src/middleware/auth.js';
11import { isPermanentSessionError } from '../../src/oauth/errors.js';
12
13describe('Auth middleware', () => {
14 it('returns 401 when no session cookie', async () => {
15 const middleware = createAuthMiddleware(
16 {} as unknown as NodeOAuthClient,
17 {} as unknown as Database,
18 );
19 const request = { cookies: {} } as unknown as FastifyRequest;
20 const reply = {
21 status: (code: number) => ({
22 send: (body: Record<string, unknown>) => ({ statusCode: code, body }),
23 }),
24 } as unknown as FastifyReply;
25
26 const result = await middleware(request, reply);
27 expect((result as { statusCode: number }).statusCode).toBe(401);
28 });
29
30 it('returns 503 when oauthClient is null', async () => {
31 const middleware = createAuthMiddleware(null, {} as unknown as Database);
32 const request = { cookies: { session: 'some-session-id' } } as unknown as FastifyRequest;
33 const reply = {
34 status: (code: number) => ({
35 send: (body: Record<string, unknown>) => ({ statusCode: code, body }),
36 }),
37 } as unknown as FastifyReply;
38
39 const result = await middleware(request, reply);
40 expect((result as { statusCode: number }).statusCode).toBe(503);
41 });
42});
43
44describe('isPermanentSessionError', () => {
45 const testDid = 'did:plc:test123';
46
47 it('returns true for TokenInvalidError', () => {
48 expect(isPermanentSessionError(new TokenInvalidError(testDid))).toBe(true);
49 expect(isPermanentSessionError(new TokenInvalidError(testDid, 'invalid_grant'))).toBe(true);
50 });
51
52 it('returns true for TokenRevokedError', () => {
53 expect(isPermanentSessionError(new TokenRevokedError(testDid))).toBe(true);
54 });
55
56 it('returns true for TokenRefreshError caused by a permanent error', () => {
57 const cause = new TokenInvalidError(testDid);
58 const err = new TokenRefreshError(testDid, 'Refresh failed', { cause });
59 expect(isPermanentSessionError(err)).toBe(true);
60 });
61
62 it('returns true for TokenRefreshError caused by a revoked token', () => {
63 const cause = new TokenRevokedError(testDid);
64 const err = new TokenRefreshError(testDid, 'Refresh failed', { cause });
65 expect(isPermanentSessionError(err)).toBe(true);
66 });
67
68 it('returns false for TokenRefreshError with a transient cause', () => {
69 const cause = new Error('fetch failed');
70 const err = new TokenRefreshError(testDid, 'Refresh failed', { cause });
71 expect(isPermanentSessionError(err)).toBe(false);
72 });
73
74 it('returns false for TokenRefreshError with no cause', () => {
75 const err = new TokenRefreshError(testDid, 'Refresh failed');
76 expect(isPermanentSessionError(err)).toBe(false);
77 });
78
79 it('returns false for network errors (transient)', () => {
80 expect(isPermanentSessionError(new Error('fetch failed'))).toBe(false);
81 expect(isPermanentSessionError(new Error('connect ECONNREFUSED'))).toBe(false);
82 expect(isPermanentSessionError(new Error('ETIMEDOUT'))).toBe(false);
83 });
84
85 it('returns false for rate limiting errors (transient)', () => {
86 const err = new Error('Failed to resolve identity: did:plc:abc');
87 err.cause = new Error('Too Many Requests');
88 expect(isPermanentSessionError(err)).toBe(false);
89 });
90
91 it('returns false for unknown errors (safe default: transient)', () => {
92 expect(isPermanentSessionError(new Error('Something unexpected happened'))).toBe(false);
93 expect(isPermanentSessionError(new Error('New error type from library v2'))).toBe(false);
94 });
95
96 it('returns false for non-Error values', () => {
97 expect(isPermanentSessionError('string error')).toBe(false);
98 expect(isPermanentSessionError(null)).toBe(false);
99 });
100});