process.env.CURR_DATE = '2026-06-08' const { channels, parser, request, url } = require('./sky.com.config.js') const fs = require('fs') const path = require('path') const axios = require('axios') const dayjs = require('dayjs') const utc = require('dayjs/plugin/utc') const customParseFormat = require('dayjs/plugin/customParseFormat') dayjs.extend(customParseFormat) dayjs.extend(utc) jest.mock('axios') const date = dayjs.utc('2026-06-08').startOf('d') const channel = { lang: 'en', name: 'Sky History HD', site_id: 'GB#4086', xmltv_id: 'SkyHistory.uk@HD' } const uhdChannel = { lang: 'en', name: 'TNTUltimateUHD', site_id: 'GB#1336', xmltv_id: 'TNTSportsUltimate.uk@SD' } function mockScheduleRequest(url) { const urls = { 20260608: 'content1.json', 20260609: 'content2.json' } let data = '' 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() } return Promise.resolve({ data, headers: {}, request: {} }) } beforeEach(() => { jest.clearAllMocks() axios.get.mockImplementation(mockScheduleRequest) }) it('can generate valid url', () => { 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' ) }) 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) }) }) it('rejects an unsupported territory in the site ID', () => { expect(() => url({ channel: { ...channel, site_id: 'XX#4086' }, date })).toThrow( 'Expected "#"' ) }) 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' }) expect(request.headers({ channel: uhdChannel })).toEqual({ 'X-SkyOTT-Proposition': 'SKYQ', 'X-SkyOTT-Provider': 'SKY', 'X-SkyOTT-Territory': 'GB' }) expect(request.cache.vary).toEqual(['X-SkyOTT-Territory']) }) it('can parse response', async () => { const content = fs.readFileSync(path.join(__dirname, '__data__', 'content1.json')) const result = (await parser({ config: { days: 1 }, content, channel, date })).map(p => { p.start = p.start.toJSON() p.stop = p.stop.toJSON() return p }) expect(result.length).toBe(30) expect(result[0]).toMatchObject({ start: '2026-06-07T23:45:00.000Z', stop: '2026-06-08T00:45:00.000Z', title: 'The UnBelievable With Dan Aykroyd', description: "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)", season: 2, episode: 6, 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' }) expect(result[29]).toMatchObject({ start: '2026-06-08T23:00:00.000Z', stop: '2026-06-09T00:00:00.000Z', title: 'Digging For Britain', description: "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)", season: 1, 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' }) expect(axios.get).toHaveBeenCalledWith(url({ channel, date: date.add(1, 'd') }), { headers: { 'X-SkyOTT-Territory': 'GB' } }) }) 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) expect(axios.get).toHaveBeenCalledWith(url({ channel, date: date.add(2, 'd') }), { headers: { 'X-SkyOTT-Territory': 'GB' } }) }) 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) expect(axios.get).toHaveBeenCalledWith(url({ channel, date: finalDate.add(1, 'd') }), { headers: { 'X-SkyOTT-Territory': 'GB' } }) }) 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' } }) }) it('can handle empty guide', async () => { const result = await parser({ config: { days: 2 }, date, channel, content: '' }) expect(result).toMatchObject([]) }) 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: {} }) } 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: {} }) } return Promise.resolve({ data: { services: [ { sid: '123', t: titles[territory] }, { sid: '123', t: titles[territory] }, ...(territory === 'IT' ? [{ sid: '582' }] : []) ] }, headers: {}, request: {} }) }) 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) expect(axios.get.mock.calls.map(([, options]) => options.headers)).toEqual( expect.arrayContaining([ { 'X-SkyOTT-Territory': 'GB' }, { 'X-SkyOTT-Territory': 'DE' }, { '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' } ]) ) }) 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') })