feat: Add tier-based vehicle limit enforcement (refs #23)
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 4m37s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 38s
Deploy to Staging / Verify Staging (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
All checks were successful
Deploy to Staging / Build Images (pull_request) Successful in 4m37s
Deploy to Staging / Deploy to Staging (pull_request) Successful in 38s
Deploy to Staging / Verify Staging (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Ready (pull_request) Successful in 6s
Deploy to Staging / Notify Staging Failure (pull_request) Has been skipped
Backend: - Add VEHICLE_LIMITS configuration to feature-tiers.ts - Add getVehicleLimit, canAddVehicle helper functions - Implement transaction-based limit check with FOR UPDATE locking - Add VehicleLimitExceededError and 403 TIER_REQUIRED response - Add countByUserId to VehiclesRepository - Add comprehensive tests for all limit logic Frontend: - Add getResourceLimit, isAtResourceLimit to useTierAccess hook - Create VehicleLimitDialog component with mobile/desktop modes - Add useVehicleLimitCheck shared hook for limit state - Update VehiclesPage with limit checks and lock icon - Update VehiclesMobileScreen with limit checks - Add tests for VehicleLimitDialog Implements vehicle limits per tier (Free: 2, Pro: 5, Enterprise: unlimited) with race condition prevention and consistent UX across mobile/desktop. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* @ai-summary Unit tests for VehiclesRepository
|
||||
* @ai-context Tests repository data access methods with mocked database
|
||||
*/
|
||||
|
||||
import { Pool } from 'pg';
|
||||
import { VehiclesRepository } from '../../data/vehicles.repository';
|
||||
|
||||
describe('VehiclesRepository', () => {
|
||||
let pool: Pool;
|
||||
let repository: VehiclesRepository;
|
||||
|
||||
beforeEach(() => {
|
||||
pool = {
|
||||
query: jest.fn(),
|
||||
} as any;
|
||||
repository = new VehiclesRepository(pool);
|
||||
});
|
||||
|
||||
describe('countByUserId', () => {
|
||||
it('returns accurate count of active vehicles', async () => {
|
||||
const mockResult = {
|
||||
rows: [{ count: '3' }],
|
||||
};
|
||||
(pool.query as jest.Mock).mockResolvedValue(mockResult);
|
||||
|
||||
const count = await repository.countByUserId('user-123');
|
||||
|
||||
expect(count).toBe(3);
|
||||
expect(pool.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining('SELECT COUNT(*) as count FROM vehicles'),
|
||||
['user-123']
|
||||
);
|
||||
expect(pool.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining('is_active = true'),
|
||||
['user-123']
|
||||
);
|
||||
});
|
||||
|
||||
it('returns 0 for user with no vehicles', async () => {
|
||||
const mockResult = {
|
||||
rows: [{ count: '0' }],
|
||||
};
|
||||
(pool.query as jest.Mock).mockResolvedValue(mockResult);
|
||||
|
||||
const count = await repository.countByUserId('user-empty');
|
||||
|
||||
expect(count).toBe(0);
|
||||
});
|
||||
|
||||
it('only counts active vehicles (excludes deleted)', async () => {
|
||||
const mockResult = {
|
||||
rows: [{ count: '2' }],
|
||||
};
|
||||
(pool.query as jest.Mock).mockResolvedValue(mockResult);
|
||||
|
||||
const count = await repository.countByUserId('user-with-deleted');
|
||||
|
||||
expect(count).toBe(2);
|
||||
expect(pool.query).toHaveBeenCalledWith(
|
||||
expect.stringContaining('is_active = true'),
|
||||
['user-with-deleted']
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -53,10 +53,19 @@ describe('VehiclesService', () => {
|
||||
findByUserAndVIN: jest.fn(),
|
||||
update: jest.fn(),
|
||||
softDelete: jest.fn(),
|
||||
countByUserId: jest.fn(),
|
||||
} as any;
|
||||
|
||||
const mockPool = {
|
||||
query: jest.fn(),
|
||||
connect: jest.fn().mockResolvedValue({
|
||||
query: jest.fn(),
|
||||
release: jest.fn(),
|
||||
}),
|
||||
} as any;
|
||||
|
||||
mockRepository.mockImplementation(() => repositoryInstance);
|
||||
service = new VehiclesService(repositoryInstance);
|
||||
service = new VehiclesService(repositoryInstance, mockPool);
|
||||
});
|
||||
|
||||
describe('dropdown data integration', () => {
|
||||
|
||||
Reference in New Issue
Block a user