codev / src /server /__tests__ /h5-access-service.test.ts
chenbhao's picture
feat: desktop
1f21206
Raw
History Blame Contribute Delete
9.9 kB
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import * as fs from 'node:fs/promises'
import * as os from 'node:os'
import * as path from 'node:path'
import {
findPrivateLanAddress,
H5AccessService,
resolveEffectiveH5PublicBaseUrl,
} from '../services/h5AccessService.js'
import { ProviderService } from '../services/providerService.js'
let tmpDir: string
let originalConfigDir: string | undefined
let originalH5PublicBaseUrl: string | undefined
let originalH5AutoPublicUrl: string | undefined
function getManagedSettingsPath(): string {
return path.join(tmpDir, 'cc-haha', 'settings.json')
}
beforeEach(async () => {
tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'h5-access-service-test-'))
originalConfigDir = process.env.CLAUDE_CONFIG_DIR
originalH5PublicBaseUrl = process.env.CLAUDE_H5_PUBLIC_BASE_URL
originalH5AutoPublicUrl = process.env.CLAUDE_H5_AUTO_PUBLIC_URL
process.env.CLAUDE_CONFIG_DIR = tmpDir
})
afterEach(async () => {
if (originalConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR
else process.env.CLAUDE_CONFIG_DIR = originalConfigDir
if (originalH5PublicBaseUrl === undefined) delete process.env.CLAUDE_H5_PUBLIC_BASE_URL
else process.env.CLAUDE_H5_PUBLIC_BASE_URL = originalH5PublicBaseUrl
if (originalH5AutoPublicUrl === undefined) delete process.env.CLAUDE_H5_AUTO_PUBLIC_URL
else process.env.CLAUDE_H5_AUTO_PUBLIC_URL = originalH5AutoPublicUrl
await fs.rm(tmpDir, { recursive: true, force: true })
})
describe('H5AccessService', () => {
test('defaults to disabled state with sanitized settings', async () => {
const service = new H5AccessService()
await expect(service.getSettings()).resolves.toEqual({
enabled: false,
tokenPreview: null,
allowedOrigins: [],
publicBaseUrl: null,
})
await expect(service.validateToken('missing-token')).resolves.toBe(false)
})
test('enable generates a token and persists only hash plus preview', async () => {
const service = new H5AccessService()
const result = await service.enable()
const raw = await fs.readFile(getManagedSettingsPath(), 'utf-8')
const saved = JSON.parse(raw) as {
h5Access: {
enabled: boolean
tokenHash: string
tokenPreview: string
}
}
expect(result.token).toMatch(/^h5_[A-Za-z0-9_-]{43}$/)
expect(result.settings).toEqual({
enabled: true,
tokenPreview: saved.h5Access.tokenPreview,
allowedOrigins: [],
publicBaseUrl: null,
})
expect(saved.h5Access.enabled).toBe(true)
expect(saved.h5Access.tokenHash).toHaveLength(64)
expect(saved.h5Access.tokenPreview).toBe(
`${result.token.slice(0, 7)}...${result.token.slice(-4)}`,
)
expect(raw).not.toContain(result.token)
expect(await service.validateToken(result.token)).toBe(true)
})
test('enabled public settings use the packaged app LAN URL when provided', async () => {
process.env.CLAUDE_H5_PUBLIC_BASE_URL = 'http://192.168.1.20:28670/'
process.env.CLAUDE_H5_AUTO_PUBLIC_URL = '1'
const service = new H5AccessService()
const result = await service.enable()
expect(result.settings.publicBaseUrl).toBe('http://192.168.1.20:28670')
})
test('configured public URL overrides stale stored local URLs', async () => {
const service = new H5AccessService()
await service.updateSettings({
publicBaseUrl: 'http://192.168.0.102:5179',
})
process.env.CLAUDE_H5_PUBLIC_BASE_URL = 'https://chat.example.com/app/'
const result = await service.enable()
expect(result.settings.publicBaseUrl).toBe('https://chat.example.com/app')
})
test('auto LAN mode fills blank or loopback URLs but preserves manual LAN URLs', () => {
expect(resolveEffectiveH5PublicBaseUrl({
enabled: true,
storedPublicBaseUrl: null,
configuredPublicBaseUrl: null,
autoPublicBaseUrl: 'http://192.168.0.102:39876',
})).toBe('http://192.168.0.102:39876')
expect(resolveEffectiveH5PublicBaseUrl({
enabled: true,
storedPublicBaseUrl: 'http://127.0.0.1:5179',
configuredPublicBaseUrl: null,
autoPublicBaseUrl: 'http://192.168.0.102:39876',
})).toBe('http://192.168.0.102:39876')
expect(resolveEffectiveH5PublicBaseUrl({
enabled: true,
storedPublicBaseUrl: 'http://192.168.1.100:54064',
configuredPublicBaseUrl: null,
autoPublicBaseUrl: 'http://172.20.16.1:39876',
})).toBe('http://192.168.1.100:54064')
expect(resolveEffectiveH5PublicBaseUrl({
enabled: true,
storedPublicBaseUrl: 'https://chat.example.com/app',
configuredPublicBaseUrl: null,
autoPublicBaseUrl: 'http://192.168.0.102:39876',
})).toBe('https://chat.example.com/app')
})
test('auto LAN detection prefers physical adapters over WSL and Docker virtual adapters', () => {
expect(findPrivateLanAddress({
'vEthernet (WSL)': [{
address: '172.20.16.1',
netmask: '255.255.240.0',
family: 'IPv4',
mac: '00:15:5d:00:00:01',
internal: false,
cidr: '172.20.16.1/20',
}],
'Docker Desktop': [{
address: '172.17.0.1',
netmask: '255.255.0.0',
family: 'IPv4',
mac: '02:42:ac:11:00:01',
internal: false,
cidr: '172.17.0.1/16',
}],
'Wi-Fi': [{
address: '192.168.1.100',
netmask: '255.255.255.0',
family: 'IPv4',
mac: 'aa:bb:cc:dd:ee:ff',
internal: false,
cidr: '192.168.1.100/24',
}],
})).toBe('192.168.1.100')
})
test('regenerateToken invalidates the previous token', async () => {
const service = new H5AccessService()
const first = await service.enable()
const second = await service.regenerateToken()
expect(second.token).toMatch(/^h5_/)
expect(second.token).not.toBe(first.token)
expect(await service.validateToken(first.token)).toBe(false)
expect(await service.validateToken(second.token)).toBe(true)
})
test('preserves unknown managed settings fields when updating h5Access', async () => {
await fs.mkdir(path.dirname(getManagedSettingsPath()), { recursive: true })
await fs.writeFile(
getManagedSettingsPath(),
JSON.stringify(
{
env: {
ANTHROPIC_MODEL: 'keep-me',
},
futureField: {
keep: true,
},
},
null,
2,
),
'utf-8',
)
const service = new H5AccessService()
await service.enable()
const saved = JSON.parse(await fs.readFile(getManagedSettingsPath(), 'utf-8')) as {
env: {
ANTHROPIC_MODEL: string
}
futureField: {
keep: boolean
}
h5Access: unknown
}
expect(saved.env.ANTHROPIC_MODEL).toBe('keep-me')
expect(saved.futureField).toEqual({ keep: true })
expect(saved.h5Access).toBeDefined()
})
test('updateSettings normalizes origins and rejects invalid ones', async () => {
const service = new H5AccessService()
await expect(
service.updateSettings({
allowedOrigins: ['https://example.com/path', 'http://localhost:3000/foo'],
publicBaseUrl: 'https://public.example.com/app/',
}),
).resolves.toEqual({
enabled: false,
tokenPreview: null,
allowedOrigins: ['https://example.com', 'http://localhost:3000'],
publicBaseUrl: 'https://public.example.com/app',
})
await expect(
service.updateSettings({
allowedOrigins: ['https://*.example.com'],
}),
).rejects.toMatchObject({
statusCode: 400,
})
})
test('isOriginAllowed requires enabled state and matches normalized origins', async () => {
const service = new H5AccessService()
await service.updateSettings({
allowedOrigins: ['https://example.com/path'],
})
await expect(service.isOriginAllowed('https://example.com')).resolves.toBe(false)
await service.enable()
await expect(service.isOriginAllowed('https://example.com')).resolves.toBe(true)
await expect(service.isOriginAllowed('https://other.example.com')).resolves.toBe(false)
await expect(service.isOriginAllowed('notaurl')).resolves.toBe(false)
})
test('malformed persisted enabled state without token hash is treated as disabled', async () => {
await fs.mkdir(path.dirname(getManagedSettingsPath()), { recursive: true })
await fs.writeFile(
getManagedSettingsPath(),
JSON.stringify({
h5Access: {
enabled: true,
allowedOrigins: ['https://example.com/path'],
publicBaseUrl: 'https://public.example.com',
},
}),
'utf-8',
)
const service = new H5AccessService()
await expect(service.getSettings()).resolves.toEqual({
enabled: false,
tokenPreview: null,
allowedOrigins: ['https://example.com'],
publicBaseUrl: 'https://public.example.com',
})
await expect(service.validateToken('anything')).resolves.toBe(false)
await expect(service.isOriginAllowed('https://example.com')).resolves.toBe(false)
})
test('concurrent h5 enable and provider managed settings update preserve both fields', async () => {
const h5Service = new H5AccessService()
const providerService = new ProviderService()
await Promise.all([
h5Service.enable(),
providerService.updateManagedSettings({
env: {
ANTHROPIC_MODEL: 'keep-me',
},
}),
])
const saved = JSON.parse(await fs.readFile(getManagedSettingsPath(), 'utf-8')) as {
env?: {
ANTHROPIC_MODEL?: string
}
h5Access?: {
enabled?: boolean
tokenHash?: string | null
}
}
expect(saved.env?.ANTHROPIC_MODEL).toBe('keep-me')
expect(saved.h5Access?.enabled).toBe(true)
expect(saved.h5Access?.tokenHash).toEqual(expect.any(String))
})
})