download.test.js
1 import * as os from 'node:os'; 2 import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; 3 import { getRegistry } from '@jackwener/opencli/registry'; 4 import { ArgumentError, AuthRequiredError, CommandExecutionError } from '@jackwener/opencli/errors'; 5 const { mockHttpDownload, logSpy } = vi.hoisted(() => ({ 6 mockHttpDownload: vi.fn(), 7 logSpy: vi.spyOn(console, 'log').mockImplementation(() => undefined), 8 })); 9 vi.mock('@jackwener/opencli/download', async () => { 10 const actual = await vi.importActual('@jackwener/opencli/download'); 11 return { ...actual, httpDownload: mockHttpDownload }; 12 }); 13 const { buildInstagramDownloadItems, parseInstagramMediaTarget, } = await import('./download.js'); 14 let cmd; 15 beforeAll(() => { 16 cmd = getRegistry().get('instagram/download'); 17 expect(cmd?.func).toBeTypeOf('function'); 18 }); 19 function createPageMock(evaluateResult) { 20 return { 21 goto: vi.fn().mockResolvedValue(undefined), 22 evaluate: vi.fn().mockResolvedValue(evaluateResult), 23 }; 24 } 25 describe('instagram download helpers', () => { 26 it('parses canonical and username-prefixed Instagram media URLs', () => { 27 expect(parseInstagramMediaTarget('https://www.instagram.com/reel/DWg8NuZEj9p/?utm_source=ig_web_copy_link')).toEqual({ 28 kind: 'reel', 29 shortcode: 'DWg8NuZEj9p', 30 canonicalUrl: 'https://www.instagram.com/reel/DWg8NuZEj9p/', 31 }); 32 expect(parseInstagramMediaTarget('https://www.instagram.com/nasa/p/DWUR_azCWbN/?img_index=1')).toEqual({ 33 kind: 'p', 34 shortcode: 'DWUR_azCWbN', 35 canonicalUrl: 'https://www.instagram.com/p/DWUR_azCWbN/', 36 }); 37 }); 38 it('rejects unsupported URLs early', () => { 39 expect(() => parseInstagramMediaTarget('https://example.com/p/abc')).toThrow(ArgumentError); 40 expect(() => parseInstagramMediaTarget('https://www.instagram.com/stories/abc/123')).toThrow(ArgumentError); 41 }); 42 it('builds padded filenames and preserves known file extensions', () => { 43 expect(buildInstagramDownloadItems('DWUR_azCWbN', [ 44 { type: 'image', url: 'https://cdn.example.com/photo.webp?foo=1' }, 45 { type: 'video', url: 'https://cdn.example.com/video.mp4?bar=2' }, 46 { type: 'image', url: 'not-a-valid-url' }, 47 ])).toEqual([ 48 { type: 'image', url: 'https://cdn.example.com/photo.webp?foo=1', filename: 'DWUR_azCWbN_01.webp' }, 49 { type: 'video', url: 'https://cdn.example.com/video.mp4?bar=2', filename: 'DWUR_azCWbN_02.mp4' }, 50 { type: 'image', url: 'not-a-valid-url', filename: 'DWUR_azCWbN_03.jpg' }, 51 ]); 52 }); 53 }); 54 describe('instagram download command', () => { 55 beforeEach(() => { 56 mockHttpDownload.mockReset(); 57 logSpy.mockClear(); 58 }); 59 it('rejects invalid URLs before browser work', async () => { 60 const page = createPageMock({ ok: true, items: [] }); 61 await expect(cmd.func(page, { url: 'https://example.com/not-instagram' })).rejects.toThrow(ArgumentError); 62 expect(page.goto.mock.calls).toHaveLength(0); 63 }); 64 it('maps auth failures to AuthRequiredError', async () => { 65 const page = createPageMock({ ok: false, errorCode: 'AUTH_REQUIRED', error: 'Instagram login required' }); 66 await expect(cmd.func(page, { url: 'https://www.instagram.com/p/DWUR_azCWbN/' })).rejects.toThrow(AuthRequiredError); 67 expect(mockHttpDownload).not.toHaveBeenCalled(); 68 }); 69 it('maps rate limit failures to CliError with RATE_LIMITED code', async () => { 70 const page = createPageMock({ ok: false, errorCode: 'RATE_LIMITED', error: 'Please wait a few minutes' }); 71 await expect(cmd.func(page, { url: 'https://www.instagram.com/p/DWUR_azCWbN/' })).rejects.toMatchObject({ code: 'RATE_LIMITED' }); 72 expect(mockHttpDownload).not.toHaveBeenCalled(); 73 }); 74 it('maps private/unavailable failures to CommandExecutionError', async () => { 75 const page = createPageMock({ ok: false, errorCode: 'PRIVATE_OR_UNAVAILABLE', error: 'Post may be private' }); 76 await expect(cmd.func(page, { url: 'https://www.instagram.com/p/DWUR_azCWbN/' })).rejects.toThrow(CommandExecutionError); 77 expect(mockHttpDownload).not.toHaveBeenCalled(); 78 }); 79 it('throws when no downloadable media is found', async () => { 80 const page = createPageMock({ ok: true, shortcode: 'DWUR_azCWbN', items: [] }); 81 await expect(cmd.func(page, { url: 'https://www.instagram.com/p/DWUR_azCWbN/' })).rejects.toThrow(CommandExecutionError); 82 expect(mockHttpDownload).not.toHaveBeenCalled(); 83 }); 84 it('downloads media and prints saved directory', async () => { 85 mockHttpDownload 86 .mockResolvedValueOnce({ success: true, size: 120_000 }) 87 .mockResolvedValueOnce({ success: true, size: 8_200_000 }); 88 const page = createPageMock({ 89 ok: true, 90 shortcode: 'DWUR_azCWbN', 91 items: [ 92 { type: 'image', url: 'https://cdn.example.com/photo.webp?foo=1' }, 93 { type: 'video', url: 'https://cdn.example.com/video.mp4?bar=2' }, 94 ], 95 }); 96 const result = await cmd.func(page, { 97 url: 'https://www.instagram.com/nasa/p/DWUR_azCWbN/?img_index=1', 98 path: './instagram-test', 99 }); 100 expect(result).toBeNull(); 101 expect(page.goto.mock.calls[0]?.[0]).toBe('https://www.instagram.com/p/DWUR_azCWbN/'); 102 expect(mockHttpDownload).toHaveBeenNthCalledWith(1, 'https://cdn.example.com/photo.webp?foo=1', expect.stringContaining('instagram-test/DWUR_azCWbN/DWUR_azCWbN_01.webp'), expect.objectContaining({ timeout: 60000 })); 103 expect(mockHttpDownload).toHaveBeenNthCalledWith(2, 'https://cdn.example.com/video.mp4?bar=2', expect.stringContaining('instagram-test/DWUR_azCWbN/DWUR_azCWbN_02.mp4'), expect.objectContaining({ timeout: 120000 })); 104 expect(logSpy).toHaveBeenCalledWith('📁 saved: instagram-test/DWUR_azCWbN'); 105 }); 106 it('uses a cross-platform Downloads default when path is omitted', async () => { 107 mockHttpDownload.mockResolvedValueOnce({ success: true, size: 120_000 }); 108 const page = createPageMock({ 109 ok: true, 110 shortcode: 'DWUR_azCWbN', 111 items: [ 112 { type: 'image', url: 'https://cdn.example.com/photo.webp?foo=1' }, 113 ], 114 }); 115 await cmd.func(page, { url: 'https://www.instagram.com/p/DWUR_azCWbN/' }); 116 expect(mockHttpDownload).toHaveBeenCalledWith('https://cdn.example.com/photo.webp?foo=1', expect.stringContaining(`${os.homedir()}/Downloads/Instagram/DWUR_azCWbN/DWUR_azCWbN_01.webp`), expect.objectContaining({ timeout: 60000 })); 117 }); 118 });