feat: add MFA/TOTP auth, PII encryption, agents/leads/inquiries modules, and comprehensive tests
- Add TOTP-based MFA with setup, verify, disable, backup codes, and challenge flow - Add PII field encryption middleware with AES-256-GCM and deterministic search hashes - Add agents, inquiries, and leads domain modules with entities, events, value objects - Add web dashboard pages for inquiries and leads with detail dialogs - Add 30+ component tests (valuation, charts, listings, search, providers, UI) - Add Prisma migrations for encryption hash columns and MFA TOTP support - Fix all ESLint errors (unused imports, duplicate imports, lint auto-fixes) - Update dependencies and lock file - Clean up obsolete exploration/QA docs, add audit documentation Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { AgentPublicProfile, AgentReviewItem } from '@/lib/agents-api';
|
||||
import { AgentProfileClient } from '../agent-profile-client';
|
||||
|
||||
// Mock next/image
|
||||
vi.mock('next/image', () => ({
|
||||
default: (props: Record<string, unknown>) => <img {...props} />,
|
||||
}));
|
||||
|
||||
// Mock lucide-react
|
||||
vi.mock('lucide-react', () => ({
|
||||
BadgeCheck: () => <span data-testid="badge-check">✓</span>,
|
||||
Building2: () => <span data-testid="building">B</span>,
|
||||
Calendar: () => <span data-testid="calendar">C</span>,
|
||||
MapPin: () => <span data-testid="map-pin">M</span>,
|
||||
Phone: () => <span data-testid="phone-icon">P</span>,
|
||||
Mail: () => <span data-testid="mail">E</span>,
|
||||
Star: ({ className }: { className?: string }) => (
|
||||
<span data-testid="star" className={className}>★</span>
|
||||
),
|
||||
Home: () => <span data-testid="home">H</span>,
|
||||
MessageSquare: () => <span data-testid="message">M</span>,
|
||||
}));
|
||||
|
||||
// Mock i18n/navigation
|
||||
vi.mock('@/i18n/navigation', () => ({
|
||||
Link: ({ children, href }: { children: React.ReactNode; href: string }) => (
|
||||
<a href={href}>{children}</a>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock currency
|
||||
vi.mock('@/lib/currency', () => ({
|
||||
formatPrice: (price: string) => {
|
||||
const n = Number(price);
|
||||
return n >= 1_000_000_000 ? `${(n / 1_000_000_000).toFixed(1)} tỷ` : String(n);
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock image-blur
|
||||
vi.mock('@/lib/image-blur', () => ({
|
||||
shimmerBlurDataURL: () => 'data:image/svg+xml;base64,mock',
|
||||
}));
|
||||
|
||||
function makeAgent(overrides: Partial<AgentPublicProfile> = {}): AgentPublicProfile {
|
||||
return {
|
||||
id: 'agent-1',
|
||||
fullName: 'Nguyễn Văn A',
|
||||
avatarUrl: null,
|
||||
phone: '0912345678',
|
||||
email: 'nguyen@example.com',
|
||||
agency: 'Công ty BĐS ABC',
|
||||
licenseNumber: 'GPHN-2025-001',
|
||||
bio: 'Chuyên viên tư vấn bất động sản khu vực Quận 7',
|
||||
qualityScore: 85,
|
||||
totalDeals: 45,
|
||||
isVerified: true,
|
||||
serviceAreas: ['Quận 7', 'Quận 2', 'Nhà Bè'],
|
||||
memberSince: '2023-06-15T00:00:00Z',
|
||||
activeListings: [],
|
||||
avgReviewRating: 4.5,
|
||||
totalReviews: 20,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeReview(overrides: Partial<AgentReviewItem> = {}): AgentReviewItem {
|
||||
return {
|
||||
id: 'review-1',
|
||||
userId: 'user-1',
|
||||
userName: 'Trần Thị B',
|
||||
targetType: 'agent',
|
||||
targetId: 'agent-1',
|
||||
rating: 5,
|
||||
comment: 'Tư vấn rất nhiệt tình',
|
||||
createdAt: '2026-01-20T10:00:00Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe('AgentProfileClient', () => {
|
||||
it('renders agent name', () => {
|
||||
render(<AgentProfileClient agent={makeAgent()} reviews={[]} />);
|
||||
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('Nguyễn Văn A');
|
||||
});
|
||||
|
||||
it('renders verified badge when verified', () => {
|
||||
render(<AgentProfileClient agent={makeAgent({ isVerified: true })} reviews={[]} />);
|
||||
expect(screen.getByText('Đã xác minh')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render verified badge when not verified', () => {
|
||||
render(<AgentProfileClient agent={makeAgent({ isVerified: false })} reviews={[]} />);
|
||||
expect(screen.queryByText('Đã xác minh')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders agency name', () => {
|
||||
render(<AgentProfileClient agent={makeAgent()} reviews={[]} />);
|
||||
expect(screen.getByText('Công ty BĐS ABC')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders license number', () => {
|
||||
render(<AgentProfileClient agent={makeAgent()} reviews={[]} />);
|
||||
expect(screen.getByText(/GPHN-2025-001/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders bio', () => {
|
||||
render(<AgentProfileClient agent={makeAgent()} reviews={[]} />);
|
||||
expect(screen.getByText(/Chuyên viên tư vấn bất động sản/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders service areas', () => {
|
||||
render(<AgentProfileClient agent={makeAgent()} reviews={[]} />);
|
||||
expect(screen.getByText('Quận 7')).toBeInTheDocument();
|
||||
expect(screen.getByText('Quận 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('Nhà Bè')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders quality score', () => {
|
||||
render(<AgentProfileClient agent={makeAgent()} reviews={[]} />);
|
||||
expect(screen.getByText('85')).toBeInTheDocument();
|
||||
expect(screen.getByText('Xuất sắc')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders "Tốt" for quality score 60-79', () => {
|
||||
render(<AgentProfileClient agent={makeAgent({ qualityScore: 70 })} reviews={[]} />);
|
||||
expect(screen.getByText('Tốt')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders contact card', () => {
|
||||
render(<AgentProfileClient agent={makeAgent()} reviews={[]} />);
|
||||
expect(screen.getAllByText('Liên hệ môi giới').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('Gọi ngay').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('renders phone number', () => {
|
||||
render(<AgentProfileClient agent={makeAgent()} reviews={[]} />);
|
||||
expect(screen.getAllByText('0912345678').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('renders email when present', () => {
|
||||
render(<AgentProfileClient agent={makeAgent()} reviews={[]} />);
|
||||
expect(screen.getAllByText('nguyen@example.com').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('renders reviews section', () => {
|
||||
const reviews = [makeReview()];
|
||||
render(<AgentProfileClient agent={makeAgent()} reviews={reviews} />);
|
||||
expect(screen.getByText('Tư vấn rất nhiệt tình')).toBeInTheDocument();
|
||||
expect(screen.getByText('Trần Thị B')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows "Chưa có đánh giá nào" when no reviews', () => {
|
||||
render(<AgentProfileClient agent={makeAgent()} reviews={[]} />);
|
||||
expect(screen.getByText('Chưa có đánh giá nào')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders breadcrumb navigation', () => {
|
||||
render(<AgentProfileClient agent={makeAgent()} reviews={[]} />);
|
||||
expect(screen.getByText('Trang chủ')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders avatar placeholder when no avatarUrl', () => {
|
||||
render(<AgentProfileClient agent={makeAgent({ avatarUrl: null })} reviews={[]} />);
|
||||
expect(screen.getByText('N')).toBeInTheDocument(); // First letter of Nguyễn
|
||||
});
|
||||
|
||||
it('renders deal count stat', () => {
|
||||
render(<AgentProfileClient agent={makeAgent()} reviews={[]} />);
|
||||
expect(screen.getByText('Giao dịch')).toBeInTheDocument();
|
||||
expect(screen.getByText('45')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user