|
1 | 1 | /* eslint-env jest */ |
2 | 2 |
|
3 | 3 | const { Op } = require('sequelize'); |
| 4 | +const realHttps = require('https'); |
4 | 5 |
|
5 | 6 | jest.mock('fs'); |
6 | 7 | jest.mock('child_process'); |
@@ -2500,6 +2501,41 @@ describe('ChannelModule', () => { |
2500 | 2501 | autoDownloadEnabledTabs: 'video,short' |
2501 | 2502 | }); |
2502 | 2503 | }); |
| 2504 | + |
| 2505 | + test('should fallback to videos tab when all RSS checks fail', async () => { |
| 2506 | + const mockChannel = { |
| 2507 | + ...mockChannelData, |
| 2508 | + available_tabs: null, // Not yet populated |
| 2509 | + auto_download_enabled_tabs: null |
| 2510 | + }; |
| 2511 | + Channel.findOne.mockResolvedValue(mockChannel); |
| 2512 | + Channel.update.mockResolvedValue([1]); |
| 2513 | + |
| 2514 | + // Mock fetch to always reject (simulating network timeout/failure) |
| 2515 | + const originalFetch = global.fetch; |
| 2516 | + global.fetch = jest.fn().mockRejectedValue(new Error('Network timeout')); |
| 2517 | + |
| 2518 | + try { |
| 2519 | + const result = await ChannelModule.detectAndSaveChannelTabs('UC123'); |
| 2520 | + |
| 2521 | + // Should fallback to videos tab |
| 2522 | + expect(result).toEqual({ |
| 2523 | + availableTabs: ['videos'], |
| 2524 | + autoDownloadEnabledTabs: 'video' |
| 2525 | + }); |
| 2526 | + |
| 2527 | + // Should save the fallback to the database |
| 2528 | + expect(Channel.update).toHaveBeenCalledWith( |
| 2529 | + expect.objectContaining({ |
| 2530 | + available_tabs: 'videos', |
| 2531 | + auto_download_enabled_tabs: 'video' |
| 2532 | + }), |
| 2533 | + expect.anything() |
| 2534 | + ); |
| 2535 | + } finally { |
| 2536 | + global.fetch = originalFetch; |
| 2537 | + } |
| 2538 | + }); |
2503 | 2539 | }); |
2504 | 2540 |
|
2505 | 2541 | describe('buildRssFeedUrl', () => { |
@@ -2534,7 +2570,7 @@ describe('ChannelModule', () => { |
2534 | 2570 | expect(exists).toBe(true); |
2535 | 2571 | expect(global.fetch).toHaveBeenCalledWith( |
2536 | 2572 | 'https://www.youtube.com/feeds/videos.xml?playlist_id=UULF123', |
2537 | | - { method: 'GET' } |
| 2573 | + expect.objectContaining({ method: 'GET', signal: expect.any(AbortSignal) }) |
2538 | 2574 | ); |
2539 | 2575 | } finally { |
2540 | 2576 | global.fetch = originalFetch; |
@@ -2969,4 +3005,95 @@ describe('ChannelModule', () => { |
2969 | 3005 | expect(ChannelModule.resizeChannelThumbnail).toHaveBeenCalledWith(channelId); |
2970 | 3006 | }); |
2971 | 3007 | }); |
| 3008 | + |
| 3009 | + describe('downloadChannelThumbnailFromUrl', () => { |
| 3010 | + let fsExtra; |
| 3011 | + let mockWriteStream; |
| 3012 | + let mockRequest; |
| 3013 | + let mockResponse; |
| 3014 | + let originalGet; |
| 3015 | + let originalCreateWriteStream; |
| 3016 | + |
| 3017 | + beforeEach(() => { |
| 3018 | + originalGet = realHttps.get; |
| 3019 | + fsExtra = require('fs-extra'); |
| 3020 | + originalCreateWriteStream = fsExtra.createWriteStream; |
| 3021 | + |
| 3022 | + mockWriteStream = { |
| 3023 | + on: jest.fn(), |
| 3024 | + close: jest.fn(), |
| 3025 | + }; |
| 3026 | + fsExtra.createWriteStream = jest.fn().mockReturnValue(mockWriteStream); |
| 3027 | + |
| 3028 | + mockRequest = { |
| 3029 | + on: jest.fn().mockReturnThis(), |
| 3030 | + destroy: jest.fn(), |
| 3031 | + }; |
| 3032 | + |
| 3033 | + mockResponse = { |
| 3034 | + statusCode: 200, |
| 3035 | + pipe: jest.fn(), |
| 3036 | + headers: {}, |
| 3037 | + }; |
| 3038 | + }); |
| 3039 | + |
| 3040 | + afterEach(() => { |
| 3041 | + realHttps.get = originalGet; |
| 3042 | + fsExtra.createWriteStream = originalCreateWriteStream; |
| 3043 | + }); |
| 3044 | + |
| 3045 | + test('should pass timeout option to protocol.get', async () => { |
| 3046 | + realHttps.get = jest.fn((url, opts, cb) => { |
| 3047 | + cb(mockResponse); |
| 3048 | + const finishCb = mockWriteStream.on.mock.calls.find(c => c[0] === 'finish')[1]; |
| 3049 | + finishCb(); |
| 3050 | + return mockRequest; |
| 3051 | + }); |
| 3052 | + |
| 3053 | + await ChannelModule.downloadChannelThumbnailFromUrl('https://example.com/thumb.jpg', 'UC123'); |
| 3054 | + |
| 3055 | + expect(realHttps.get).toHaveBeenCalledWith( |
| 3056 | + 'https://example.com/thumb.jpg', |
| 3057 | + expect.objectContaining({ timeout: 15000 }), |
| 3058 | + expect.any(Function) |
| 3059 | + ); |
| 3060 | + }); |
| 3061 | + |
| 3062 | + test('should reject and clean up partial file on timeout', async () => { |
| 3063 | + fsExtra.existsSync = jest.fn().mockReturnValue(true); |
| 3064 | + fsExtra.unlinkSync = jest.fn(); |
| 3065 | + |
| 3066 | + realHttps.get = jest.fn(() => { |
| 3067 | + return mockRequest; |
| 3068 | + }); |
| 3069 | + |
| 3070 | + const promise = ChannelModule.downloadChannelThumbnailFromUrl('https://example.com/thumb.jpg', 'UC123'); |
| 3071 | + |
| 3072 | + const timeoutHandler = mockRequest.on.mock.calls.find(c => c[0] === 'timeout')[1]; |
| 3073 | + timeoutHandler(); |
| 3074 | + |
| 3075 | + await expect(promise).rejects.toThrow('Thumbnail download timed out'); |
| 3076 | + expect(mockRequest.destroy).toHaveBeenCalled(); |
| 3077 | + expect(mockWriteStream.close).toHaveBeenCalled(); |
| 3078 | + expect(fsExtra.unlinkSync).toHaveBeenCalled(); |
| 3079 | + }); |
| 3080 | + |
| 3081 | + test('should reject on network error and clean up', async () => { |
| 3082 | + fsExtra.existsSync = jest.fn().mockReturnValue(true); |
| 3083 | + fsExtra.unlinkSync = jest.fn(); |
| 3084 | + |
| 3085 | + realHttps.get = jest.fn(() => { |
| 3086 | + return mockRequest; |
| 3087 | + }); |
| 3088 | + |
| 3089 | + const promise = ChannelModule.downloadChannelThumbnailFromUrl('https://example.com/thumb.jpg', 'UC123'); |
| 3090 | + |
| 3091 | + const errorHandler = mockRequest.on.mock.calls.find(c => c[0] === 'error')[1]; |
| 3092 | + errorHandler(new Error('ECONNREFUSED')); |
| 3093 | + |
| 3094 | + await expect(promise).rejects.toThrow('ECONNREFUSED'); |
| 3095 | + expect(mockWriteStream.close).toHaveBeenCalled(); |
| 3096 | + expect(fsExtra.unlinkSync).toHaveBeenCalled(); |
| 3097 | + }); |
| 3098 | + }); |
2972 | 3099 | }); |
0 commit comments