102 lines
3.2 KiB
JavaScript
102 lines
3.2 KiB
JavaScript
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||
|
|
|
||
|
|
vi.mock('../../lib/sql.js', () => ({
|
||
|
|
sql: vi.fn(),
|
||
|
|
}));
|
||
|
|
vi.mock('../../lib/permission-middleware.js', () => ({
|
||
|
|
getUserFromRequest: vi.fn(),
|
||
|
|
}));
|
||
|
|
vi.mock('../../lib/rate-limit.js', () => ({
|
||
|
|
checkStoresRateLimit: vi.fn(),
|
||
|
|
}));
|
||
|
|
vi.mock('../../lib/pricing/geo-cache.js', () => ({
|
||
|
|
getZipCoords: vi.fn(),
|
||
|
|
getStoresNear: vi.fn(),
|
||
|
|
}));
|
||
|
|
|
||
|
|
import { sql } from '../../lib/sql.js';
|
||
|
|
import { getUserFromRequest } from '../../lib/permission-middleware.js';
|
||
|
|
import { checkStoresRateLimit } from '../../lib/rate-limit.js';
|
||
|
|
import { getZipCoords, getStoresNear } from '../../lib/pricing/geo-cache.js';
|
||
|
|
import handler from '../../pages/api/stores/nearby.js';
|
||
|
|
|
||
|
|
function createRes() {
|
||
|
|
const res = {
|
||
|
|
statusCode: 200,
|
||
|
|
headers: {},
|
||
|
|
body: null,
|
||
|
|
status(code) {
|
||
|
|
res.statusCode = code;
|
||
|
|
return res;
|
||
|
|
},
|
||
|
|
setHeader(name, value) {
|
||
|
|
res.headers[name] = value;
|
||
|
|
return res;
|
||
|
|
},
|
||
|
|
json(data) {
|
||
|
|
res.body = data;
|
||
|
|
return res;
|
||
|
|
},
|
||
|
|
};
|
||
|
|
return res;
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('GET /api/stores/nearby', () => {
|
||
|
|
beforeEach(() => {
|
||
|
|
vi.clearAllMocks();
|
||
|
|
process.env.PRICING_LOCAL_ENABLED = 'true';
|
||
|
|
getUserFromRequest.mockResolvedValue({ userId: 1, email: 'a@b.c', role: 'user' });
|
||
|
|
checkStoresRateLimit.mockResolvedValue({ allowed: true, remaining: 0, reset: Date.now() + 3600000 });
|
||
|
|
sql.mockResolvedValue({ rows: [{ home_zip: '10001' }] });
|
||
|
|
getZipCoords.mockResolvedValue({ lat: 40.7, lng: -74.0, displayName: 'NYC', cached: true });
|
||
|
|
getStoresNear.mockResolvedValue({
|
||
|
|
stores: [{ name: 'Game Shop', distanceMiles: 1.2 }],
|
||
|
|
cached: true,
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns 401 without authentication', async () => {
|
||
|
|
getUserFromRequest.mockResolvedValue(null);
|
||
|
|
const res = createRes();
|
||
|
|
await handler({ method: 'GET', query: {} }, res);
|
||
|
|
expect(res.statusCode).toBe(401);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns 503 when local pricing is disabled', async () => {
|
||
|
|
process.env.PRICING_LOCAL_ENABLED = 'false';
|
||
|
|
const res = createRes();
|
||
|
|
await handler({ method: 'GET', query: {} }, res);
|
||
|
|
expect(res.statusCode).toBe(503);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns 429 with Retry-After when rate limited', async () => {
|
||
|
|
checkStoresRateLimit.mockResolvedValue({
|
||
|
|
allowed: false,
|
||
|
|
remaining: 0,
|
||
|
|
reset: Date.now() + 1800000,
|
||
|
|
});
|
||
|
|
const res = createRes();
|
||
|
|
await handler({ method: 'GET', query: {} }, res);
|
||
|
|
expect(res.statusCode).toBe(429);
|
||
|
|
expect(res.headers['Retry-After']).toBeTruthy();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns stores for authenticated user with saved zip', async () => {
|
||
|
|
const res = createRes();
|
||
|
|
await handler({ method: 'GET', query: { zip: '90210' } }, res);
|
||
|
|
expect(res.statusCode).toBe(200);
|
||
|
|
expect(res.body.homeZip).toBe('10001');
|
||
|
|
expect(res.body.stores).toHaveLength(1);
|
||
|
|
expect(getZipCoords).toHaveBeenCalledWith('10001');
|
||
|
|
expect(getStoresNear).toHaveBeenCalled();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('returns 400 when home zip is not configured', async () => {
|
||
|
|
sql.mockResolvedValue({ rows: [{ home_zip: null }] });
|
||
|
|
const res = createRes();
|
||
|
|
await handler({ method: 'GET', query: {} }, res);
|
||
|
|
expect(res.statusCode).toBe(400);
|
||
|
|
expect(res.body.code).toBe('NO_HOME_ZIP');
|
||
|
|
});
|
||
|
|
});
|