mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-11-12 21:44:22 +08:00
feat(domain): 支持通配符域名及关联功能适配 (#63)
* feat(domain): 支持通配符域名及关联功能适配 * fix(domain): preserve legacy Unicode imports and address review findings * fix(domain): canonicalize legacy IDNs and exclude empty host labels * fix(domain): normalize equivalent Unicode IDN spellings --------- Co-authored-by: Ryan <63696351+Rain-kl@users.noreply.github.com> Co-authored-by: ryan <ryan@arctel.net>
This commit is contained in:
@@ -114,7 +114,7 @@ export function QuickCreateZoneDomainDialog({
|
||||
|
||||
const schema = z.object({
|
||||
zone_id: z.string().min(1, t('selectZone')),
|
||||
domain_input: z.string().trim().min(1, t('enterDomain')),
|
||||
domain_input: z.string().min(1, t('enterDomain')),
|
||||
cert_id: z.string(),
|
||||
});
|
||||
const form = useForm<Values>({
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* - `@` → apex (zone root itself), e.g. `example.com`
|
||||
* - single label `name` → `name.example.com`
|
||||
* - `*` → `*.example.com`; other wildcards require a full FQDN
|
||||
* - full FQDN must equal the root or end with `.root`
|
||||
*/
|
||||
export type ZoneDomainInputError =
|
||||
@@ -13,6 +14,15 @@ export type ZoneDomainInputError =
|
||||
| 'invalidSubdomain'
|
||||
| 'mustBelongToZone';
|
||||
|
||||
const dnsLabelPattern = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
||||
|
||||
function isDnsName(value: string): boolean {
|
||||
return (
|
||||
value.length <= 253 &&
|
||||
value.split('.').every((label) => dnsLabelPattern.test(label))
|
||||
);
|
||||
}
|
||||
|
||||
export function resolveZoneDomainInput(
|
||||
rawInput: string,
|
||||
zoneRoot: string,
|
||||
@@ -26,15 +36,7 @@ export function resolveZoneDomainInput(
|
||||
if (!input) {
|
||||
return { domain: '', error: 'enterDomain' };
|
||||
}
|
||||
if (input.includes('*')) {
|
||||
return { domain: '', error: 'noWildcard' };
|
||||
}
|
||||
if (
|
||||
input.includes('://') ||
|
||||
input.includes('/') ||
|
||||
input.includes('?') ||
|
||||
input.includes('#')
|
||||
) {
|
||||
if (/\s/.test(input) || !isDnsName(root)) {
|
||||
return { domain: '', error: 'invalidFormat' };
|
||||
}
|
||||
|
||||
@@ -42,17 +44,28 @@ export function resolveZoneDomainInput(
|
||||
return { domain: root };
|
||||
}
|
||||
|
||||
// Short label: no dots → prefix under zone root
|
||||
if (!input.includes('.')) {
|
||||
if (!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(input)) {
|
||||
return { domain: '', error: 'invalidSubdomain' };
|
||||
}
|
||||
return { domain: `${input}.${root}` };
|
||||
// 星号只能作为最左侧完整标签;*.s3 不推断为 Zone 内的相对名称。
|
||||
const wildcard = input === '*' || input.startsWith('*.');
|
||||
if (input.includes('*') && (!wildcard || input.slice(1).includes('*'))) {
|
||||
return { domain: '', error: 'noWildcard' };
|
||||
}
|
||||
|
||||
// Full FQDN
|
||||
if (input === root || input.endsWith(`.${root}`)) {
|
||||
return { domain: input };
|
||||
let domain = input;
|
||||
if (input === '*') {
|
||||
domain = `*.${root}`;
|
||||
} else if (!input.includes('.')) {
|
||||
if (!dnsLabelPattern.test(input)) {
|
||||
return { domain: '', error: 'invalidSubdomain' };
|
||||
}
|
||||
domain = `${input}.${root}`;
|
||||
}
|
||||
|
||||
const hostname = wildcard ? domain.slice(2) : domain;
|
||||
if (domain.length > 253 || !isDnsName(hostname)) {
|
||||
return { domain: '', error: 'invalidFormat' };
|
||||
}
|
||||
if (hostname === root || hostname.endsWith(`.${root}`)) {
|
||||
return { domain };
|
||||
}
|
||||
|
||||
return { domain: '', error: 'mustBelongToZone' };
|
||||
|
||||
@@ -79,21 +79,21 @@
|
||||
"expiresAt": "Expires",
|
||||
"selectZone": "Select a Zone first",
|
||||
"enterDomain": "Enter a domain",
|
||||
"noWildcard": "Zone domains do not support wildcards",
|
||||
"noWildcard": "A wildcard must be a single * in the leftmost label, such as *.example.com",
|
||||
"invalidFormat": "Invalid domain format",
|
||||
"invalidSubdomain": "Invalid subdomain name format",
|
||||
"mustBelongToZone": "The domain must belong to Zone {root}",
|
||||
"domainAdded": "Domain added",
|
||||
"addFailed": "Add failed",
|
||||
"quickCreateTitle": "Quick add domain",
|
||||
"quickCreateDesc": "After selecting a Zone, enter a subdomain name (such as api), @ (the root), or a full FQDN.",
|
||||
"quickCreateDesc": "Enter a subdomain (api), @ (the root), * (wildcard under the root), or a full FQDN. For nested wildcards, enter the full name, such as *.s3.example.com; *.s3 shorthand is not supported.",
|
||||
"selectRegisteredRoot": "Select registered root domain",
|
||||
"zoneLabel": "Zone:",
|
||||
"domain": "Domain",
|
||||
"domainPlaceholderWithZone": "api or @ or api.{domain}",
|
||||
"domainPlaceholder": "api / @ / full domain",
|
||||
"domainPlaceholderWithZone": "api / @ / * / *.s3.{domain}",
|
||||
"domainPlaceholder": "api / @ / * / full domain",
|
||||
"willCreate": "Will create:",
|
||||
"domainExample": "Example: enter <api>api</api> → <fqdn>api.zone.com</fqdn>; <apex>@</apex> → the root itself",
|
||||
"domainExample": "Example: <api>api</api> → <fqdn>api.zone.com</fqdn>; <apex>@</apex> → the root; * → *.zone.com. A wildcard cannot be opened directly; visit a concrete matching hostname.",
|
||||
"certOptional": "Certificate (optional)",
|
||||
"noCert": "No certificate"
|
||||
},
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
"expiresAt": "Срок действия истекает",
|
||||
"selectZone": "Сначала выберите зону",
|
||||
"enterDomain": "Введите домен",
|
||||
"noWildcard": "Домены зон не поддерживают подстановочные знаки",
|
||||
"noWildcard": "Допустим только один символ * как крайняя левая метка, например *.example.com",
|
||||
"invalidFormat": "Неверный формат домена",
|
||||
"invalidSubdomain": "Неверный формат имени субдомена",
|
||||
"mustBelongToZone": "Домен должен принадлежать зоне {root}",
|
||||
|
||||
@@ -79,21 +79,21 @@
|
||||
"expiresAt": "到期时间",
|
||||
"selectZone": "请先选择 Zone",
|
||||
"enterDomain": "请输入域名",
|
||||
"noWildcard": "Zone 域名不支持通配符",
|
||||
"noWildcard": "通配符只能是最左侧的完整标签 *,例如 *.example.com,且只能出现一次",
|
||||
"invalidFormat": "域名格式不合法",
|
||||
"invalidSubdomain": "子域名称格式不合法",
|
||||
"mustBelongToZone": "域名必须属于 Zone {root}",
|
||||
"domainAdded": "域名已添加",
|
||||
"addFailed": "添加失败",
|
||||
"quickCreateTitle": "快捷新增域名",
|
||||
"quickCreateDesc": "选择 Zone 后输入:子域名称(如 api)、@(根域)或完整 FQDN。",
|
||||
"quickCreateDesc": "输入子域名称(api)、@(根域)、*(根域下泛域名)或完整 FQDN。多级泛域名请填写完整名称,例如 *.s3.example.com,不支持 *.s3 简写。",
|
||||
"selectRegisteredRoot": "选择注册根域",
|
||||
"zoneLabel": "Zone:",
|
||||
"domain": "域名",
|
||||
"domainPlaceholderWithZone": "api 或 @ 或 api.{domain}",
|
||||
"domainPlaceholder": "api / @ / 完整域名",
|
||||
"domainPlaceholderWithZone": "api / @ / * / *.s3.{domain}",
|
||||
"domainPlaceholder": "api / @ / * / 完整域名",
|
||||
"willCreate": "将创建:",
|
||||
"domainExample": "示例:输入 <api>api</api> → <fqdn>api.zone.com</fqdn>;<apex>@</apex> → 根域本身",
|
||||
"domainExample": "示例:<api>api</api> → <fqdn>api.zone.com</fqdn>;<apex>@</apex> → 根域;* → *.zone.com。泛域名不能直接打开,请使用匹配的具体主机名访问。",
|
||||
"certOptional": "证书(可选)",
|
||||
"noCert": "不绑定证书"
|
||||
},
|
||||
|
||||
@@ -150,4 +150,38 @@ describe('MemberAddDialog advanced domain picker', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: '添加并同步' }));
|
||||
expect(onSubmit).toHaveBeenCalledWith([2], false);
|
||||
});
|
||||
|
||||
it('searches and selects a wildcard domain without treating it as a URL', () => {
|
||||
const onSubmit = vi.fn();
|
||||
render(
|
||||
<NextIntlClientProvider
|
||||
locale='zh-CN'
|
||||
messages={zhCN}
|
||||
timeZone='Asia/Shanghai'
|
||||
>
|
||||
<MemberAddDialog
|
||||
open
|
||||
onOpenChange={vi.fn()}
|
||||
domains={[
|
||||
...domains,
|
||||
{ ...domains[0], id: 5, domain: '*.s3.example.com' },
|
||||
]}
|
||||
defaultProxied={false}
|
||||
pending={false}
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</NextIntlClientProvider>,
|
||||
);
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('搜索域名或顶级域…'), {
|
||||
target: { value: '*.s3' },
|
||||
});
|
||||
expect(screen.queryByText('www.example.com')).not.toBeInTheDocument();
|
||||
const label = screen.getByText('*.s3.example.com').closest('label');
|
||||
expect(label).toBeTruthy();
|
||||
fireEvent.click(within(label as HTMLElement).getByRole('checkbox'));
|
||||
fireEvent.click(screen.getByRole('button', { name: '添加并同步' }));
|
||||
expect(onSubmit).toHaveBeenCalledWith([5], false);
|
||||
expect(screen.queryByRole('link')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,34 +1,118 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { resolveZoneDomainInput } from '@/app/(main)/websites/components/resolve-zone-domain-input';
|
||||
import {
|
||||
previewZoneDomainInput,
|
||||
resolveZoneDomainInput,
|
||||
} from '@/app/(main)/websites/components/resolve-zone-domain-input';
|
||||
|
||||
const root = 'example.com';
|
||||
|
||||
describe('resolveZoneDomainInput', () => {
|
||||
it('maps short label and apex @ under zone root', () => {
|
||||
expect(resolveZoneDomainInput('api', 'example.com')).toEqual({
|
||||
domain: 'api.example.com',
|
||||
});
|
||||
expect(resolveZoneDomainInput('@', 'example.com')).toEqual({
|
||||
domain: 'example.com',
|
||||
it.each([
|
||||
['api', 'api.example.com'],
|
||||
['@', root],
|
||||
[root, root],
|
||||
['www.api.example.com', 'www.api.example.com'],
|
||||
['API.Example.COM', 'api.example.com'],
|
||||
['*', '*.example.com'],
|
||||
['*.example.com', '*.example.com'],
|
||||
['*.s3.example.com', '*.s3.example.com'],
|
||||
['*.S3.Example.COM', '*.s3.example.com'],
|
||||
[' *.Example.COM ', '*.example.com'],
|
||||
[' api ', 'api.example.com'],
|
||||
['*.example.com\n', '*.example.com'],
|
||||
['\t*.example.com', '*.example.com'],
|
||||
[`${'a'.repeat(63)}.example.com`, `${'a'.repeat(63)}.example.com`],
|
||||
])('resolves %s to %s', (input, domain) => {
|
||||
expect(resolveZoneDomainInput(input, root)).toEqual({ domain });
|
||||
expect(previewZoneDomainInput(input, root)).toBe(domain);
|
||||
});
|
||||
|
||||
it.each([
|
||||
'evil.com',
|
||||
'*.evil.com',
|
||||
'*.notexample.com',
|
||||
'*.example.com.evil.com',
|
||||
'*.s3',
|
||||
'api.s3',
|
||||
])(
|
||||
'rejects %s outside the zone without expanding dotted shorthand',
|
||||
(input) => {
|
||||
expect(resolveZoneDomainInput(input, root)).toEqual({
|
||||
domain: '',
|
||||
error: 'mustBelongToZone',
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
'foo*',
|
||||
'foo*.example.com',
|
||||
'a.*',
|
||||
'a.*.example.com',
|
||||
'**',
|
||||
'**.example.com',
|
||||
'*.*.example.com',
|
||||
'*.s*.example.com',
|
||||
])('rejects wildcard outside the single leftmost label: %s', (input) => {
|
||||
expect(resolveZoneDomainInput(input, root)).toEqual({
|
||||
domain: '',
|
||||
error: 'noWildcard',
|
||||
});
|
||||
});
|
||||
|
||||
it('accepts full FQDN under the zone', () => {
|
||||
expect(
|
||||
resolveZoneDomainInput('www.api.example.com', 'example.com'),
|
||||
).toEqual({
|
||||
domain: 'www.api.example.com',
|
||||
});
|
||||
expect(resolveZoneDomainInput('example.com', 'example.com')).toEqual({
|
||||
domain: 'example.com',
|
||||
});
|
||||
it.each([
|
||||
'-api',
|
||||
'api-',
|
||||
'_api',
|
||||
'-api.example.com',
|
||||
'api-.example.com',
|
||||
'_api.example.com',
|
||||
'*.bad_.example.com',
|
||||
'*.-bad.example.com',
|
||||
'*.bad-.example.com',
|
||||
'..example.com',
|
||||
'*.example..com',
|
||||
'*.example.com.',
|
||||
'*.example.com:443',
|
||||
'*.example.com;other',
|
||||
'*.example.com/path',
|
||||
'*.example.com?query',
|
||||
'*.example.com#fragment',
|
||||
'https://*.example.com',
|
||||
'https://api.example.com',
|
||||
'user@api.example.com',
|
||||
'api.example.com:80',
|
||||
'api.example.com;other',
|
||||
'a b.example.com',
|
||||
'*.s3 .example.com',
|
||||
'*.exa\nmple.com',
|
||||
'*.s3\t.example.com',
|
||||
`${'a'.repeat(64)}.example.com`,
|
||||
`*.${'a'.repeat(64)}.example.com`,
|
||||
`${`${'a'.repeat(63)}.`.repeat(4)}example.com`,
|
||||
])('rejects malformed DNS names and unsafe input: %s', (input) => {
|
||||
expect(resolveZoneDomainInput(input, root).error).toBeTruthy();
|
||||
expect(previewZoneDomainInput(input, root)).toBe('');
|
||||
});
|
||||
|
||||
it('rejects foreign domains and wildcards', () => {
|
||||
expect(
|
||||
resolveZoneDomainInput('evil.com', 'example.com').error,
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
resolveZoneDomainInput('*.example.com', 'example.com').error,
|
||||
).toBeTruthy();
|
||||
it('checks total length after expanding shorthand', () => {
|
||||
const longRoot = `${'a'.repeat(63)}.${'b'.repeat(63)}.${'c'.repeat(63)}.${'d'.repeat(61)}`;
|
||||
expect(longRoot).toHaveLength(253);
|
||||
expect(resolveZoneDomainInput('@', longRoot)).toEqual({ domain: longRoot });
|
||||
expect(resolveZoneDomainInput('*', longRoot).error).toBe('invalidFormat');
|
||||
expect(resolveZoneDomainInput('api', longRoot).error).toBe('invalidFormat');
|
||||
});
|
||||
|
||||
it('requires input and a selected, non-wildcard zone root', () => {
|
||||
expect(resolveZoneDomainInput('*', '').error).toBe('selectZone');
|
||||
expect(resolveZoneDomainInput('', root).error).toBe('enterDomain');
|
||||
expect(resolveZoneDomainInput(' ', root).error).toBe('enterDomain');
|
||||
expect(resolveZoneDomainInput('*', '*.example.com').error).toBe(
|
||||
'invalidFormat',
|
||||
);
|
||||
expect(resolveZoneDomainInput('*', 'Example.COM')).toEqual({
|
||||
domain: '*.example.com',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import type { ReactElement } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import { NextIntlClientProvider } from 'next-intl';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { QuickCreateZoneDomainDialog } from '@/app/(main)/websites/components/quick-create-zone-domain-dialog';
|
||||
import { ZoneDomainDialog } from '@/app/(main)/websites/[zoneId]/components/zone-domain-dialog';
|
||||
import { ZoneDomainsTable } from '@/app/(main)/websites/[zoneId]/components/zone-domains-table';
|
||||
import { ZoneEditorDialog } from '@/app/(main)/websites/[zoneId]/components/zone-editor-dialog';
|
||||
import {
|
||||
TlsCertificateService,
|
||||
ZoneDomainService,
|
||||
ZoneService,
|
||||
type ZoneDomainItem,
|
||||
} from '@/lib/services/openflare';
|
||||
import zhCN from '@/messages/zh-CN.json';
|
||||
|
||||
vi.mock('@/lib/services/openflare', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@/lib/services/openflare')>();
|
||||
return {
|
||||
...actual,
|
||||
TlsCertificateService: { list: vi.fn() },
|
||||
ZoneDomainService: { create: vi.fn() },
|
||||
ZoneService: { create: vi.fn(), update: vi.fn() },
|
||||
};
|
||||
});
|
||||
|
||||
const zone = { id: 1, domain: 'example.com', created_at: '', updated_at: '' };
|
||||
const wildcardDomain: ZoneDomainItem = {
|
||||
id: 7,
|
||||
zone_id: 1,
|
||||
domain: '*.example.com',
|
||||
cert_id: null,
|
||||
proxy_route_id: null,
|
||||
created_at: '',
|
||||
updated_at: '',
|
||||
};
|
||||
|
||||
function renderUI(ui: ReactElement) {
|
||||
const client = new QueryClient({
|
||||
defaultOptions: { queries: { retry: false, gcTime: 0 } },
|
||||
});
|
||||
return render(
|
||||
<NextIntlClientProvider
|
||||
locale='zh-CN'
|
||||
messages={zhCN}
|
||||
timeZone='Asia/Shanghai'
|
||||
>
|
||||
<QueryClientProvider client={client}>{ui}</QueryClientProvider>
|
||||
</NextIntlClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('wildcard domain entry points', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
vi.mocked(TlsCertificateService.list).mockResolvedValue([]);
|
||||
vi.mocked(ZoneDomainService.create).mockImplementation(
|
||||
async (_zoneId, payload) => ({
|
||||
...wildcardDomain,
|
||||
domain: payload.domain,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('quick-add previews and submits * as a wildcard under the selected zone', async () => {
|
||||
const onCreated = vi.fn();
|
||||
renderUI(
|
||||
<QuickCreateZoneDomainDialog
|
||||
open
|
||||
onOpenChange={vi.fn()}
|
||||
zones={[zone]}
|
||||
fixedZoneId={zone.id}
|
||||
onCreated={onCreated}
|
||||
/>,
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText('域名'), {
|
||||
target: { value: '*' },
|
||||
});
|
||||
expect(screen.getByText('*.example.com')).toBeVisible();
|
||||
fireEvent.submit(document.getElementById('quick-create-zone-domain')!);
|
||||
await waitFor(() => {
|
||||
expect(ZoneDomainService.create).toHaveBeenCalledWith(1, {
|
||||
domain: '*.example.com',
|
||||
cert_id: null,
|
||||
});
|
||||
expect(onCreated).toHaveBeenCalledWith(wildcardDomain);
|
||||
});
|
||||
});
|
||||
|
||||
it('the Zone details dialog uses the same nested wildcard resolver', async () => {
|
||||
renderUI(
|
||||
<ZoneDomainDialog
|
||||
open
|
||||
onOpenChange={vi.fn()}
|
||||
zoneId={zone.id}
|
||||
zoneRoot={zone.domain}
|
||||
onSaved={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText('域名'), {
|
||||
target: { value: '*.S3.Example.COM' },
|
||||
});
|
||||
expect(screen.getByText('*.s3.example.com')).toBeVisible();
|
||||
fireEvent.submit(document.getElementById('quick-create-zone-domain')!);
|
||||
await waitFor(() => {
|
||||
expect(ZoneDomainService.create).toHaveBeenCalledWith(1, {
|
||||
domain: '*.s3.example.com',
|
||||
cert_id: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.each(['*.s3', '*.other.com', 'a.*.example.com', '*.exa mple.com'])(
|
||||
'blocks invalid quick-add input without calling the API: %s',
|
||||
async (input) => {
|
||||
renderUI(
|
||||
<ZoneDomainDialog
|
||||
open
|
||||
onOpenChange={vi.fn()}
|
||||
zoneId={zone.id}
|
||||
zoneRoot={zone.domain}
|
||||
onSaved={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText('域名'), {
|
||||
target: { value: input },
|
||||
});
|
||||
fireEvent.submit(document.getElementById('quick-create-zone-domain')!);
|
||||
await waitFor(() => {
|
||||
expect(document.querySelector('.text-destructive')).toBeInTheDocument();
|
||||
});
|
||||
expect(ZoneDomainService.create).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it.each([false, true])(
|
||||
'root domain still rejects wildcards (edit=%s)',
|
||||
async (edit) => {
|
||||
renderUI(
|
||||
<ZoneEditorDialog
|
||||
open
|
||||
onOpenChange={vi.fn()}
|
||||
zone={edit ? zone : undefined}
|
||||
/>,
|
||||
);
|
||||
fireEvent.change(screen.getByLabelText('根域'), {
|
||||
target: { value: '*.example.com' },
|
||||
});
|
||||
fireEvent.submit(document.getElementById('zone-editor')!);
|
||||
expect(
|
||||
await screen.findByText('请输入不含协议或通配符的根域'),
|
||||
).toBeVisible();
|
||||
expect(ZoneService.create).not.toHaveBeenCalled();
|
||||
expect(ZoneService.update).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it('lists wildcard domains as text, never as a clickable wildcard URL', () => {
|
||||
renderUI(
|
||||
<ZoneDomainsTable
|
||||
zoneId={zone.id}
|
||||
zoneRoot={zone.domain}
|
||||
domains={[wildcardDomain]}
|
||||
certificates={[]}
|
||||
routes={[]}
|
||||
onChanged={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByText('*.example.com')).toBeVisible();
|
||||
expect(screen.queryByRole('link')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -110,4 +110,28 @@ describe('ZoneDomainSelector', () => {
|
||||
expect(screen.getByText('bound.example.com')).toBeVisible();
|
||||
expect(screen.getByText('api.example.com')).toBeVisible();
|
||||
});
|
||||
|
||||
it('searches and selects wildcard domains by ID without generating wildcard URLs', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onChange = vi.fn();
|
||||
renderSelector(
|
||||
<ZoneDomainSelector
|
||||
value={[]}
|
||||
onChange={onChange}
|
||||
domains={[
|
||||
...domains,
|
||||
{ ...domains[0], id: 9, domain: '*.s3.example.com' },
|
||||
]}
|
||||
zones={zones}
|
||||
/>,
|
||||
);
|
||||
|
||||
await user.type(screen.getByRole('textbox'), '*.s3');
|
||||
expect(screen.queryByText('api.example.com')).not.toBeInTheDocument();
|
||||
await user.click(screen.getByText('*.s3.example.com'));
|
||||
expect(onChange).toHaveBeenCalledWith([9]);
|
||||
for (const link of screen.getAllByRole('link')) {
|
||||
expect(link.getAttribute('href')).not.toContain('*');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user