Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Integration test: POST /api/v1/alerts returns 400 when target_price is zero or
// negative (#549)
//
// A price alert with a zero or negative target price is meaningless. The
// registration endpoint must reject these values with a 400 *before* writing
// anything to the database.
//
// Uses Jest mocks — no live database connection is required.

import { httpCreateAlert } from '../alert.controllers';

// ── Lightweight request / response mocks ──────────────────────────────────────

const VALID_PAYLOAD = {
creator_id: 'creator-abc-123',
wallet_address: 'GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF',
target_price: 100,
direction: 'above',
callback_url: 'https://example.com/webhook',
};

function makeReq(body: Record<string, unknown> = {}): any {
return { body };
}

function makeRes(): any {
const res: any = {};
res.status = jest.fn().mockReturnValue(res);
res.json = jest.fn().mockReturnValue(res);
res.setHeader = jest.fn().mockReturnValue(res);
res.set = jest.fn().mockReturnValue(res);
return res;
}

function makeNext(): jest.Mock {
return jest.fn();
}

// ── Mock the alert service so no DB writes occur ───────────────────────────────

jest.mock('../alert.service', () => ({
createAlert: jest.fn(),
listAlerts: jest.fn(),
deleteAlert: jest.fn(),
}));

import { createAlert } from '../alert.service';

const mockCreateAlert = createAlert as jest.Mock;

// ── Tests ─────────────────────────────────────────────────────────────────────

describe('POST /api/v1/alerts — zero or negative target_price (#549)', () => {
beforeEach(() => {
jest.clearAllMocks();
});

// Acceptance criterion: zero target_price returns 400
it('returns 400 when target_price is zero', async () => {
const req = makeReq({ ...VALID_PAYLOAD, target_price: 0 });
const res = makeRes();
await httpCreateAlert(req, res, makeNext());

expect(res.status).toHaveBeenCalledWith(400);
});

// Acceptance criterion: negative target_price returns 400
it('returns 400 when target_price is negative (-1)', async () => {
const req = makeReq({ ...VALID_PAYLOAD, target_price: -1 });
const res = makeRes();
await httpCreateAlert(req, res, makeNext());

expect(res.status).toHaveBeenCalledWith(400);
});

it('returns 400 when target_price is a large negative number', async () => {
const req = makeReq({ ...VALID_PAYLOAD, target_price: -9999 });
const res = makeRes();
await httpCreateAlert(req, res, makeNext());

expect(res.status).toHaveBeenCalledWith(400);
});

// Acceptance criterion: error body identifies the target_price field
it('error body identifies the target_price field when target_price is zero', async () => {
const req = makeReq({ ...VALID_PAYLOAD, target_price: 0 });
const res = makeRes();
await httpCreateAlert(req, res, makeNext());

const body = res.json.mock.calls[0][0];
expect(body).toBeDefined();

// The response must reference 'target_price' either in a field array
// or in the top-level message.
const bodyStr = JSON.stringify(body);
expect(bodyStr).toMatch(/target_price/);
});

it('error body identifies the target_price field when target_price is negative', async () => {
const req = makeReq({ ...VALID_PAYLOAD, target_price: -1 });
const res = makeRes();
await httpCreateAlert(req, res, makeNext());

const body = res.json.mock.calls[0][0];
const bodyStr = JSON.stringify(body);
expect(bodyStr).toMatch(/target_price/);
});

// Acceptance criterion: no alert record created after failed request
it('does not call createAlert when target_price is zero', async () => {
const req = makeReq({ ...VALID_PAYLOAD, target_price: 0 });
const res = makeRes();
await httpCreateAlert(req, res, makeNext());

expect(mockCreateAlert).not.toHaveBeenCalled();
});

it('does not call createAlert when target_price is negative', async () => {
const req = makeReq({ ...VALID_PAYLOAD, target_price: -1 });
const res = makeRes();
await httpCreateAlert(req, res, makeNext());

expect(mockCreateAlert).not.toHaveBeenCalled();
});

// Sanity check: a valid positive target_price still reaches createAlert
it('does not return 400 when target_price is a valid positive number', async () => {
mockCreateAlert.mockResolvedValue({ id: 'alert-1', ...VALID_PAYLOAD });

const req = makeReq({ ...VALID_PAYLOAD, target_price: 50 });
const res = makeRes();
await httpCreateAlert(req, res, makeNext());

expect(res.status).not.toHaveBeenCalledWith(400);
expect(mockCreateAlert).toHaveBeenCalled();
});
});
129 changes: 129 additions & 0 deletions src/modules/creators/creator-list-empty-database.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Integration test: creator list returns empty array when no creators exist (#547)
//
// The creator list endpoint must return a 200 with an empty data array and
// accurate metadata when the database has no creator records — never a 404.
//
// Uses Jest mocks (isolated empty fixture) — no database connection required.

import { httpListCreators } from './creators.controllers';
import * as creatorsUtils from './creators.utils';

// ── Lightweight request / response mocks ──────────────────────────────────────

function makeReq(query: Record<string, string> = {}): any {
return { query };
}

function makeRes(): any {
const res: any = {};
res.status = jest.fn().mockReturnValue(res);
res.json = jest.fn().mockReturnValue(res);
res.setHeader = jest.fn().mockReturnValue(res);
res.set = jest.fn().mockReturnValue(res);
return res;
}

function makeNext(): jest.Mock {
return jest.fn();
}

// ── Tests ─────────────────────────────────────────────────────────────────────

describe('GET /api/v1/creators — empty database (#547)', () => {
beforeEach(() => {
// Simulate an empty database: no creator records exist.
jest.spyOn(creatorsUtils, 'fetchCreatorList').mockResolvedValue([[], 0]);
});

afterEach(() => {
jest.restoreAllMocks();
});

// Acceptance criterion: Returns 200 with empty data array
it('returns HTTP 200 when no creators exist (not 404)', async () => {
const req = makeReq();
const res = makeRes();
await httpListCreators(req, res, makeNext());

expect(res.status).toHaveBeenCalledWith(200);
});

it('returns an empty data array when no creators exist', async () => {
const req = makeReq();
const res = makeRes();
await httpListCreators(req, res, makeNext());

const body = res.json.mock.calls[0][0];
expect(body).toHaveProperty('data');
expect(body.data).toHaveProperty('items');
expect(Array.isArray(body.data.items)).toBe(true);
expect(body.data.items).toHaveLength(0);
});

// Acceptance criterion: meta.total is 0
it('returns meta.total of 0 when no creators exist', async () => {
const req = makeReq();
const res = makeRes();
await httpListCreators(req, res, makeNext());

const body = res.json.mock.calls[0][0];
expect(body.data).toHaveProperty('meta');
expect(body.data.meta).toHaveProperty('total', 0);
expect(typeof body.data.meta.total).toBe('number');
});

// Acceptance criterion: meta.hasMore is false
it('returns meta.hasMore of false when no creators exist', async () => {
const req = makeReq();
const res = makeRes();
await httpListCreators(req, res, makeNext());

const body = res.json.mock.calls[0][0];
expect(body.data.meta).toHaveProperty('hasMore', false);
});

it('includes all required pagination metadata fields', async () => {
const req = makeReq();
const res = makeRes();
await httpListCreators(req, res, makeNext());

const meta = res.json.mock.calls[0][0].data.meta;
expect(meta).toHaveProperty('limit');
expect(meta).toHaveProperty('offset');
expect(meta).toHaveProperty('total');
expect(meta).toHaveProperty('hasMore');
expect(typeof meta.limit).toBe('number');
expect(typeof meta.offset).toBe('number');
expect(typeof meta.hasMore).toBe('boolean');
});

it('applies default offset of 0 when not specified', async () => {
const req = makeReq();
const res = makeRes();
await httpListCreators(req, res, makeNext());

const meta = res.json.mock.calls[0][0].data.meta;
expect(meta.offset).toBe(0);
});

it('still returns 200 with empty array for explicit pagination params', async () => {
const req = makeReq({ limit: '50', offset: '100' });
const res = makeRes();
await httpListCreators(req, res, makeNext());

const body = res.json.mock.calls[0][0];
expect(res.status).toHaveBeenCalledWith(200);
expect(body.data.items).toEqual([]);
expect(body.data.meta.total).toBe(0);
expect(body.data.meta.hasMore).toBe(false);
});

it('does not invoke the error handler (next) on a successful empty response', async () => {
const req = makeReq();
const res = makeRes();
const next = makeNext();
await httpListCreators(req, res, next);

expect(next).not.toHaveBeenCalled();
});
});
Loading
Loading