- Add frontend unit tests with Vitest for components, hooks, pages, and utilities - Add backend tests for webhooks, notifications, middleware, and edge cases - Add ForgotPassword, NotFound, and ResetPassword pages - Add migration for orphaned staff resources conversion - Add coverage directory to gitignore (generated reports) - Various bug fixes and improvements from previous work 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
650 lines
19 KiB
TypeScript
650 lines
19 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
|
|
// Mock apiClient
|
|
vi.mock('../client', () => ({
|
|
default: {
|
|
get: vi.fn(),
|
|
post: vi.fn(),
|
|
patch: vi.fn(),
|
|
delete: vi.fn(),
|
|
},
|
|
}));
|
|
|
|
import {
|
|
searchDomains,
|
|
getDomainPrices,
|
|
registerDomain,
|
|
getRegisteredDomains,
|
|
getDomainRegistration,
|
|
updateNameservers,
|
|
toggleAutoRenew,
|
|
renewDomain,
|
|
syncDomain,
|
|
getSearchHistory,
|
|
DomainAvailability,
|
|
DomainPrice,
|
|
DomainRegisterRequest,
|
|
DomainRegistration,
|
|
DomainSearchHistory,
|
|
} from '../domains';
|
|
import apiClient from '../client';
|
|
|
|
describe('domains API', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('searchDomains', () => {
|
|
it('searches for domains with default TLDs', async () => {
|
|
const mockResults: DomainAvailability[] = [
|
|
{
|
|
domain: 'example.com',
|
|
available: true,
|
|
price: 12.99,
|
|
premium: false,
|
|
premium_price: null,
|
|
},
|
|
{
|
|
domain: 'example.net',
|
|
available: false,
|
|
price: null,
|
|
premium: false,
|
|
premium_price: null,
|
|
},
|
|
{
|
|
domain: 'example.org',
|
|
available: true,
|
|
price: 14.99,
|
|
premium: false,
|
|
premium_price: null,
|
|
},
|
|
];
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResults });
|
|
|
|
const result = await searchDomains('example');
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/domains/search/search/', {
|
|
query: 'example',
|
|
tlds: ['.com', '.net', '.org'],
|
|
});
|
|
expect(result).toEqual(mockResults);
|
|
expect(result).toHaveLength(3);
|
|
});
|
|
|
|
it('searches for domains with custom TLDs', async () => {
|
|
const mockResults: DomainAvailability[] = [
|
|
{
|
|
domain: 'mybusiness.io',
|
|
available: true,
|
|
price: 39.99,
|
|
premium: false,
|
|
premium_price: null,
|
|
},
|
|
{
|
|
domain: 'mybusiness.dev',
|
|
available: true,
|
|
price: 12.99,
|
|
premium: false,
|
|
premium_price: null,
|
|
},
|
|
];
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResults });
|
|
|
|
const result = await searchDomains('mybusiness', ['.io', '.dev']);
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/domains/search/search/', {
|
|
query: 'mybusiness',
|
|
tlds: ['.io', '.dev'],
|
|
});
|
|
expect(result).toEqual(mockResults);
|
|
});
|
|
|
|
it('handles premium domain results', async () => {
|
|
const mockResults: DomainAvailability[] = [
|
|
{
|
|
domain: 'premium.com',
|
|
available: true,
|
|
price: 12.99,
|
|
premium: true,
|
|
premium_price: 5000.0,
|
|
},
|
|
];
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: mockResults });
|
|
|
|
const result = await searchDomains('premium');
|
|
|
|
expect(result[0].premium).toBe(true);
|
|
expect(result[0].premium_price).toBe(5000.0);
|
|
});
|
|
});
|
|
|
|
describe('getDomainPrices', () => {
|
|
it('fetches domain prices for all TLDs', async () => {
|
|
const mockPrices: DomainPrice[] = [
|
|
{
|
|
tld: '.com',
|
|
registration: 12.99,
|
|
renewal: 14.99,
|
|
transfer: 12.99,
|
|
},
|
|
{
|
|
tld: '.net',
|
|
registration: 14.99,
|
|
renewal: 16.99,
|
|
transfer: 14.99,
|
|
},
|
|
{
|
|
tld: '.org',
|
|
registration: 14.99,
|
|
renewal: 16.99,
|
|
transfer: 14.99,
|
|
},
|
|
{
|
|
tld: '.io',
|
|
registration: 39.99,
|
|
renewal: 39.99,
|
|
transfer: 39.99,
|
|
},
|
|
];
|
|
vi.mocked(apiClient.get).mockResolvedValue({ data: mockPrices });
|
|
|
|
const result = await getDomainPrices();
|
|
|
|
expect(apiClient.get).toHaveBeenCalledWith('/domains/search/prices/');
|
|
expect(result).toEqual(mockPrices);
|
|
expect(result).toHaveLength(4);
|
|
});
|
|
});
|
|
|
|
describe('registerDomain', () => {
|
|
it('registers a new domain with full contact information', async () => {
|
|
const registerRequest: DomainRegisterRequest = {
|
|
domain: 'newbusiness.com',
|
|
years: 2,
|
|
whois_privacy: true,
|
|
auto_renew: true,
|
|
nameservers: ['ns1.digitalocean.com', 'ns2.digitalocean.com'],
|
|
contact: {
|
|
first_name: 'John',
|
|
last_name: 'Doe',
|
|
email: 'john@example.com',
|
|
phone: '+1.5551234567',
|
|
address: '123 Main St',
|
|
city: 'New York',
|
|
state: 'NY',
|
|
zip_code: '10001',
|
|
country: 'US',
|
|
},
|
|
auto_configure: true,
|
|
};
|
|
|
|
const mockRegistration: DomainRegistration = {
|
|
id: 1,
|
|
domain: 'newbusiness.com',
|
|
status: 'pending',
|
|
registered_at: null,
|
|
expires_at: null,
|
|
auto_renew: true,
|
|
whois_privacy: true,
|
|
purchase_price: 25.98,
|
|
renewal_price: null,
|
|
nameservers: ['ns1.digitalocean.com', 'ns2.digitalocean.com'],
|
|
days_until_expiry: null,
|
|
is_expiring_soon: false,
|
|
created_at: '2024-01-15T10:00:00Z',
|
|
registrant_first_name: 'John',
|
|
registrant_last_name: 'Doe',
|
|
registrant_email: 'john@example.com',
|
|
};
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: mockRegistration });
|
|
|
|
const result = await registerDomain(registerRequest);
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/domains/search/register/', registerRequest);
|
|
expect(result).toEqual(mockRegistration);
|
|
expect(result.status).toBe('pending');
|
|
});
|
|
|
|
it('registers domain without optional nameservers', async () => {
|
|
const registerRequest: DomainRegisterRequest = {
|
|
domain: 'simple.com',
|
|
years: 1,
|
|
whois_privacy: false,
|
|
auto_renew: false,
|
|
contact: {
|
|
first_name: 'Jane',
|
|
last_name: 'Smith',
|
|
email: 'jane@example.com',
|
|
phone: '+1.5559876543',
|
|
address: '456 Oak Ave',
|
|
city: 'Boston',
|
|
state: 'MA',
|
|
zip_code: '02101',
|
|
country: 'US',
|
|
},
|
|
auto_configure: false,
|
|
};
|
|
|
|
const mockRegistration: DomainRegistration = {
|
|
id: 2,
|
|
domain: 'simple.com',
|
|
status: 'pending',
|
|
registered_at: null,
|
|
expires_at: null,
|
|
auto_renew: false,
|
|
whois_privacy: false,
|
|
purchase_price: 12.99,
|
|
renewal_price: null,
|
|
nameservers: [],
|
|
days_until_expiry: null,
|
|
is_expiring_soon: false,
|
|
created_at: '2024-01-15T10:00:00Z',
|
|
};
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: mockRegistration });
|
|
|
|
const result = await registerDomain(registerRequest);
|
|
|
|
expect(result.whois_privacy).toBe(false);
|
|
expect(result.auto_renew).toBe(false);
|
|
expect(result.nameservers).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('getRegisteredDomains', () => {
|
|
it('fetches all registered domains for current business', async () => {
|
|
const mockDomains: DomainRegistration[] = [
|
|
{
|
|
id: 1,
|
|
domain: 'business1.com',
|
|
status: 'active',
|
|
registered_at: '2023-01-15T10:00:00Z',
|
|
expires_at: '2025-01-15T10:00:00Z',
|
|
auto_renew: true,
|
|
whois_privacy: true,
|
|
purchase_price: 12.99,
|
|
renewal_price: 14.99,
|
|
nameservers: ['ns1.digitalocean.com', 'ns2.digitalocean.com'],
|
|
days_until_expiry: 365,
|
|
is_expiring_soon: false,
|
|
created_at: '2023-01-15T09:00:00Z',
|
|
},
|
|
{
|
|
id: 2,
|
|
domain: 'business2.net',
|
|
status: 'active',
|
|
registered_at: '2024-01-01T10:00:00Z',
|
|
expires_at: '2024-03-01T10:00:00Z',
|
|
auto_renew: false,
|
|
whois_privacy: false,
|
|
purchase_price: 14.99,
|
|
renewal_price: 16.99,
|
|
nameservers: ['ns1.example.com', 'ns2.example.com'],
|
|
days_until_expiry: 30,
|
|
is_expiring_soon: true,
|
|
created_at: '2024-01-01T09:00:00Z',
|
|
},
|
|
];
|
|
vi.mocked(apiClient.get).mockResolvedValue({ data: mockDomains });
|
|
|
|
const result = await getRegisteredDomains();
|
|
|
|
expect(apiClient.get).toHaveBeenCalledWith('/domains/registrations/');
|
|
expect(result).toEqual(mockDomains);
|
|
expect(result).toHaveLength(2);
|
|
expect(result[1].is_expiring_soon).toBe(true);
|
|
});
|
|
|
|
it('handles empty domain list', async () => {
|
|
vi.mocked(apiClient.get).mockResolvedValue({ data: [] });
|
|
|
|
const result = await getRegisteredDomains();
|
|
|
|
expect(result).toEqual([]);
|
|
expect(result).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('getDomainRegistration', () => {
|
|
it('fetches a single domain registration by ID', async () => {
|
|
const mockDomain: DomainRegistration = {
|
|
id: 5,
|
|
domain: 'example.com',
|
|
status: 'active',
|
|
registered_at: '2023-06-01T10:00:00Z',
|
|
expires_at: '2025-06-01T10:00:00Z',
|
|
auto_renew: true,
|
|
whois_privacy: true,
|
|
purchase_price: 12.99,
|
|
renewal_price: 14.99,
|
|
nameservers: ['ns1.digitalocean.com', 'ns2.digitalocean.com', 'ns3.digitalocean.com'],
|
|
days_until_expiry: 500,
|
|
is_expiring_soon: false,
|
|
created_at: '2023-06-01T09:30:00Z',
|
|
registrant_first_name: 'Alice',
|
|
registrant_last_name: 'Johnson',
|
|
registrant_email: 'alice@example.com',
|
|
};
|
|
vi.mocked(apiClient.get).mockResolvedValue({ data: mockDomain });
|
|
|
|
const result = await getDomainRegistration(5);
|
|
|
|
expect(apiClient.get).toHaveBeenCalledWith('/domains/registrations/5/');
|
|
expect(result).toEqual(mockDomain);
|
|
expect(result.registrant_email).toBe('alice@example.com');
|
|
});
|
|
|
|
it('fetches domain with failed status', async () => {
|
|
const mockDomain: DomainRegistration = {
|
|
id: 10,
|
|
domain: 'failed.com',
|
|
status: 'failed',
|
|
registered_at: null,
|
|
expires_at: null,
|
|
auto_renew: false,
|
|
whois_privacy: false,
|
|
purchase_price: null,
|
|
renewal_price: null,
|
|
nameservers: [],
|
|
days_until_expiry: null,
|
|
is_expiring_soon: false,
|
|
created_at: '2024-01-10T10:00:00Z',
|
|
};
|
|
vi.mocked(apiClient.get).mockResolvedValue({ data: mockDomain });
|
|
|
|
const result = await getDomainRegistration(10);
|
|
|
|
expect(result.status).toBe('failed');
|
|
expect(result.registered_at).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('updateNameservers', () => {
|
|
it('updates nameservers for a domain', async () => {
|
|
const nameservers = [
|
|
'ns1.customdns.com',
|
|
'ns2.customdns.com',
|
|
'ns3.customdns.com',
|
|
'ns4.customdns.com',
|
|
];
|
|
const mockUpdated: DomainRegistration = {
|
|
id: 3,
|
|
domain: 'updated.com',
|
|
status: 'active',
|
|
registered_at: '2023-01-01T10:00:00Z',
|
|
expires_at: '2024-01-01T10:00:00Z',
|
|
auto_renew: true,
|
|
whois_privacy: true,
|
|
purchase_price: 12.99,
|
|
renewal_price: 14.99,
|
|
nameservers: nameservers,
|
|
days_until_expiry: 100,
|
|
is_expiring_soon: false,
|
|
created_at: '2023-01-01T09:00:00Z',
|
|
};
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: mockUpdated });
|
|
|
|
const result = await updateNameservers(3, nameservers);
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/domains/registrations/3/update_nameservers/', {
|
|
nameservers: nameservers,
|
|
});
|
|
expect(result.nameservers).toEqual(nameservers);
|
|
expect(result.nameservers).toHaveLength(4);
|
|
});
|
|
|
|
it('updates to default DigitalOcean nameservers', async () => {
|
|
const nameservers = ['ns1.digitalocean.com', 'ns2.digitalocean.com', 'ns3.digitalocean.com'];
|
|
const mockUpdated: DomainRegistration = {
|
|
id: 7,
|
|
domain: 'reset.com',
|
|
status: 'active',
|
|
registered_at: '2023-01-01T10:00:00Z',
|
|
expires_at: '2024-01-01T10:00:00Z',
|
|
auto_renew: false,
|
|
whois_privacy: false,
|
|
purchase_price: 12.99,
|
|
renewal_price: 14.99,
|
|
nameservers: nameservers,
|
|
days_until_expiry: 200,
|
|
is_expiring_soon: false,
|
|
created_at: '2023-01-01T09:00:00Z',
|
|
};
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: mockUpdated });
|
|
|
|
const result = await updateNameservers(7, nameservers);
|
|
|
|
expect(result.nameservers).toEqual(nameservers);
|
|
});
|
|
});
|
|
|
|
describe('toggleAutoRenew', () => {
|
|
it('enables auto-renewal for a domain', async () => {
|
|
const mockUpdated: DomainRegistration = {
|
|
id: 4,
|
|
domain: 'autorenew.com',
|
|
status: 'active',
|
|
registered_at: '2023-01-01T10:00:00Z',
|
|
expires_at: '2024-01-01T10:00:00Z',
|
|
auto_renew: true,
|
|
whois_privacy: true,
|
|
purchase_price: 12.99,
|
|
renewal_price: 14.99,
|
|
nameservers: ['ns1.digitalocean.com', 'ns2.digitalocean.com'],
|
|
days_until_expiry: 150,
|
|
is_expiring_soon: false,
|
|
created_at: '2023-01-01T09:00:00Z',
|
|
};
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: mockUpdated });
|
|
|
|
const result = await toggleAutoRenew(4, true);
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/domains/registrations/4/toggle_auto_renew/', {
|
|
auto_renew: true,
|
|
});
|
|
expect(result.auto_renew).toBe(true);
|
|
});
|
|
|
|
it('disables auto-renewal for a domain', async () => {
|
|
const mockUpdated: DomainRegistration = {
|
|
id: 6,
|
|
domain: 'noautorenew.com',
|
|
status: 'active',
|
|
registered_at: '2023-01-01T10:00:00Z',
|
|
expires_at: '2024-01-01T10:00:00Z',
|
|
auto_renew: false,
|
|
whois_privacy: false,
|
|
purchase_price: 12.99,
|
|
renewal_price: 14.99,
|
|
nameservers: ['ns1.example.com', 'ns2.example.com'],
|
|
days_until_expiry: 60,
|
|
is_expiring_soon: true,
|
|
created_at: '2023-01-01T09:00:00Z',
|
|
};
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: mockUpdated });
|
|
|
|
const result = await toggleAutoRenew(6, false);
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/domains/registrations/6/toggle_auto_renew/', {
|
|
auto_renew: false,
|
|
});
|
|
expect(result.auto_renew).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('renewDomain', () => {
|
|
it('renews domain for 1 year (default)', async () => {
|
|
const mockRenewed: DomainRegistration = {
|
|
id: 8,
|
|
domain: 'renew.com',
|
|
status: 'active',
|
|
registered_at: '2022-01-01T10:00:00Z',
|
|
expires_at: '2025-01-01T10:00:00Z',
|
|
auto_renew: true,
|
|
whois_privacy: true,
|
|
purchase_price: 12.99,
|
|
renewal_price: 14.99,
|
|
nameservers: ['ns1.digitalocean.com', 'ns2.digitalocean.com'],
|
|
days_until_expiry: 365,
|
|
is_expiring_soon: false,
|
|
created_at: '2022-01-01T09:00:00Z',
|
|
};
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: mockRenewed });
|
|
|
|
const result = await renewDomain(8);
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/domains/registrations/8/renew/', {
|
|
years: 1,
|
|
});
|
|
expect(result).toEqual(mockRenewed);
|
|
});
|
|
|
|
it('renews domain for multiple years', async () => {
|
|
const mockRenewed: DomainRegistration = {
|
|
id: 9,
|
|
domain: 'longterm.com',
|
|
status: 'active',
|
|
registered_at: '2022-01-01T10:00:00Z',
|
|
expires_at: '2027-01-01T10:00:00Z',
|
|
auto_renew: false,
|
|
whois_privacy: false,
|
|
purchase_price: 12.99,
|
|
renewal_price: 14.99,
|
|
nameservers: ['ns1.example.com', 'ns2.example.com'],
|
|
days_until_expiry: 1095,
|
|
is_expiring_soon: false,
|
|
created_at: '2022-01-01T09:00:00Z',
|
|
};
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: mockRenewed });
|
|
|
|
const result = await renewDomain(9, 5);
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/domains/registrations/9/renew/', {
|
|
years: 5,
|
|
});
|
|
expect(result).toEqual(mockRenewed);
|
|
});
|
|
|
|
it('renews domain for 2 years', async () => {
|
|
const mockRenewed: DomainRegistration = {
|
|
id: 11,
|
|
domain: 'twoyears.com',
|
|
status: 'active',
|
|
registered_at: '2023-01-01T10:00:00Z',
|
|
expires_at: '2026-01-01T10:00:00Z',
|
|
auto_renew: true,
|
|
whois_privacy: true,
|
|
purchase_price: 12.99,
|
|
renewal_price: 14.99,
|
|
nameservers: ['ns1.digitalocean.com', 'ns2.digitalocean.com'],
|
|
days_until_expiry: 730,
|
|
is_expiring_soon: false,
|
|
created_at: '2023-01-01T09:00:00Z',
|
|
};
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: mockRenewed });
|
|
|
|
const result = await renewDomain(11, 2);
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/domains/registrations/11/renew/', {
|
|
years: 2,
|
|
});
|
|
expect(result.expires_at).toBe('2026-01-01T10:00:00Z');
|
|
});
|
|
});
|
|
|
|
describe('syncDomain', () => {
|
|
it('syncs domain information from NameSilo', async () => {
|
|
const mockSynced: DomainRegistration = {
|
|
id: 12,
|
|
domain: 'synced.com',
|
|
status: 'active',
|
|
registered_at: '2023-05-15T10:00:00Z',
|
|
expires_at: '2024-05-15T10:00:00Z',
|
|
auto_renew: true,
|
|
whois_privacy: true,
|
|
purchase_price: 12.99,
|
|
renewal_price: 14.99,
|
|
nameservers: ['ns1.namesilo.com', 'ns2.namesilo.com'],
|
|
days_until_expiry: 120,
|
|
is_expiring_soon: false,
|
|
created_at: '2023-05-15T09:30:00Z',
|
|
};
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: mockSynced });
|
|
|
|
const result = await syncDomain(12);
|
|
|
|
expect(apiClient.post).toHaveBeenCalledWith('/domains/registrations/12/sync/');
|
|
expect(result).toEqual(mockSynced);
|
|
});
|
|
|
|
it('syncs domain and updates status', async () => {
|
|
const mockSynced: DomainRegistration = {
|
|
id: 13,
|
|
domain: 'expired.com',
|
|
status: 'expired',
|
|
registered_at: '2020-01-01T10:00:00Z',
|
|
expires_at: '2023-01-01T10:00:00Z',
|
|
auto_renew: false,
|
|
whois_privacy: false,
|
|
purchase_price: 12.99,
|
|
renewal_price: 14.99,
|
|
nameservers: [],
|
|
days_until_expiry: -365,
|
|
is_expiring_soon: false,
|
|
created_at: '2020-01-01T09:00:00Z',
|
|
};
|
|
vi.mocked(apiClient.post).mockResolvedValue({ data: mockSynced });
|
|
|
|
const result = await syncDomain(13);
|
|
|
|
expect(result.status).toBe('expired');
|
|
expect(result.days_until_expiry).toBeLessThan(0);
|
|
});
|
|
});
|
|
|
|
describe('getSearchHistory', () => {
|
|
it('fetches domain search history', async () => {
|
|
const mockHistory: DomainSearchHistory[] = [
|
|
{
|
|
id: 1,
|
|
searched_domain: 'example.com',
|
|
was_available: true,
|
|
price: 12.99,
|
|
searched_at: '2024-01-15T10:00:00Z',
|
|
},
|
|
{
|
|
id: 2,
|
|
searched_domain: 'taken.com',
|
|
was_available: false,
|
|
price: null,
|
|
searched_at: '2024-01-15T10:05:00Z',
|
|
},
|
|
{
|
|
id: 3,
|
|
searched_domain: 'premium.com',
|
|
was_available: true,
|
|
price: 5000.0,
|
|
searched_at: '2024-01-15T10:10:00Z',
|
|
},
|
|
];
|
|
vi.mocked(apiClient.get).mockResolvedValue({ data: mockHistory });
|
|
|
|
const result = await getSearchHistory();
|
|
|
|
expect(apiClient.get).toHaveBeenCalledWith('/domains/history/');
|
|
expect(result).toEqual(mockHistory);
|
|
expect(result).toHaveLength(3);
|
|
expect(result[1].was_available).toBe(false);
|
|
expect(result[2].price).toBe(5000.0);
|
|
});
|
|
|
|
it('handles empty search history', async () => {
|
|
vi.mocked(apiClient.get).mockResolvedValue({ data: [] });
|
|
|
|
const result = await getSearchHistory();
|
|
|
|
expect(result).toEqual([]);
|
|
expect(result).toHaveLength(0);
|
|
});
|
|
});
|
|
});
|