File size: 5,905 Bytes
1f21206 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
import * as fs from 'node:fs/promises'
import * as os from 'node:os'
import * as path from 'node:path'
import { handleAdaptersApi } from '../api/adapters.js'
let tmpDir: string
let originalConfigDir: string | undefined
async function setup() {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'claude-adapters-test-'))
originalConfigDir = process.env.CLAUDE_CONFIG_DIR
process.env.CLAUDE_CONFIG_DIR = tmpDir
}
async function teardown() {
if (originalConfigDir !== undefined) {
process.env.CLAUDE_CONFIG_DIR = originalConfigDir
} else {
delete process.env.CLAUDE_CONFIG_DIR
}
await fs.rm(tmpDir, { recursive: true, force: true })
}
function makeRequest(method: string, pathName: string, body?: Record<string, unknown>) {
const url = new URL(pathName, 'http://localhost:3456')
const req = new Request(url.toString(), {
method,
headers: body ? { 'Content-Type': 'application/json' } : undefined,
body: body ? JSON.stringify(body) : undefined,
})
const segments = url.pathname.split('/').filter(Boolean)
return { req, url, segments }
}
describe('Adapters API', () => {
beforeEach(setup)
afterEach(teardown)
it('masks WeChat bot tokens in GET responses', async () => {
const put = makeRequest('PUT', '/api/adapters', {
wechat: {
accountId: 'bot-1',
botToken: 'wechat-secret-token',
baseUrl: 'https://ilinkai.weixin.qq.com',
userId: 'wx-user',
pairedUsers: [{ userId: 'wx-user', displayName: 'WeChat User', pairedAt: 1 }],
},
})
expect((await handleAdaptersApi(put.req, put.url, put.segments)).status).toBe(200)
const get = makeRequest('GET', '/api/adapters')
const res = await handleAdaptersApi(get.req, get.url, get.segments)
expect(res.status).toBe(200)
const json = await res.json() as any
expect(json.wechat.botToken).toBe('****oken')
expect(json.wechat.accountId).toBe('bot-1')
})
it('writes adapter credentials with owner-only permissions', async () => {
const put = makeRequest('PUT', '/api/adapters', {
telegram: {
botToken: 'telegram-secret-token',
},
})
expect((await handleAdaptersApi(put.req, put.url, put.segments)).status).toBe(200)
const configPath = path.join(tmpDir, 'adapters.json')
const stat = await fs.stat(configPath)
if (process.platform === 'win32') {
expect(stat.isFile()).toBe(true)
return
}
expect(stat.mode & 0o777).toBe(0o600)
})
it('masks and preserves DingTalk client secrets', async () => {
const put = makeRequest('PUT', '/api/adapters', {
dingtalk: {
clientId: 'ding-client-1',
clientSecret: 'dingtalk-client-secret',
permissionCardTemplateId: 'permission-template',
pairedUsers: [{ userId: 'ding-user', displayName: 'DingTalk User', pairedAt: 1 }],
},
})
expect((await handleAdaptersApi(put.req, put.url, put.segments)).status).toBe(200)
const get = makeRequest('GET', '/api/adapters')
const res = await handleAdaptersApi(get.req, get.url, get.segments)
expect(res.status).toBe(200)
const json = await res.json() as any
expect(json.dingtalk.clientSecret).toBe('****cret')
expect(json.dingtalk.clientId).toBe('ding-client-1')
expect(json.dingtalk.permissionCardTemplateId).toBe('permission-template')
const maskedPut = makeRequest('PUT', '/api/adapters', {
dingtalk: {
clientSecret: json.dingtalk.clientSecret,
allowedUsers: ['ding-user'],
},
})
expect((await handleAdaptersApi(maskedPut.req, maskedPut.url, maskedPut.segments)).status).toBe(200)
const raw = JSON.parse(await fs.readFile(path.join(tmpDir, 'adapters.json'), 'utf-8')) as any
expect(raw.dingtalk.clientSecret).toBe('dingtalk-client-secret')
expect(raw.dingtalk.allowedUsers).toEqual(['ding-user'])
expect(raw.dingtalk.permissionCardTemplateId).toBe('permission-template')
})
it('clears WeChat credentials on unbind', async () => {
const put = makeRequest('PUT', '/api/adapters', {
wechat: {
accountId: 'bot-1',
botToken: 'wechat-secret-token',
userId: 'wx-user',
allowedUsers: ['wx-allowed-user'],
pairedUsers: [{ userId: 'wx-user', displayName: 'WeChat User', pairedAt: 1 }],
},
})
await handleAdaptersApi(put.req, put.url, put.segments)
const unbind = makeRequest('POST', '/api/adapters/wechat/unbind')
const res = await handleAdaptersApi(unbind.req, unbind.url, unbind.segments)
expect(res.status).toBe(200)
const json = await res.json() as any
expect(json.wechat.botToken).toBeUndefined()
expect(json.wechat.accountId).toBeUndefined()
expect(json.wechat.userId).toBeUndefined()
expect(json.wechat.allowedUsers).toEqual([])
expect(json.wechat.pairedUsers).toEqual([])
})
it('clears DingTalk credentials on unbind', async () => {
const put = makeRequest('PUT', '/api/adapters', {
dingtalk: {
clientId: 'ding-client-1',
clientSecret: 'dingtalk-client-secret',
allowedUsers: ['ding-allowed-user'],
permissionCardTemplateId: 'permission-template',
pairedUsers: [{ userId: 'ding-user', displayName: 'DingTalk User', pairedAt: 1 }],
},
})
await handleAdaptersApi(put.req, put.url, put.segments)
const unbind = makeRequest('POST', '/api/adapters/dingtalk/unbind')
const res = await handleAdaptersApi(unbind.req, unbind.url, unbind.segments)
expect(res.status).toBe(200)
const json = await res.json() as any
expect(json.dingtalk.clientId).toBeUndefined()
expect(json.dingtalk.clientSecret).toBeUndefined()
expect(json.dingtalk.allowedUsers).toEqual([])
expect(json.dingtalk.permissionCardTemplateId).toBeUndefined()
expect(json.dingtalk.pairedUsers).toEqual([])
})
})
|