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
174 changes: 174 additions & 0 deletions src/modules/creators/creator-public-profile.controller.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// src/modules/creators/creator-public-profile.controller.test.ts
// Unit tests for public creator profile controller (#1009).

import { Request, Response } from 'express';
import {
httpGetCreatorPublicProfile,
httpGetCreatorIssuedKeys,
} from './creator-public-profile.controller';
import * as service from './creator-public-profile.service';

jest.mock('./creator-public-profile.service');

describe('Creator Public Profile Controllers (#1009)', () => {
const validWallet = 'G' + 'A'.repeat(55);
const unregisteredWallet = 'G' + 'B'.repeat(55);

let req: Partial<Request>;
let res: Partial<Response>;
let next: jest.Mock;

beforeEach(() => {
jest.clearAllMocks();
req = {
params: {},
query: {},
};
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
set: jest.fn().mockReturnThis(),
};
next = jest.fn();
});

describe('httpGetCreatorPublicProfile', () => {
it('returns 400 validation error when wallet address is invalid format', async () => {
req.params = { wallet: 'invalid-wallet-format' };

await httpGetCreatorPublicProfile(req as Request, res as Response, next);

expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
error: expect.objectContaining({
code: 'VALIDATION_ERROR',
message: 'Invalid wallet address',
}),
})
);
});

it('returns 404 when wallet is unregistered (not found in DB)', async () => {
req.params = { wallet: unregisteredWallet };
(service.getCreatorPublicProfile as jest.Mock).mockResolvedValue(null);

await httpGetCreatorPublicProfile(req as Request, res as Response, next);

expect(res.status).toHaveBeenCalledWith(404);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
error: expect.objectContaining({
code: 'NOT_FOUND',
}),
})
);
});

it('returns 200 with profile data for registered wallet', async () => {
req.params = { wallet: validWallet };
const mockProfile = {
wallet: validWallet,
memberSince: '2026-01-01T00:00:00.000Z',
totalKeys: 2,
totalVolume: '5000',
avgRating: 4.8,
isVerified: true,
handle: 'test_creator',
displayName: 'Test Creator',
bio: 'Bio',
avatarUrl: null,
stats: {
totalHolders: 10,
totalTradingVolume: '5000',
followersCount: 20,
},
};
(service.getCreatorPublicProfile as jest.Mock).mockResolvedValue(mockProfile);

await httpGetCreatorPublicProfile(req as Request, res as Response, next);

expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: true,
data: mockProfile,
})
);
expect(res.set).toHaveBeenCalledWith('X-Response-Timestamp', expect.any(String));
});
});

describe('httpGetCreatorIssuedKeys', () => {
it('returns 400 when pagination query parameter types are invalid', async () => {
req.params = { wallet: validWallet };
req.query = { limit: ['10', '20'] as any };

await httpGetCreatorIssuedKeys(req as Request, res as Response, next);

expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: false,
error: expect.objectContaining({
code: 'VALIDATION_ERROR',
message: 'Invalid keys pagination parameters',
}),
})
);
});

it('returns 404 when wallet is not found', async () => {
req.params = { wallet: unregisteredWallet };
(service.getCreatorDeployedKeys as jest.Mock).mockResolvedValue(null);

await httpGetCreatorIssuedKeys(req as Request, res as Response, next);

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

it('returns 200 with paginated keys in deployment order', async () => {
req.params = { wallet: validWallet };
req.query = { limit: '20' };
const mockPage = {
items: [
{
id: 'key-1',
handle: 'key1',
displayName: 'Key 1',
bio: null,
avatarUrl: null,
isVerified: true,
holderCount: 5,
totalTradingVolume: '1000',
status: 'ACTIVE',
price: '100',
createdAt: '2026-01-01T00:00:00.000Z',
},
],
nextCursor: null,
hasMore: false,
limit: 20,
stats: {
totalKeys: 1,
totalHolders: 5,
totalTradingVolume: '1000',
},
};
(service.getCreatorDeployedKeys as jest.Mock).mockResolvedValue(mockPage);

await httpGetCreatorIssuedKeys(req as Request, res as Response, next);

expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith(
expect.objectContaining({
success: true,
data: mockPage,
})
);
});
});
});
55 changes: 30 additions & 25 deletions src/modules/creators/creator-public-profile.controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// src/modules/creators/creator-public-profile.controller.ts
// HTTP controllers for the public creator profile endpoints (#900).
// HTTP controllers for the public creator profile endpoints (#1009).

import { AsyncController } from '../../types/auth.types';
import {
Expand All @@ -10,27 +10,34 @@ import {
import { attachTimestampHeader } from '../../utils/timestamp-headers.utils';
import {
getCreatorPublicProfile,
getCreatorIssuedKeys,
getCreatorDeployedKeys,
} from './creator-public-profile.service';

const StellarWalletAddressRegex = /^G[A-Z2-7]{55}$/;

/**
* GET /api/v1/creators/:id
*
* Returns the public profile for a creator together with social stats
* (total holders, total trading volume, followers count).
* GET /api/v1/creators/:wallet
*
* No authentication required. Resolves by creator ID or handle.
* Returns the public profile for a creator wallet with memberSince, totalKeys,
* totalVolume, avgRating, and stats aggregated across all their deployed keys.
* Verified badge derived from KYC status. Returns 404 for unregistered wallets.
*/
export const httpGetCreatorPublicProfile: AsyncController = async (
req,
res,
next
) => {
try {
const rawId = req.params.id;
const creatorId = Array.isArray(rawId) ? rawId[0] : rawId;
const rawWallet = req.params.wallet ?? req.params.id;
const wallet = Array.isArray(rawWallet) ? rawWallet[0] : rawWallet;

if (!wallet || !StellarWalletAddressRegex.test(wallet)) {
return sendValidationError(res, 'Invalid wallet address', [
{ field: 'wallet', message: 'A valid Stellar wallet address is required' },
]);
}

const profile = await getCreatorPublicProfile(creatorId);
const profile = await getCreatorPublicProfile(wallet);
if (!profile) {
return sendNotFound(res, 'Creator');
}
Expand All @@ -43,31 +50,29 @@ export const httpGetCreatorPublicProfile: AsyncController = async (
};

/**
* GET /api/v1/creators/:id/keys
* GET /api/v1/creators/:wallet/keys
*
* Returns a cursor-paginated list of keys (CreatorProfile records) issued by
* the same user who owns the given creator profile, together with aggregate
* stats across all their keys.
*
* Query params:
* - limit (optional, 1–100, default 20)
* - cursor (optional, opaque pagination cursor)
*
* No authentication required. Resolves by creator ID or handle.
* Returns a cursor-paginated list of deployed keys for a creator wallet in
* deployment order with status and price.
*/
export const httpGetCreatorIssuedKeys: AsyncController = async (
req,
res,
next
) => {
try {
const rawId = req.params.id;
const creatorId = Array.isArray(rawId) ? rawId[0] : rawId;
const rawWallet = req.params.wallet ?? req.params.id;
const wallet = Array.isArray(rawWallet) ? rawWallet[0] : rawWallet;

if (!wallet || !StellarWalletAddressRegex.test(wallet)) {
return sendValidationError(res, 'Invalid wallet address', [
{ field: 'wallet', message: 'A valid Stellar wallet address is required' },
]);
}

const rawLimit = req.query.limit;
const rawCursor = req.query.cursor;

// Reject repeated query params
if (
(rawLimit !== undefined && typeof rawLimit !== 'string') ||
(rawCursor !== undefined && typeof rawCursor !== 'string')
Expand All @@ -84,8 +89,8 @@ export const httpGetCreatorIssuedKeys: AsyncController = async (
);
}

const page = await getCreatorIssuedKeys(
creatorId,
const page = await getCreatorDeployedKeys(
wallet,
rawLimit,
rawCursor
);
Expand Down
Loading
Loading