@@ -4,6 +4,10 @@ import request from 'supertest';
44
55import { createApp } from './app.js' ;
66import { InMemoryUsageEventsRepository } from './repositories/usageEventsRepository.js' ;
7+ import type { Api } from './db/schema.js' ;
8+ import type { ApiRepository , ApiListFilters } from './repositories/apiRepository.js' ;
9+ import type { Developer } from './db/schema.js' ;
10+ import type { DeveloperRepository } from './repositories/developerRepository.js' ;
711import { InMemoryApiRepository } from './repositories/apiRepository.js' ;
812
913const seedRepository = ( ) =>
@@ -55,6 +59,130 @@ const seedRepository = () =>
5559 } ,
5660 ] ) ;
5761
62+ const developerProfile : Developer = {
63+ id : 11 ,
64+ user_id : 'dev-1' ,
65+ name : 'Test Developer' ,
66+ website : null ,
67+ description : null ,
68+ category : null ,
69+ created_at : 1 ,
70+ updated_at : 1 ,
71+ } ;
72+
73+ const sampleApis : Api [ ] = [
74+ {
75+ id : 101 ,
76+ developer_id : 11 ,
77+ name : 'Search API' ,
78+ description : null ,
79+ base_url : 'https://search.example.com' ,
80+ logo_url : null ,
81+ category : 'search' ,
82+ status : 'active' ,
83+ created_at : 1 ,
84+ updated_at : 1 ,
85+ } ,
86+ {
87+ id : 102 ,
88+ developer_id : 11 ,
89+ name : 'Chat API' ,
90+ description : null ,
91+ base_url : 'https://chat.example.com' ,
92+ logo_url : null ,
93+ category : 'chat' ,
94+ status : 'active' ,
95+ created_at : 1 ,
96+ updated_at : 1 ,
97+ } ,
98+ {
99+ id : 103 ,
100+ developer_id : 11 ,
101+ name : 'Archived API' ,
102+ description : null ,
103+ base_url : 'https://archive.example.com' ,
104+ logo_url : null ,
105+ category : 'archive' ,
106+ status : 'archived' ,
107+ created_at : 1 ,
108+ updated_at : 1 ,
109+ } ,
110+ ] ;
111+
112+ class FakeApiRepository implements ApiRepository {
113+ constructor ( private readonly apis : Api [ ] ) { }
114+
115+ async listByDeveloper ( developerId : number , filters : ApiListFilters = { } ) : Promise < Api [ ] > {
116+ let results = this . apis . filter ( ( api ) => api . developer_id === developerId ) ;
117+ if ( filters . status ) {
118+ results = results . filter ( ( api ) => api . status === filters . status ) ;
119+ }
120+ if ( typeof filters . offset === 'number' ) {
121+ results = results . slice ( filters . offset ) ;
122+ }
123+ if ( typeof filters . limit === 'number' ) {
124+ results = results . slice ( 0 , filters . limit ) ;
125+ }
126+ return results ;
127+ }
128+ }
129+
130+ const createDeveloperRepository = ( profile ?: Developer ) : DeveloperRepository => ( {
131+ async findByUserId ( userId : string ) {
132+ if ( profile && profile . user_id === userId ) {
133+ return profile ;
134+ }
135+ return undefined ;
136+ } ,
137+ } ) ;
138+
139+ const usageEventsForApis = ( ) =>
140+ new InMemoryUsageEventsRepository ( [
141+ {
142+ id : 'evt-search-1' ,
143+ developerId : 'dev-1' ,
144+ apiId : '101' ,
145+ endpoint : '/v1/search' ,
146+ userId : 'user-a' ,
147+ occurredAt : new Date ( '2026-02-01T01:00:00.000Z' ) ,
148+ revenue : 100n ,
149+ } ,
150+ {
151+ id : 'evt-search-2' ,
152+ developerId : 'dev-1' ,
153+ apiId : '101' ,
154+ endpoint : '/v1/search' ,
155+ userId : 'user-b' ,
156+ occurredAt : new Date ( '2026-02-01T02:00:00.000Z' ) ,
157+ revenue : 200n ,
158+ } ,
159+ {
160+ id : 'evt-chat-1' ,
161+ developerId : 'dev-1' ,
162+ apiId : '102' ,
163+ endpoint : '/v1/send' ,
164+ userId : 'user-c' ,
165+ occurredAt : new Date ( '2026-02-02T01:00:00.000Z' ) ,
166+ revenue : 150n ,
167+ } ,
168+ {
169+ id : 'evt-other' ,
170+ developerId : 'dev-2' ,
171+ apiId : '101' ,
172+ endpoint : '/v1/search' ,
173+ userId : 'user-z' ,
174+ occurredAt : new Date ( '2026-02-03T01:00:00.000Z' ) ,
175+ revenue : 999n ,
176+ } ,
177+ ] ) ;
178+
179+ const createDeveloperApisApp = ( ) =>
180+ createApp ( {
181+ usageEventsRepository : usageEventsForApis ( ) ,
182+ developerRepository : createDeveloperRepository ( developerProfile ) ,
183+ apiRepository : new FakeApiRepository ( sampleApis ) ,
184+ } ) ;
185+
58186test ( 'GET /api/developers/analytics returns 401 when unauthenticated' , async ( ) => {
59187 const app = createApp ( { usageEventsRepository : seedRepository ( ) } ) ;
60188 const response = await request ( app ) . get ( '/api/developers/analytics' ) ;
@@ -136,6 +264,51 @@ test('GET /api/developers/analytics filters by apiId and blocks non-owned API',
136264 assert . equal ( blocked . status , 403 ) ;
137265} ) ;
138266
267+ test ( 'GET /api/developers/apis returns 401 when unauthenticated' , async ( ) => {
268+ const response = await request ( createDeveloperApisApp ( ) ) . get ( '/api/developers/apis' ) ;
269+ assert . equal ( response . status , 401 ) ;
270+ } ) ;
271+
272+ test ( 'GET /api/developers/apis returns 404 when developer profile is missing' , async ( ) => {
273+ const app = createApp ( {
274+ usageEventsRepository : usageEventsForApis ( ) ,
275+ developerRepository : createDeveloperRepository ( undefined ) ,
276+ apiRepository : new FakeApiRepository ( sampleApis ) ,
277+ } ) ;
278+ const response = await request ( app ) . get ( '/api/developers/apis' ) . set ( 'x-user-id' , 'dev-1' ) ;
279+ assert . equal ( response . status , 404 ) ;
280+ } ) ;
281+
282+ test ( 'GET /api/developers/apis validates status query parameter' , async ( ) => {
283+ const response = await request ( createDeveloperApisApp ( ) )
284+ . get ( '/api/developers/apis?status=unknown' )
285+ . set ( 'x-user-id' , 'dev-1' ) ;
286+ assert . equal ( response . status , 400 ) ;
287+ } ) ;
288+
289+ test ( 'GET /api/developers/apis lists APIs with stats, filters, and pagination' , async ( ) => {
290+ const app = createDeveloperApisApp ( ) ;
291+ const fullResponse = await request ( app ) . get ( '/api/developers/apis' ) . set ( 'x-user-id' , 'dev-1' ) ;
292+ assert . equal ( fullResponse . status , 200 ) ;
293+ assert . deepEqual ( fullResponse . body . data , [
294+ { id : 101 , name : 'Search API' , status : 'active' , callCount : 2 , revenue : '300' } ,
295+ { id : 102 , name : 'Chat API' , status : 'active' , callCount : 1 , revenue : '150' } ,
296+ { id : 103 , name : 'Archived API' , status : 'archived' , callCount : 0 } ,
297+ ] ) ;
298+
299+ const limited = await request ( app )
300+ . get ( '/api/developers/apis?limit=1&offset=1' )
301+ . set ( 'x-user-id' , 'dev-1' ) ;
302+ assert . deepEqual ( limited . body . data , [
303+ { id : 102 , name : 'Chat API' , status : 'active' , callCount : 1 , revenue : '150' } ,
304+ ] ) ;
305+
306+ const filtered = await request ( app )
307+ . get ( '/api/developers/apis?status=archived' )
308+ . set ( 'x-user-id' , 'dev-1' ) ;
309+ assert . deepEqual ( filtered . body . data , [
310+ { id : 103 , name : 'Archived API' , status : 'archived' , callCount : 0 } ,
311+ ] ) ;
139312// ── GET /api/apis/:id ────────────────────────────────────────────────────────
140313
141314const buildApiRepo = ( ) => {
0 commit comments