@@ -228,4 +228,120 @@ describe('Feed API', () => {
228228 } ) ;
229229 } ) ;
230230 } ) ;
231+
232+ describe ( 'GET /feed/:serviceId' , ( ) => {
233+ const SERVICE = 'service_without_history' ;
234+ const OTHER_SERVICE = 'service_with_history' ;
235+ const TERMS = 'Terms of Service' ;
236+
237+ let repository ;
238+
239+ before ( async function ( ) {
240+ this . timeout ( 5000 ) ;
241+ repository = RepositoryFactory . create ( storageConfig ) ;
242+ await repository . initialize ( ) ;
243+
244+ await repository . save ( new Version ( {
245+ serviceId : SERVICE ,
246+ termsType : TERMS ,
247+ content : 'c1' ,
248+ fetchDate : new Date ( '2024-01-01T00:00:00Z' ) ,
249+ snapshotIds : [ 's1' ] ,
250+ } ) ) ;
251+ await repository . save ( new Version ( {
252+ serviceId : SERVICE ,
253+ termsType : TERMS ,
254+ content : 'c2' ,
255+ fetchDate : new Date ( '2024-02-01T00:00:00Z' ) ,
256+ snapshotIds : [ 's2' ] ,
257+ } ) ) ;
258+ await repository . save ( new Version ( {
259+ serviceId : OTHER_SERVICE ,
260+ termsType : TERMS ,
261+ content : 'c3' ,
262+ fetchDate : new Date ( '2024-03-01T00:00:00Z' ) ,
263+ snapshotIds : [ 's3' ] ,
264+ } ) ) ;
265+ } ) ;
266+
267+ after ( ( ) => repository . removeAll ( ) ) ;
268+
269+ context ( 'when the service exists and has versions' , ( ) => {
270+ let response ;
271+
272+ before ( async ( ) => {
273+ response = await request . get ( `${ basePath } /v1/feed/${ encodeURIComponent ( SERVICE ) } ` ) ;
274+ } ) ;
275+
276+ it ( 'responds with 200' , ( ) => {
277+ expect ( response . status ) . to . equal ( 200 ) ;
278+ } ) ;
279+
280+ it ( 'responds with Content-Type application/atom+xml' , ( ) => {
281+ expect ( response . headers [ 'content-type' ] ) . to . match ( / ^ a p p l i c a t i o n \/ a t o m \+ x m l / ) ;
282+ } ) ;
283+
284+ it ( 'includes only entries for that service' , ( ) => {
285+ const serviceTerms = [ ...response . text . matchAll ( / s c h e m e = " t a g : o p e n t e r m s a r c h i v e .o r g , 2 0 2 6 : s c h e m e : s e r v i c e " [ ^ / ] * t e r m = " ( [ ^ " ] + ) " / g) ]
286+ . concat ( [ ...response . text . matchAll ( / t e r m = " ( [ ^ " ] + ) " [ ^ / ] * s c h e m e = " t a g : o p e n t e r m s a r c h i v e .o r g , 2 0 2 6 : s c h e m e : s e r v i c e " / g) ] )
287+ . map ( match => match [ 1 ] ) ;
288+
289+ expect ( serviceTerms ) . to . not . be . empty ;
290+
291+ for ( const term of serviceTerms ) {
292+ expect ( term ) . to . equal ( SERVICE ) ;
293+ }
294+ } ) ;
295+
296+ it ( 'has a feed id including the service id' , ( ) => {
297+ expect ( extractTag ( response . text , 'id' ) ) . to . equal ( `tag:opentermsarchive.org,2026:feed:test:${ SERVICE } ` ) ;
298+ } ) ;
299+
300+ it ( 'has a self link pointing to the service-scoped feed endpoint' , ( ) => {
301+ const href = response . text . match ( / < l i n k [ ^ > ] * r e l = " s e l f " [ ^ > ] * h r e f = " ( [ ^ " ] + ) " / ) [ 1 ] ;
302+
303+ expect ( href ) . to . match ( new RegExp ( `/feed/${ SERVICE } $` ) ) ;
304+ } ) ;
305+ } ) ;
306+
307+ context ( 'when the service exists but has no versions' , ( ) => {
308+ let response ;
309+
310+ before ( async ( ) => {
311+ response = await request . get ( `${ basePath } /v1/feed/${ encodeURIComponent ( 'service_with_filters_history' ) } ` ) ;
312+ } ) ;
313+
314+ it ( 'responds with 200' , ( ) => {
315+ expect ( response . status ) . to . equal ( 200 ) ;
316+ } ) ;
317+
318+ it ( 'returns an empty feed (no entries)' , ( ) => {
319+ expect ( response . text ) . to . not . include ( '<entry>' ) ;
320+ } ) ;
321+ } ) ;
322+
323+ context ( 'when the service does not exist' , ( ) => {
324+ let response ;
325+
326+ before ( async ( ) => {
327+ response = await request . get ( `${ basePath } /v1/feed/DoesNotExist` ) ;
328+ } ) ;
329+
330+ it ( 'responds with 404' , ( ) => {
331+ expect ( response . status ) . to . equal ( 404 ) ;
332+ } ) ;
333+ } ) ;
334+
335+ context ( 'when the serviceId uses different casing' , ( ) => {
336+ let response ;
337+
338+ before ( async ( ) => {
339+ response = await request . get ( `${ basePath } /v1/feed/${ encodeURIComponent ( SERVICE . toUpperCase ( ) ) } ` ) ;
340+ } ) ;
341+
342+ it ( 'still resolves to the service (case-insensitive)' , ( ) => {
343+ expect ( response . status ) . to . equal ( 200 ) ;
344+ } ) ;
345+ } ) ;
346+ } ) ;
231347} ) ;
0 commit comments