mirror of
https://github.com/hanzoai/extension.git
synced 2026-11-02 00:29:00 +00:00
Office, Outlook, PDF, Clio, Meetings, Google Workspace and Salesforce defaulted to zen-1, Teams offered zen-eco-3b, zen-mini-8b and zen-4b, and JetBrains defaulted to claude-3-5-sonnet-20241022 with a fallback list of retired upstream ids. None of those are in api.hanzo.ai/v1/models, so a first request on each of them named a model the gateway does not serve. zen5 is the id the catalog itself marks as the default: catalog.hanzo.ai /v1/pricing lists it as "Zen5 (Default)", "the everyday Zen5 chat model", and nineteen other surfaces here already defaulted to it. Teams now offers the catalog's Zen 5 family with zen5 first. JetBrains preselects zen5 when the live list carries it, and offers only zen5 when it cannot load the list, rather than a fabricated one. Claude-Session: https://claude.ai/code/session_01Dc8DXc3fEQCRHUa6XWK4Ku
103 lines
3.9 KiB
TypeScript
103 lines
3.9 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
pickBearer,
|
|
chatCompletionsURL,
|
|
modelsURL,
|
|
readServerConfig,
|
|
DEFAULT_MODEL,
|
|
} from '../src/config';
|
|
import { authorizeUrl, tokenExchange, parseTokenResponse } from '../src/zoom/oauth';
|
|
|
|
describe('pickBearer', () => {
|
|
it('prefers a pasted API key over an OAuth token', () => {
|
|
expect(pickBearer('sk-key', 'oauth-tok')).toBe('sk-key');
|
|
});
|
|
it('falls back to the OAuth token, then empty', () => {
|
|
expect(pickBearer('', 'oauth-tok')).toBe('oauth-tok');
|
|
expect(pickBearer('', '')).toBe('');
|
|
});
|
|
it('ignores whitespace-only credentials', () => {
|
|
expect(pickBearer(' ', 'oauth-tok')).toBe('oauth-tok');
|
|
});
|
|
});
|
|
|
|
describe('gateway URLs (/v1, never /api/)', () => {
|
|
it('points at api.hanzo.ai/v1', () => {
|
|
expect(chatCompletionsURL()).toBe('https://api.hanzo.ai/v1/chat/completions');
|
|
expect(modelsURL()).toBe('https://api.hanzo.ai/v1/models');
|
|
expect(chatCompletionsURL()).not.toContain('/api/');
|
|
});
|
|
it('has a zen default model', () => {
|
|
expect(DEFAULT_MODEL).toBe('zen5');
|
|
});
|
|
});
|
|
|
|
describe('readServerConfig', () => {
|
|
const full = {
|
|
ZOOM_CLIENT_ID: 'cid',
|
|
ZOOM_CLIENT_SECRET: 'csecret',
|
|
ZOOM_REDIRECT_URI: 'https://meetings.hanzo.ai/zoom/oauth/callback',
|
|
ZOOM_WEBHOOK_SECRET_TOKEN: 'whsec',
|
|
};
|
|
|
|
it('reads a complete config with the optional Hanzo key and a default port', () => {
|
|
const cfg = readServerConfig({ ...full, HANZO_API_KEY: 'sk-1' });
|
|
expect(cfg.zoomClientId).toBe('cid');
|
|
expect(cfg.zoomWebhookSecret).toBe('whsec');
|
|
expect(cfg.hanzoApiKey).toBe('sk-1');
|
|
expect(cfg.port).toBe(8788);
|
|
});
|
|
|
|
it('allows a missing HANZO_API_KEY (verify+route without summarizing)', () => {
|
|
const cfg = readServerConfig(full);
|
|
expect(cfg.hanzoApiKey).toBe('');
|
|
});
|
|
|
|
it('honors PORT', () => {
|
|
expect(readServerConfig({ ...full, PORT: '9000' }).port).toBe(9000);
|
|
});
|
|
|
|
it('throws naming every missing required secret', () => {
|
|
expect(() => readServerConfig({})).toThrow(/ZOOM_CLIENT_ID/);
|
|
expect(() => readServerConfig({ ZOOM_CLIENT_ID: 'x' })).toThrow(/ZOOM_CLIENT_SECRET/);
|
|
expect(() => readServerConfig({ ...full, ZOOM_WEBHOOK_SECRET_TOKEN: undefined })).toThrow(
|
|
/ZOOM_WEBHOOK_SECRET_TOKEN/,
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('Zoom OAuth request shaping', () => {
|
|
it('builds the authorize URL with state', () => {
|
|
const url = new URL(authorizeUrl('cid', 'https://h/cb', 'st8'));
|
|
expect(url.origin + url.pathname).toBe('https://zoom.us/oauth/authorize');
|
|
expect(url.searchParams.get('response_type')).toBe('code');
|
|
expect(url.searchParams.get('client_id')).toBe('cid');
|
|
expect(url.searchParams.get('redirect_uri')).toBe('https://h/cb');
|
|
expect(url.searchParams.get('state')).toBe('st8');
|
|
});
|
|
|
|
it('puts the secret in the Basic header, not the body', () => {
|
|
const req = tokenExchange({ clientId: 'cid', clientSecret: 'sec', redirectUri: 'https://h/cb', code: 'abc' });
|
|
expect(req.url).toBe('https://zoom.us/oauth/token');
|
|
expect(req.headers.Authorization).toBe('Basic ' + Buffer.from('cid:sec').toString('base64'));
|
|
expect(req.headers['Content-Type']).toBe('application/x-www-form-urlencoded');
|
|
expect(req.body).not.toContain('sec'); // secret is NOT in the body
|
|
const params = new URLSearchParams(req.body);
|
|
expect(params.get('grant_type')).toBe('authorization_code');
|
|
expect(params.get('code')).toBe('abc');
|
|
expect(params.get('redirect_uri')).toBe('https://h/cb');
|
|
});
|
|
|
|
it('parses a token response and throws on an error body', () => {
|
|
expect(parseTokenResponse({ access_token: 'at', refresh_token: 'rt', expires_in: 3600 })).toEqual({
|
|
access_token: 'at',
|
|
refresh_token: 'rt',
|
|
expires_in: 3600,
|
|
scope: undefined,
|
|
token_type: undefined,
|
|
});
|
|
expect(() => parseTokenResponse({ error: 'invalid_grant', reason: 'bad code' })).toThrow(/bad code/);
|
|
expect(() => parseTokenResponse({})).toThrow(/missing access_token/);
|
|
});
|
|
});
|