Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions apps/public-api/src/__tests__/aggregation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
const mockAggregate = jest.fn();

jest.mock('@urbackend/common', () => ({
sanitize: (v) => v,
AppError: class AppError extends Error { constructor(code, msg, errTitle) { super(msg); this.statusCode=code; this.error=errTitle||'Error'; } },
ApiResponse: class ApiResponse { constructor(d, m) { this.data=d; this.message=m; this.success=true; } send(res, code) { return res.status(code).json({ success: this.success, data: this.data, message: this.message }); } },
sanitize: (v) => v,
Project: {},
getConnection: jest.fn().mockResolvedValue({}),
getCompiledModel: jest.fn(() => ({
Expand All @@ -15,6 +17,7 @@ jest.mock('@urbackend/common', () => ({
aggregateSchema: require('../../../../packages/common/src/utils/input.validation').aggregateSchema,
}));

const { AppError } = require('@urbackend/common');
const { aggregateData } = require('../controllers/data.controller');

function makeReq(overrides = {}) {
Expand Down Expand Up @@ -50,16 +53,19 @@ function makeRes() {
}

describe('aggregateData controller', () => {
let next;

beforeEach(() => {
jest.clearAllMocks();
mockAggregate.mockResolvedValue([{ _id: 'published', count: 2 }]);
next = jest.fn();
});

test('executes a valid aggregation pipeline', async () => {
const req = makeReq({ query: {} });
const res = makeRes();

await aggregateData(req, res);
await aggregateData(req, res, next);

expect(mockAggregate).toHaveBeenCalledWith([
{ $match: { isDeleted: { $ne: true } } },
Expand All @@ -86,7 +92,7 @@ describe('aggregateData controller', () => {
});
const res = makeRes();

await aggregateData(req, res);
await aggregateData(req, res, next);

expect(mockAggregate).toHaveBeenCalledWith([
{ $match: { userId: 'user_1', isDeleted: { $ne: true } } },
Expand All @@ -103,7 +109,7 @@ describe('aggregateData controller', () => {
});
const res = makeRes();

await aggregateData(req, res);
await aggregateData(req, res, next);

expect(mockAggregate).toHaveBeenCalledWith([
{ $match: {} }, // softDeleteFilter should be empty
Expand All @@ -118,15 +124,12 @@ describe('aggregateData controller', () => {
});
const res = makeRes();

await aggregateData(req, res);
await aggregateData(req, res, next);

expect(mockAggregate).not.toHaveBeenCalled();
expect(res.statusCode).toBe(400);
expect(res.body).toEqual({
success: false,
data: {},
message: 'Aggregation pipeline contains blocked stage.',
});
expect(next).toHaveBeenCalledWith(expect.any(AppError));
expect(next.mock.calls[0][0].statusCode).toBe(400);
expect(next.mock.calls[0][0].message).toBe('Aggregation pipeline contains blocked stage.');
});

test('rejects invalid pipeline payloads', async () => {
Expand All @@ -135,14 +138,11 @@ describe('aggregateData controller', () => {
});
const res = makeRes();

await aggregateData(req, res);
await aggregateData(req, res, next);

expect(mockAggregate).not.toHaveBeenCalled();
expect(res.statusCode).toBe(400);
expect(res.body).toEqual({
success: false,
data: {},
message: 'Invalid input: expected array, received object',
});
expect(next).toHaveBeenCalledWith(expect.any(AppError));
expect(next.mock.calls[0][0].statusCode).toBe(400);
expect(next.mock.calls[0][0].message).toBe('Invalid input: expected array, received object');
});
});
46 changes: 21 additions & 25 deletions apps/public-api/src/__tests__/data.controller.read.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ const mockQueryEngine = jest.fn((query) => {
});

jest.mock('@urbackend/common', () => ({
sanitize: (v) => v,
AppError: class AppError extends Error { constructor(code, msg, errTitle) { super(msg); this.statusCode=code; this.error=errTitle||'Error'; } },
ApiResponse: class ApiResponse { constructor(d, m) { this.data=d; this.message=m; this.success=true; } send(res, code) { return res.status(code).json({ success: this.success, data: this.data, message: this.message }); } },
sanitize: (v) => v,
Project: {},
getConnection: jest.fn().mockResolvedValue({}),
getCompiledModel: jest.fn((connection, collectionConfig, projectId, isExternal) => ({
Expand Down Expand Up @@ -57,6 +59,7 @@ jest.mock('@urbackend/common', () => ({
validateUpdateData: jest.fn(),
}));

const { AppError } = require('@urbackend/common');
const { getAllData, getSingleDoc } = require('../controllers/data.controller');

function makeReq(overrides = {}) {
Expand Down Expand Up @@ -92,16 +95,19 @@ function makeRes() {
}

describe('data.controller read RLS filters', () => {
let next;

beforeEach(() => {
jest.clearAllMocks();
mockEnginePopulate.mockClear();
next = jest.fn();
});

test('getAllData applies rlsFilter to find()', async () => {
const req = makeReq({ rlsFilter: { userId: 'user_1' } });
const res = makeRes();

await getAllData(req, res);
await getAllData(req, res, next);

expect(mockFind).toHaveBeenCalledWith();
expect(mockAnd).toHaveBeenCalledWith([{ userId: 'user_1' }]);
Expand All @@ -111,8 +117,7 @@ describe('data.controller read RLS filters', () => {
test('getSingleDoc applies rlsFilter to findOne()', async () => {
const req = makeReq({ rlsFilter: { userId: 'user_1' } });
const res = makeRes();

await getSingleDoc(req, res);
await getSingleDoc(req, res, next);

expect(mockFindOne).toHaveBeenCalledWith({
$and: [
Expand All @@ -127,8 +132,7 @@ describe('data.controller read RLS filters', () => {
test('getAllData calls engine.populate() when populate param is provided', async () => {
const req = makeReq({ query: { populate: 'author,comments' } });
const res = makeRes();

await getAllData(req, res);
await getAllData(req, res, next);

expect(mockQueryEngine).toHaveBeenCalled();
expect(mockEnginePopulate).toHaveBeenCalled();
Expand All @@ -137,15 +141,14 @@ describe('data.controller read RLS filters', () => {
test('getAllData does not forward populate/expand to Mongo filter', async () => {
const req = makeReq({ query: { populate: 'author', expand: 'category', title: 'hello' } });
const res = makeRes();

await getAllData(req, res);
await getAllData(req, res, next);

// mockFind is called with the raw Mongoose query (no args for Model.find())
// The real filter exclusion is tested via the QueryEngine directly,
// but we confirm find() was invoked and the request did not error out.
expect(mockFind).toHaveBeenCalledWith();
expect(res.json).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalledWith(500);
expect(next).not.toHaveBeenCalled();
});

test('getAllData returns 400 when QueryEngine throws query validation error', async () => {
Expand All @@ -165,21 +168,17 @@ describe('data.controller read RLS filters', () => {
const req = makeReq({ query: { name_regex: '[' } });
const res = makeRes();

await getAllData(req, res);
await getAllData(req, res, next);

expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({
success: false,
data: {},
message: 'Invalid regex pattern for "name_regex".',
});
expect(next).toHaveBeenCalledWith(expect.any(AppError));
expect(next.mock.calls[0][0].statusCode).toBe(400);
expect(next.mock.calls[0][0].message).toContain('Invalid regex pattern for "name_regex".');
});

test('getSingleDoc calls populate on the query', async () => {
const req = makeReq({ query: { populate: 'author' } });
const res = makeRes();

await getSingleDoc(req, res);
await getSingleDoc(req, res, next);

expect(mockPopulate).toHaveBeenCalledWith('author');
expect(res.json).toHaveBeenCalled();
Expand All @@ -188,20 +187,18 @@ describe('data.controller read RLS filters', () => {
test('getSingleDoc handles array-format populate param without crashing', async () => {
const req = makeReq({ query: { populate: ['author', 'category'] } });
const res = makeRes();

await getSingleDoc(req, res);
await getSingleDoc(req, res, next);

expect(mockPopulate).toHaveBeenCalledWith('author');
expect(mockPopulate).toHaveBeenCalledWith('category');
expect(res.json).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalledWith(500);
expect(next).not.toHaveBeenCalled();
});

test('getSingleDoc excludes soft-deleted documents by default', async () => {
const req = makeReq();
const res = makeRes();

await getSingleDoc(req, res);
await getSingleDoc(req, res, next);

expect(mockFindOne).toHaveBeenCalled();
const findOneArgs = mockFindOne.mock.calls[0][0];
Expand All @@ -219,8 +216,7 @@ describe('data.controller read RLS filters', () => {
rlsFilter: { ownerId: 'user_123' }
});
const res = makeRes();

await getSingleDoc(req, res);
await getSingleDoc(req, res, next);

expect(mockFindOne).toHaveBeenCalled();
const findOneArgs = mockFindOne.mock.calls[0][0];
Expand Down
26 changes: 13 additions & 13 deletions apps/public-api/src/__tests__/health.route.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ jest.mock('mongoose', () => ({
}));

jest.mock('@urbackend/common', () => ({
AppError: class AppError extends Error { constructor(code, msg, errTitle) { super(msg); this.statusCode=code; this.error=errTitle||'Error'; } },
ApiResponse: class ApiResponse { constructor(d, m) { this.data=d; this.message=m; this.success=true; } send(res, code) { return res.status(code).json({ success: this.success, data: this.data, message: this.message }); } },
redis: mockRedis,
}));

Expand All @@ -31,18 +33,22 @@ describe('health route', () => {

app = express();
app.use('/api/health', healthRoute);
app.use((err, req, res, next) => {
res.status(err.statusCode || 500).json({ success: false, error: err.error, message: err.message });
});
});

test('returns ok when mongodb and redis are connected', async () => {
const res = await request(app).get('/api/health');

expect(res.status).toBe(200);
expect(res.body.status).toBe('ok');
expect(res.body.dependencies).toEqual({
expect(res.body.success).toBe(true);
expect(res.body.data.status).toBe('ok');
expect(res.body.data.dependencies).toEqual({
mongodb: 'connected',
redis: 'connected',
});
expect(typeof res.body.timestamp).toBe('string');
expect(typeof res.body.data.timestamp).toBe('string');
expect(mockRedis.ping).toHaveBeenCalledTimes(1);
});

Expand All @@ -52,11 +58,8 @@ describe('health route', () => {
const res = await request(app).get('/api/health');

expect(res.status).toBe(503);
expect(res.body.status).toBe('error');
expect(res.body.dependencies).toEqual({
mongodb: 'disconnected',
redis: 'connected',
});
expect(res.body.success).toBe(false);
expect(res.body.message).toBe('Service unavailable');
});

test('returns error when redis is not responsive', async () => {
Expand All @@ -65,10 +68,7 @@ describe('health route', () => {
const res = await request(app).get('/api/health');

expect(res.status).toBe(503);
expect(res.body.status).toBe('error');
expect(res.body.dependencies).toEqual({
mongodb: 'connected',
redis: 'disconnected',
});
expect(res.body.success).toBe(false);
expect(res.body.message).toBe('Service unavailable');
});
});
Loading
Loading