2026-07-29 18:08:51 +02:00
process . env . CURR_DATE = '2026-06-08'
const { channels , parser , request , url } = require ( './sky.com.config.js' )
2025-09-28 17:55:05 +03:00
const fs = require ( 'fs' )
const path = require ( 'path' )
2026-06-08 19:04:43 +07:00
const axios = require ( 'axios' )
2025-09-28 17:55:05 +03:00
const dayjs = require ( 'dayjs' )
const utc = require ( 'dayjs/plugin/utc' )
const customParseFormat = require ( 'dayjs/plugin/customParseFormat' )
dayjs . extend ( customParseFormat )
dayjs . extend ( utc )
2026-06-08 19:04:43 +07:00
jest . mock ( 'axios' )
const date = dayjs . utc ( '2026-06-08' ). startOf ( 'd' )
2025-09-28 17:55:05 +03:00
const channel = {
2026-07-29 18:08:51 +02:00
lang : 'en' ,
2026-07-29 22:18:15 +02:00
name : 'Sky History HD' ,
2026-07-29 18:08:51 +02:00
site_id : 'GB#4086' ,
2026-06-08 19:04:43 +07:00
xmltv_id : 'SkyHistory.uk@HD'
2025-09-28 17:55:05 +03:00
}
2026-07-29 22:18:15 +02:00
const uhdChannel = {
lang : 'en' ,
name : 'TNTUltimateUHD' ,
site_id : 'GB#1336' ,
xmltv_id : 'TNTSportsUltimate.uk@SD'
}
2025-09-28 17:55:05 +03:00
2026-07-29 18:08:51 +02:00
function mockScheduleRequest ( url ) {
2026-06-08 19:04:43 +07:00
const urls = {
2026-07-29 22:18:15 +02:00
20260608 : 'content1.json' ,
20260609 : 'content2.json'
2026-06-08 19:04:43 +07:00
}
let data = ''
2026-07-29 18:08:51 +02:00
const match = url . match ( /\/schedule\/(\d{8})\/([^/]+)$/ )
if ( match && match [ 2 ]. split ( ',' ). includes ( '4086' ) && urls [ match [ 1 ]] !== undefined ) {
data = fs . readFileSync ( path . join ( __dirname , '__data__' , urls [ match [ 1 ]])). toString ()
2026-06-08 19:04:43 +07:00
}
2026-07-29 18:08:51 +02:00
return Promise . resolve ({ data , headers : {}, request : {} })
}
beforeEach (() => {
jest . clearAllMocks ()
axios . get . mockImplementation ( mockScheduleRequest )
2026-06-08 19:04:43 +07:00
})
2025-09-28 17:55:05 +03:00
it ( 'can generate valid url' , () => {
2026-07-29 18:08:51 +02:00
const result = url ({ channel , date })
const sids = result . split ( '/' ). pop (). split ( ',' )
expect ( result ). toContain ( 'https://awk.epgsky.com/hawk/linear/schedule/20260608/' )
expect ( sids ). toHaveLength ( 20 )
expect ( sids ). toContain ( '4086' )
expect ( new Set ( sids )). toHaveProperty ( 'size' , 20 )
const companion = {
... channel ,
site_id : `GB# ${ sids . find ( sid => sid !== '4086' ) } `
}
expect ( url ({ channel : companion , date })). toBe ( result )
})
it ( 'falls back to a single SID for a channel outside the repository XML' , () => {
expect ( url ({ channel : { ... channel , site_id : 'GB#999999' }, date })). toBe (
'https://awk.epgsky.com/hawk/linear/schedule/20260608/999999'
)
})
2026-07-29 22:18:15 +02:00
it . each ([
[ 'TNTUltimateUHD' , 'GB#1336' , '1336' ],
[ 'Ultra HD' , 'GB#7233' , '7233' ],
[ 'Sky Sport 4k' , 'IT#628' , '628' ]
])( 'uses Atlantis for %s' , ( name , siteId , sid ) => {
const currentChannel = {
... channel ,
name ,
site_id : siteId
}
expect ( url ({ channel : currentChannel , date })). toBe (
`https://atlantis.epgsky.com/as/schedule/20260608/ ${ sid } `
)
expect ( request . headers ({ channel : currentChannel })). toEqual ({
'X-SkyOTT-Proposition' : 'SKYQ' ,
'X-SkyOTT-Provider' : 'SKY' ,
'X-SkyOTT-Territory' : siteId . slice ( 0 , 2 )
})
})
2026-07-29 18:08:51 +02:00
it ( 'rejects an unsupported territory in the site ID' , () => {
expect (() => url ({ channel : { ... channel , site_id : 'XX#4086' }, date })). toThrow (
'Expected "<territory>#<sid>"'
)
})
it ( 'can generate territory headers from the site ID' , () => {
expect ( request . headers ({ channel })). toEqual ({
'X-SkyOTT-Territory' : 'GB'
})
expect ( request . headers ({ channel : { ... channel , lang : 'en' , site_id : 'DE#4086' } })). toEqual ({
'X-SkyOTT-Territory' : 'DE'
})
expect ( request . headers ({ channel : { ... channel , lang : 'it' , site_id : 'IT#4086' } })). toEqual ({
'X-SkyOTT-Territory' : 'IT'
})
2026-07-29 22:18:15 +02:00
expect ( request . headers ({ channel : uhdChannel })). toEqual ({
'X-SkyOTT-Proposition' : 'SKYQ' ,
'X-SkyOTT-Provider' : 'SKY' ,
'X-SkyOTT-Territory' : 'GB'
})
2026-07-29 18:08:51 +02:00
expect ( request . cache . vary ). toEqual ([ 'X-SkyOTT-Territory' ])
2025-09-28 17:55:05 +03:00
})
2026-06-08 19:04:43 +07:00
it ( 'can parse response' , async () => {
const content = fs . readFileSync ( path . join ( __dirname , '__data__' , 'content1.json' ))
2026-07-29 22:18:15 +02:00
const result = ( await parser ({ config : { days : 1 }, content , channel , date })). map ( p => {
p . start = p . start . toJSON ()
p . stop = p . stop . toJSON ()
return p
})
2025-09-28 17:55:05 +03:00
2026-06-08 19:04:43 +07:00
expect ( result . length ). toBe ( 30 )
2025-09-28 17:55:05 +03:00
expect ( result [ 0 ]). toMatchObject ({
2026-06-08 19:04:43 +07:00
start : '2026-06-07T23:45:00.000Z' ,
stop : '2026-06-08T00:45:00.000Z' ,
title : 'The UnBelievable With Dan Aykroyd' ,
2025-09-28 17:55:05 +03:00
description :
2026-07-29 22:18:15 +02:00
"Bizarre Innovations: Discover bizarre innovations like a fully functioning car that hovers in midair. Or clothing made out of everyone's favorite source of calcium. (S2, ep 6)" ,
2026-06-08 19:04:43 +07:00
season : 2 ,
2025-09-28 17:55:05 +03:00
episode : 6 ,
2026-06-08 19:04:43 +07:00
icon : 'https://images.metadata.sky.com/pd-image/007ade72-3239-47d7-a452-3070eb8e591d/16-9/640' ,
image : 'https://images.metadata.sky.com/pd-image/007ade72-3239-47d7-a452-3070eb8e591d/16-9/640'
2025-09-28 17:55:05 +03:00
})
2026-06-08 19:04:43 +07:00
expect ( result [ 29 ]). toMatchObject ({
start : '2026-06-08T23:00:00.000Z' ,
stop : '2026-06-09T00:00:00.000Z' ,
title : 'Digging For Britain' ,
2026-04-19 02:00:35 +03:00
description :
2026-07-29 22:18:15 +02:00
"The Tudors: Dr Alice Roberts pays tribute to the Bard, visiting Shakespeare's first theatre in London's Shoreditch and sifting through the poet's rubbish! (S1, ep 4)" ,
2026-04-19 02:00:35 +03:00
season : 1 ,
2026-06-08 19:04:43 +07:00
episode : 4 ,
icon : 'https://images.metadata.sky.com/pd-image/68152ae7-97d6-44c8-8a54-e78710b94a76/16-9/640' ,
image : 'https://images.metadata.sky.com/pd-image/68152ae7-97d6-44c8-8a54-e78710b94a76/16-9/640'
2026-04-19 02:00:35 +03:00
})
2026-07-29 22:18:15 +02:00
expect ( axios . get ). toHaveBeenCalledWith ( url ({ channel , date : date . add ( 1 , 'd' ) }), {
headers : {
'X-SkyOTT-Territory' : 'GB'
2026-07-29 18:08:51 +02:00
}
2026-07-29 22:18:15 +02:00
})
2026-07-29 18:08:51 +02:00
})
it ( 'only loads a follow-up batch for the final requested date' , async () => {
await parser ({
config : { days : 2 },
content : '' ,
channel ,
date
})
expect ( axios . get ). not . toHaveBeenCalled ()
await parser ({
config : { days : 2 },
content : '' ,
channel ,
date : date . add ( 1 , 'd' )
})
expect ( axios . get ). toHaveBeenCalledTimes ( 1 )
2026-07-29 22:18:15 +02:00
expect ( axios . get ). toHaveBeenCalledWith ( url ({ channel , date : date . add ( 2 , 'd' ) }), {
headers : {
'X-SkyOTT-Territory' : 'GB'
2026-07-29 18:08:51 +02:00
}
2026-07-29 22:18:15 +02:00
})
2026-07-29 18:08:51 +02:00
})
it ( 'loads a shared follow-up batch only once' , async () => {
const sids = url ({ channel , date }). split ( '/' ). pop (). split ( ',' )
const companion = {
... channel ,
site_id : `GB# ${ sids . find ( sid => sid !== '4086' ) } `
}
const finalDate = date . add ( 2 , 'd' )
await Promise . all ([
parser ({ config : { days : 3 }, content : '' , channel , date : finalDate }),
parser ({ config : { days : 3 }, content : '' , channel : companion , date : finalDate })
])
expect ( axios . get ). toHaveBeenCalledTimes ( 1 )
2026-07-29 22:18:15 +02:00
expect ( axios . get ). toHaveBeenCalledWith ( url ({ channel , date : finalDate . add ( 1 , 'd' ) }), {
headers : {
'X-SkyOTT-Territory' : 'GB'
2026-07-29 18:08:51 +02:00
}
2026-07-29 22:18:15 +02:00
})
})
it ( 'loads an UHD follow-up schedule from Atlantis' , async () => {
const finalDate = date . add ( 1 , 'd' )
await parser ({
config : { days : 2 },
content : '' ,
channel : uhdChannel ,
date : finalDate
})
expect ( axios . get ). toHaveBeenCalledTimes ( 1 )
expect ( axios . get ). toHaveBeenCalledWith ( 'https://atlantis.epgsky.com/as/schedule/20260610/1336' , {
headers : {
'X-SkyOTT-Proposition' : 'SKYQ' ,
'X-SkyOTT-Provider' : 'SKY' ,
'X-SkyOTT-Territory' : 'GB'
}
})
2025-09-28 17:55:05 +03:00
})
2026-06-08 19:04:43 +07:00
it ( 'can handle empty guide' , async () => {
const result = await parser ({
2026-07-29 18:08:51 +02:00
config : { days : 2 },
2025-09-28 17:55:05 +03:00
date ,
channel ,
content : ''
})
expect ( result ). toMatchObject ([])
})
2026-07-29 18:08:51 +02:00
it ( 'keeps identical event IDs isolated by territory' , async () => {
const content = JSON . stringify ({
schedule : [
{
sid : '123' ,
events : [
{
eid : 'shared-event' ,
st : date . unix (),
d : 3600 ,
t : 'Shared event'
}
]
}
]
})
const englishChannel = { lang : 'en' , site_id : 'GB#123' }
const germanChannel = { lang : 'de' , site_id : 'DE#123' }
expect ( await parser ({ content , channel : englishChannel , date })). toHaveLength ( 1 )
expect ( await parser ({ content , channel : germanChannel , date })). toHaveLength ( 1 )
expect ( await parser ({ content , channel : englishChannel , date })). toHaveLength ( 0 )
})
it ( 'can load and deduplicate all territories by language and site ID' , async () => {
axios . get . mockImplementation (( url , options ) => {
const territory = options . headers [ 'X-SkyOTT-Territory' ]
const titles = {
DE : 'Sky One Deutschland HD' ,
GB : 'Sky One HD' ,
IT : 'Sky Uno HD'
}
if ( url . endsWith ( '/regions' )) {
return Promise . resolve ({
data : {
regions : [
{ bouquetId : 1 , subBouquetId : 2 },
{ bouquetId : 1 , subBouquetId : 2 }
]
},
headers : {},
request : {}
})
}
2026-07-29 22:18:15 +02:00
if ( url . includes ( 'atlantis.epgsky.com' )) {
return Promise . resolve ({
data : {
services : [
{ schedule : true , sid : '123' , t : ` ${ titles [ territory ] } Atlantis` },
{ schedule : true , sid : '456' , t : ` ${ territory } UHD` },
{ schedule : false , sid : '789' , t : ` ${ territory } Internal` }
]
},
headers : {},
request : {}
})
}
2026-07-29 18:08:51 +02:00
return Promise . resolve ({
data : {
services : [
{ sid : '123' , t : titles [ territory ] },
{ sid : '123' , t : titles [ territory ] },
...( territory === 'IT' ? [{ sid : '582' }] : [])
]
},
headers : {},
request : {}
})
})
2026-07-29 22:18:15 +02:00
const result = await channels ()
expect ( result ). toHaveLength ( 6 )
expect ( result ). toEqual (
expect . arrayContaining ([
{
lang : 'de' ,
site_id : 'DE#123' ,
name : 'Sky One Deutschland HD'
},
{
lang : 'de' ,
site_id : 'DE#456' ,
name : 'DE UHD'
},
{
lang : 'en' ,
site_id : 'GB#123' ,
name : 'Sky One HD'
},
{
lang : 'en' ,
site_id : 'GB#456' ,
name : 'GB UHD'
},
{
lang : 'it' ,
site_id : 'IT#123' ,
name : 'Sky Uno HD'
},
{
lang : 'it' ,
site_id : 'IT#456' ,
name : 'IT UHD'
}
])
)
expect ( result ). not . toEqual (
expect . arrayContaining ([
expect . objectContaining ({
site_id : expect . stringMatching ( /#789$/ )
})
])
)
expect ( axios . get ). toHaveBeenCalledTimes ( 9 )
2026-07-29 18:08:51 +02:00
expect ( axios . get . mock . calls . map (([, options ]) => options . headers )). toEqual (
expect . arrayContaining ([
{ 'X-SkyOTT-Territory' : 'GB' },
{ 'X-SkyOTT-Territory' : 'DE' },
2026-07-29 22:18:15 +02:00
{ 'X-SkyOTT-Territory' : 'IT' },
{
'X-SkyOTT-Proposition' : 'SKYQ' ,
'X-SkyOTT-Provider' : 'SKY' ,
'X-SkyOTT-Territory' : 'GB'
},
{
'X-SkyOTT-Proposition' : 'SKYQ' ,
'X-SkyOTT-Provider' : 'SKY' ,
'X-SkyOTT-Territory' : 'DE'
},
{
'X-SkyOTT-Proposition' : 'SKYQ' ,
'X-SkyOTT-Provider' : 'SKY' ,
'X-SkyOTT-Territory' : 'IT'
}
2026-07-29 18:08:51 +02:00
])
)
})
it ( 'rejects unsupported channel territories' , () => {
expect (() => request . headers ({ channel : { ... channel , site_id : 'FR#4086' } })). toThrow (
'territory "DE", "GB" or "IT"'
)
})
it ( 'rejects an incomplete channel list' , async () => {
axios . get . mockImplementation (( url , options ) => {
if ( url . endsWith ( '/regions' )) {
return Promise . resolve ({
data : {
regions : [{ bouquetId : 1 , subBouquetId : 2 }]
},
headers : {},
request : {}
})
}
if ( options . headers [ 'X-SkyOTT-Territory' ] === 'DE' ) {
return Promise . resolve ({
data : {
services : [{ sid : '123' , t : 'Sky One Deutschland HD' }]
},
headers : {},
request : {}
})
}
return Promise . resolve ({
data : {},
headers : {},
request : {}
})
})
await expect ( channels ()). rejects . toThrow ( 'Unable to load complete Sky channel list' )
})