mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
DingTalk bot unbind was routed through the generic adapter config update, but undefined credential fields are dropped during JSON serialization. That left stored Client ID and Secret values intact after clicking the unbind button. Match the working WeChat path with a dedicated server endpoint and frontend API call so credential removal is explicit. Constraint: JSON request bodies cannot preserve undefined fields for generic config clearing Rejected: Keep using updateConfig with undefined fields | serialized requests omit the fields before the server can clear them Confidence: high Scope-risk: narrow Directive: Keep credential-clearing flows on explicit unbind endpoints rather than relying on partial config merges Tested: bun test src/server/__tests__/adapters.test.ts Tested: cd desktop && bun run test src/stores/adapterStore.test.ts Tested: bun run check:desktop Tested: bun run check:server Not-tested: bun run quality:pr is blocked by repository policy requiring allow-cli-core-change approval for the broader branch impact
152 lines
5.7 KiB
TypeScript
152 lines
5.7 KiB
TypeScript
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)
|
|
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([])
|
|
})
|
|
})
|