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,119 @@
// Integration test: creator stats endpoint — no trade history
//
// Verifies that:
// 1. A creator with no trade history returns a valid response
// 2. Supply and holder count fields are zero rather than null or absent
// 3. All expected fields are present and not null
//
// Uses Jest mocks with a minimal fixture set — no database required.
// Follows the same conventions as creator-detail-empty-social-links.integration.test.ts

import { httpGetCreatorStats } from './creators.controllers';

Check failure on line 11 in src/modules/creator/creator-detail-no-trade-history.integration.test.ts

View workflow job for this annotation

GitHub Actions / verify

Cannot find module './creators.controllers' or its corresponding type declarations.

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

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

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/:id/stats — no trade history', () => {
afterEach(() => {
jest.restoreAllMocks();
});

it('returns HTTP 200 for a creator with no trade history', async () => {
const req = makeReq({ id: 'creator-1' });
const res = makeRes();
await httpGetCreatorStats(req, res, makeNext());

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

it('response includes holderCount field set to zero', async () => {
const req = makeReq({ id: 'creator-1' });
const res = makeRes();
await httpGetCreatorStats(req, res, makeNext());

const body = res.json.mock.calls[0][0];
expect(body.data).toHaveProperty('holderCount');
expect(body.data.holderCount).toBe(0);
});

it('response includes totalSupply field set to zero', async () => {
const req = makeReq({ id: 'creator-1' });
const res = makeRes();
await httpGetCreatorStats(req, res, makeNext());

const body = res.json.mock.calls[0][0];
expect(body.data).toHaveProperty('totalSupply');
expect(body.data.totalSupply).toBe(0);
});

it('response includes totalVolume field set to zero', async () => {
const req = makeReq({ id: 'creator-1' });
const res = makeRes();
await httpGetCreatorStats(req, res, makeNext());

const body = res.json.mock.calls[0][0];
expect(body.data).toHaveProperty('totalVolume');
expect(body.data.totalVolume).toBe(0);
});

it('holderCount is not null or absent', async () => {
const req = makeReq({ id: 'creator-1' });
const res = makeRes();
await httpGetCreatorStats(req, res, makeNext());

const body = res.json.mock.calls[0][0];
expect(body.data.holderCount).not.toBeNull();
expect(body.data.holderCount).toBeDefined();
});

it('totalSupply is not null or absent', async () => {
const req = makeReq({ id: 'creator-1' });
const res = makeRes();
await httpGetCreatorStats(req, res, makeNext());

const body = res.json.mock.calls[0][0];
expect(body.data.totalSupply).not.toBeNull();
expect(body.data.totalSupply).toBeDefined();
});

it('response envelope is well-formed with success field', async () => {
const req = makeReq({ id: 'creator-1' });
const res = makeRes();
await httpGetCreatorStats(req, res, makeNext());

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

it('all expected numeric fields are present in response', async () => {
const req = makeReq({ id: 'creator-1' });
const res = makeRes();
await httpGetCreatorStats(req, res, makeNext());

const body = res.json.mock.calls[0][0];
const expectedFields = ['holderCount', 'totalSupply', 'totalVolume'];
expectedFields.forEach(field => {
expect(body.data).toHaveProperty(field);
expect(typeof body.data[field]).toBe('number');
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// Integration test: creator list endpoint — negative page size
//
// Verifies that:
// 1. A request with a negative page size returns HTTP 400
// 2. The error body matches the standard validation error shape
// 3. No database query is issued for the invalid input
//
// Uses Jest mocks with a minimal fixture set — no database required.
// Follows the same conventions as creator-list-page-size-boundary.integration.test.ts

import { httpListCreators } from './creators.controllers';

Check failure on line 11 in src/modules/creator/creator-list-negative-page-size.integration.test.ts

View workflow job for this annotation

GitHub Actions / verify

Cannot find module './creators.controllers' or its corresponding type declarations.
import * as creatorsUtils from './creators.utils';

Check failure on line 12 in src/modules/creator/creator-list-negative-page-size.integration.test.ts

View workflow job for this annotation

GitHub Actions / verify

Cannot find module './creators.utils' or its corresponding type declarations.

// ── 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 — negative page size', () => {
afterEach(() => {
jest.restoreAllMocks();
});

it('rejects limit=-1 with HTTP 400', async () => {
jest.spyOn(creatorsUtils, 'fetchCreatorList');

const req = makeReq({ limit: '-1' });
const res = makeRes();
await httpListCreators(req, res, makeNext());

expect(res.status).toHaveBeenCalledWith(400);
const body = res.json.mock.calls[0][0];
expect(body.success).toBe(false);
});

it('rejects limit=-10 with HTTP 400', async () => {
jest.spyOn(creatorsUtils, 'fetchCreatorList');

const req = makeReq({ limit: '-10' });
const res = makeRes();
await httpListCreators(req, res, makeNext());

expect(res.status).toHaveBeenCalledWith(400);
const body = res.json.mock.calls[0][0];
expect(body.success).toBe(false);
});

it('rejects limit=-100 with HTTP 400', async () => {
jest.spyOn(creatorsUtils, 'fetchCreatorList');

const req = makeReq({ limit: '-100' });
const res = makeRes();
await httpListCreators(req, res, makeNext());

expect(res.status).toHaveBeenCalledWith(400);
const body = res.json.mock.calls[0][0];
expect(body.success).toBe(false);
});

it('does not call fetchCreatorList when limit is negative', async () => {
const spy = jest.spyOn(creatorsUtils, 'fetchCreatorList');

const req = makeReq({ limit: '-5' });
const res = makeRes();
await httpListCreators(req, res, makeNext());

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

it('response body matches standard validation error shape', async () => {
jest.spyOn(creatorsUtils, 'fetchCreatorList');

const req = makeReq({ limit: '-1' });
const res = makeRes();
await httpListCreators(req, res, makeNext());

const body = res.json.mock.calls[0][0];
expect(body).toHaveProperty('success', false);
expect(body).toHaveProperty('message');
expect(body).toHaveProperty('data');
expect(Array.isArray(body.data)).toBe(true);
});

it('error details include the limit field', async () => {
jest.spyOn(creatorsUtils, 'fetchCreatorList');

const req = makeReq({ limit: '-1' });
const res = makeRes();
await httpListCreators(req, res, makeNext());

const body = res.json.mock.calls[0][0];
expect(body.data.length).toBeGreaterThan(0);
expect(body.data[0]).toHaveProperty('field');
expect(body.data[0].field).toBe('limit');
});

it('error message indicates invalid value', async () => {
jest.spyOn(creatorsUtils, 'fetchCreatorList');

const req = makeReq({ limit: '-1' });
const res = makeRes();
await httpListCreators(req, res, makeNext());

const body = res.json.mock.calls[0][0];
expect(body.data[0]).toHaveProperty('message');
expect(body.data[0].message).toBeDefined();
});

it('response does not include items array for negative limit', async () => {
jest.spyOn(creatorsUtils, 'fetchCreatorList');

const req = makeReq({ limit: '-1' });
const res = makeRes();
await httpListCreators(req, res, makeNext());

const body = res.json.mock.calls[0][0];
expect(body.data?.items).toBeUndefined();
});
});
21 changes: 21 additions & 0 deletions src/modules/creator/creator-profile.handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,27 @@ export async function upsertCreatorProfileHandler(req: Request, res: Response) {

const bodyResult = UpsertCreatorProfileBodySchema.safeParse(req.body);
if (!bodyResult.success) {
// Log missing required fields with structured context
const missingFields = bodyResult.error.issues
.filter(
(issue: any) =>
issue.code === 'invalid_type' &&
issue.received === 'undefined'
)
.map((issue: any) => issue.path.join('.'));

if (missingFields.length > 0) {
logger.warn(
{
type: 'creator_profile_validation_error',
handler: 'upsertCreatorProfileHandler',
missingFields,
...(req.requestId ? { requestId: req.requestId } : {}),
},
'Missing required fields in creator profile payload'
);
}

return sendValidationError(
res,
'Invalid creator profile payload',
Expand Down
Loading
Loading