feat(cache): implement Redis caching for search & analytics hot paths
- Add TTL-specific cache durations: district stats (5min), market report (15min), heatmap (5min) - Add Redis caching to GeoSearch handler with 60s TTL - Add cache invalidation on listing.approved, listing.updated, listing.deactivated, listing.sold events - Invalidate search, geo_search, and all analytics cache prefixes on listing state changes - Update tests for new CacheService dependency in event handler and geo-search handler Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
140
apps/web/app/(public)/search/__tests__/search.spec.tsx
Normal file
140
apps/web/app/(public)/search/__tests__/search.spec.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const mockPush = vi.fn();
|
||||
const mockReplace = vi.fn();
|
||||
const mockSearchParams = new URLSearchParams();
|
||||
vi.mock('next/navigation', () => ({
|
||||
useRouter: () => ({ push: mockPush, replace: mockReplace }),
|
||||
useSearchParams: () => mockSearchParams,
|
||||
}));
|
||||
|
||||
vi.mock('next/link', () => ({
|
||||
default: ({ children, href, ...props }: { children: React.ReactNode; href: string; [key: string]: unknown }) => (
|
||||
<a href={href} {...props}>{children}</a>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('next/image', () => ({
|
||||
default: (props: Record<string, unknown>) => <img {...props} />,
|
||||
}));
|
||||
|
||||
// Mock dynamic import for map component
|
||||
vi.mock('next/dynamic', () => ({
|
||||
default: () => {
|
||||
const MockMap = () => <div data-testid="map-placeholder">Map</div>;
|
||||
MockMap.displayName = 'MockMap';
|
||||
return MockMap;
|
||||
},
|
||||
}));
|
||||
|
||||
const mockListings = {
|
||||
data: [
|
||||
{
|
||||
id: '1',
|
||||
status: 'ACTIVE',
|
||||
transactionType: 'SALE',
|
||||
priceVND: '5000000000',
|
||||
pricePerM2: null,
|
||||
rentPriceMonthly: null,
|
||||
commissionPct: null,
|
||||
viewCount: 10,
|
||||
saveCount: 2,
|
||||
inquiryCount: 1,
|
||||
publishedAt: '2024-01-01',
|
||||
createdAt: '2024-01-01',
|
||||
property: {
|
||||
id: 'p1',
|
||||
propertyType: 'APARTMENT',
|
||||
title: 'Căn hộ Quận 7',
|
||||
description: 'Căn hộ view sông',
|
||||
address: '123 Nguyễn Hữu Thọ',
|
||||
ward: 'Phường Tân Hưng',
|
||||
district: 'Quận 7',
|
||||
city: 'Hồ Chí Minh',
|
||||
areaM2: 75,
|
||||
bedrooms: 2,
|
||||
bathrooms: 2,
|
||||
floors: null,
|
||||
direction: null,
|
||||
yearBuilt: null,
|
||||
legalStatus: null,
|
||||
amenities: null,
|
||||
projectName: null,
|
||||
media: [],
|
||||
},
|
||||
seller: { id: 's1', fullName: 'Nguyen Van A', phone: '0912345678' },
|
||||
agent: null,
|
||||
},
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
limit: 12,
|
||||
totalPages: 1,
|
||||
};
|
||||
|
||||
vi.mock('@/lib/listings-api', () => ({
|
||||
listingsApi: {
|
||||
search: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { listingsApi } from '@/lib/listings-api';
|
||||
import SearchPage from '../page';
|
||||
|
||||
const mockedListingsApi = vi.mocked(listingsApi);
|
||||
|
||||
describe('SearchPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockedListingsApi.search.mockResolvedValue(mockListings as never);
|
||||
});
|
||||
|
||||
it('renders the search page title', async () => {
|
||||
render(<SearchPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Tìm kiếm bất động sản')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders view mode toggle buttons', async () => {
|
||||
render(<SearchPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /danh sách/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: /bản đồ/i })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls listings API on mount', async () => {
|
||||
render(<SearchPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedListingsApi.search).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('displays listing results after loading', async () => {
|
||||
render(<SearchPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/căn hộ quận 7/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('switches to map view when map button is clicked', async () => {
|
||||
render(<SearchPage />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /bản đồ/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByRole('button', { name: /bản đồ/i }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('map-placeholder')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user