mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-09 06:06:08 +00:00
9b91f0f42e
Fold the standalone 3x-ui-docs project (Next.js 16 + Fumadocs, deployed to docs.sanaei.dev) into docs/ so the panel and its documentation share a single source of truth, the way sing-box keeps its docs in-tree. The old repo becomes redundant and can be retired. - Import the full site under docs/ (app, components, content, lib, public, scripts, config). The self-contained pnpm project sits alongside the existing engineering notes with no filename collisions. - Re-point "Edit on GitHub" links from MHSanaei/3x-ui-docs to this repo's docs/content/docs path (docs/lib/shared.ts, docs/app/.../page.tsx). - Add docs-ci.yml and docs-deploy.yml under .github/workflows/, scoped to docs/** and run with working-directory: docs, since GitHub only runs workflows from the repo-root .github/. deploy-static.yml's GitHub Pages publish (CNAME docs.sanaei.dev) carries over unchanged. Follow-up (outside this commit): attach the docs.sanaei.dev custom domain to this repository's Pages (or set the Vercel project's root directory to docs), confirm the site is live from the monorepo, then delete MHSanaei/3x-ui-docs.
138 lines
4.2 KiB
TypeScript
138 lines
4.2 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
parseLink,
|
|
detectProtocol,
|
|
buildVless,
|
|
buildVmess,
|
|
buildTrojan,
|
|
buildShadowsocks,
|
|
} from './links';
|
|
|
|
describe('detectProtocol', () => {
|
|
it('recognises supported schemes', () => {
|
|
expect(detectProtocol('vless://x')).toBe('vless');
|
|
expect(detectProtocol('vmess://x')).toBe('vmess');
|
|
expect(detectProtocol('trojan://x')).toBe('trojan');
|
|
expect(detectProtocol('ss://x')).toBe('ss');
|
|
expect(detectProtocol('https://x')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('parseLink — vless', () => {
|
|
const link =
|
|
'vless://11111111-2222-3333-4444-555555555555@example.com:443?security=reality&pbk=abc&sid=ff&sni=www.microsoft.com&fp=chrome&flow=xtls-rprx-vision&type=tcp#My%20Node';
|
|
|
|
it('extracts address, port, credential, name, and params', () => {
|
|
const r = parseLink(link);
|
|
expect(r.protocol).toBe('vless');
|
|
expect(r.address).toBe('example.com');
|
|
expect(r.port).toBe(443);
|
|
expect(r.credential).toBe('11111111-2222-3333-4444-555555555555');
|
|
expect(r.name).toBe('My Node');
|
|
expect(r.params.security).toBe('reality');
|
|
expect(r.params.pbk).toBe('abc');
|
|
expect(r.params.flow).toBe('xtls-rprx-vision');
|
|
});
|
|
});
|
|
|
|
describe('parseLink — trojan', () => {
|
|
it('parses password and query params', () => {
|
|
const r = parseLink('trojan://p%40ss@1.2.3.4:8443?sni=a.com&type=ws&path=%2Fws#t');
|
|
expect(r.protocol).toBe('trojan');
|
|
expect(r.credential).toBe('p@ss');
|
|
expect(r.address).toBe('1.2.3.4');
|
|
expect(r.port).toBe(8443);
|
|
expect(r.params.path).toBe('/ws');
|
|
expect(r.name).toBe('t');
|
|
});
|
|
});
|
|
|
|
describe('parseLink — vmess', () => {
|
|
it('decodes the base64 JSON payload', () => {
|
|
const link = buildVmess({
|
|
ps: 'tehran',
|
|
add: 'host.example',
|
|
port: 443,
|
|
id: 'uuid-123',
|
|
net: 'ws',
|
|
tls: 'tls',
|
|
host: 'cdn.example',
|
|
path: '/v2',
|
|
});
|
|
const r = parseLink(link);
|
|
expect(r.protocol).toBe('vmess');
|
|
expect(r.name).toBe('tehran');
|
|
expect(r.address).toBe('host.example');
|
|
expect(r.port).toBe(443);
|
|
expect(r.credential).toBe('uuid-123');
|
|
expect(r.params.net).toBe('ws');
|
|
expect(r.params.path).toBe('/v2');
|
|
});
|
|
});
|
|
|
|
describe('parseLink — shadowsocks', () => {
|
|
it('parses SIP002 with base64 userinfo', () => {
|
|
const link = buildShadowsocks({
|
|
method: 'aes-256-gcm',
|
|
password: 's3cret',
|
|
address: '192.0.2.1',
|
|
port: 8388,
|
|
name: 'ss-node',
|
|
});
|
|
const r = parseLink(link);
|
|
expect(r.protocol).toBe('ss');
|
|
expect(r.credential).toBe('aes-256-gcm:s3cret');
|
|
expect(r.address).toBe('192.0.2.1');
|
|
expect(r.port).toBe(8388);
|
|
expect(r.name).toBe('ss-node');
|
|
});
|
|
|
|
it('parses the legacy fully-base64 form', () => {
|
|
// base64("aes-128-gcm:pw@example.com:8388")
|
|
const legacy =
|
|
'ss://' + Buffer.from('aes-128-gcm:pw@example.com:8388').toString('base64') + '#legacy';
|
|
const r = parseLink(legacy);
|
|
expect(r.credential).toBe('aes-128-gcm:pw');
|
|
expect(r.address).toBe('example.com');
|
|
expect(r.port).toBe(8388);
|
|
expect(r.name).toBe('legacy');
|
|
});
|
|
});
|
|
|
|
describe('build → parse round-trips', () => {
|
|
it('vless round-trips', () => {
|
|
const link = buildVless({
|
|
credential: 'uuid-abc',
|
|
address: 'example.com',
|
|
port: 443,
|
|
params: { security: 'reality', sni: 'a.com', flow: 'xtls-rprx-vision' },
|
|
name: 'node 1',
|
|
});
|
|
const r = parseLink(link);
|
|
expect(r.credential).toBe('uuid-abc');
|
|
expect(r.address).toBe('example.com');
|
|
expect(r.port).toBe(443);
|
|
expect(r.params.sni).toBe('a.com');
|
|
expect(r.name).toBe('node 1');
|
|
});
|
|
|
|
it('trojan round-trips', () => {
|
|
const r = parseLink(buildTrojan({ credential: 'pw', address: 'h.com', port: 443, name: 'x' }));
|
|
expect(r.credential).toBe('pw');
|
|
expect(r.port).toBe(443);
|
|
});
|
|
|
|
it('ss round-trips', () => {
|
|
const r = parseLink(
|
|
buildShadowsocks({ method: 'chacha20-poly1305', password: 'p', address: 'h', port: 8388 }),
|
|
);
|
|
expect(r.credential).toBe('chacha20-poly1305:p');
|
|
});
|
|
});
|
|
|
|
describe('parseLink — errors', () => {
|
|
it('throws on unsupported scheme', () => {
|
|
expect(() => parseLink('http://example.com')).toThrow(/Unsupported/);
|
|
});
|
|
});
|