Import Instagram archive to a Bluesky account
9
fork

Configure Feed

Select the types of activity you want to include in your feed.

Refactor media processing tests with comprehensive Instagram media processor coverage

- Update media test suite to cover InstagramMediaProcessor, InstagramImageProcessor, and InstagramVideoProcessor
- Add tests for processing single and multiple media types
- Implement mock factory and custom processor injection tests
- Remove legacy media processing test cases
- Improve test coverage for media processing scenarios

+151 -186
+151 -186
src/media/media.test.ts
··· 1 - import path from "path"; 2 - 3 1 import fs from "fs"; 4 2 5 - import { BlueskyClient } from '@bluesky/bluesky.js'; 6 - import { processVideoPost } from '@video/video.js'; 7 - 8 - import { getMimeType, processMedia, processPost } from "./media.js"; 3 + import { InstagramImageProcessor, InstagramMediaProcessor, InstagramVideoProcessor } from "./media.js"; 4 + import { InstagramExportedPost, VideoMedia, ImageMedia } from "./InstagramExportedPost.js"; 9 5 10 6 // Mock the file system 11 7 jest.mock("fs", () => ({ 12 8 readFileSync: jest.fn(), 13 9 })); 14 10 15 - // Mock the logger to avoid console noise during tests 16 - jest.mock("../src/logger", () => ({ 11 + // Mock the logger 12 + jest.mock("@logger/logger", () => ({ 17 13 logger: { 18 14 error: jest.fn(), 19 15 warn: jest.fn(), ··· 24 20 25 21 // Mock the video validation 26 22 jest.mock("../src/video", () => ({ 27 - validateVideo: jest.fn().mockReturnValue(true), 28 - getVideoDimensions: jest.fn().mockResolvedValue({ width: 640, height: 480 }), 29 - createVideoEmbed: jest.fn(), 30 - processVideoPost: jest.fn() 23 + validateVideo: jest.fn().mockReturnValue(true) 31 24 })); 32 25 33 - jest.mock('../src/bluesky', () => { 34 - const actual = jest.requireActual('../src/bluesky'); 35 - return { 36 - ...actual, // Keep the real ImageEmbedImpl and VideoEmbedImpl 37 - BlueskyClient: jest.fn().mockImplementation(() => ({ 38 - uploadVideo: jest.fn().mockResolvedValue({ ref: { $link: 'test-ref' } }), 39 - createPost: jest.fn() 40 - })) 41 - }; 42 - }); 43 - 44 - describe("Media Processing", () => { 26 + describe("Instagram Media Processing", () => { 45 27 beforeEach(() => { 46 - // Clear all mocks before each test 47 28 jest.clearAllMocks(); 48 - // Setup default mock for readFileSync 49 29 (fs.readFileSync as jest.Mock).mockReturnValue(Buffer.from("test")); 50 30 }); 51 31 52 - describe("getMimeType", () => { 53 - test("should return correct mime types for supported files", () => { 54 - expect(getMimeType("jpg")).toBe("image/jpeg"); 55 - expect(getMimeType("mp4")).toBe("video/mp4"); 56 - expect(getMimeType("mov")).toBe("video/quicktime"); 57 - }); 32 + describe("InstagramMediaProcessor", () => { 33 + const mockArchiveFolder = "/test/archive"; 34 + 35 + test("should process a post with multiple images", async () => { 36 + const mockPost: InstagramExportedPost = { 37 + creation_timestamp: 1234567890, 38 + title: "Test Post", 39 + media: [ 40 + { 41 + uri: "photo1.jpg", 42 + title: "Image 1", 43 + creation_timestamp: 1234567890, 44 + media_metadata: {}, 45 + cross_post_source: { source_app: "Instagram" }, 46 + backup_uri: "backup1.jpg", 47 + }, 48 + { 49 + uri: "photo2.jpg", 50 + title: "Image 2", 51 + creation_timestamp: 1234567890, 52 + media_metadata: {}, 53 + cross_post_source: { source_app: "Instagram" }, 54 + backup_uri: "backup2.jpg", 55 + }, 56 + ] as ImageMedia[], 57 + }; 58 58 59 - test("should return empty string for unsupported files", () => { 60 - expect(getMimeType("xyz")).toBe(""); 61 - }); 62 - }); 59 + const processor = new InstagramMediaProcessor([mockPost], mockArchiveFolder); 60 + const result = await processor.process(); 63 61 64 - describe("processMedia", () => { 65 - const testMedia = { 66 - uri: "test.mp4", 67 - creation_timestamp: Date.now() / 1000, 68 - title: "Test Media", 69 - media_metadata: { 70 - photo_metadata: { 71 - exif_data: [ 72 - { 73 - latitude: 45.5, 74 - longitude: -122.5, 75 - }, 76 - ], 77 - }, 78 - }, 79 - }; 80 - 81 - test("should process video media file correctly", async () => { 82 - const result = await processMedia( 83 - testMedia, 84 - path.join(__dirname, "../transfer/test_videos") 85 - ); 86 - 87 - expect(result.mimeType).toBe("video/mp4"); 88 - expect(result.isVideo).toBe(true); 89 - expect(result.mediaBuffer).toBeTruthy(); 90 - expect(result.mediaText).toContain("Test Media"); 91 - expect(result.mediaText).toContain("geo:45.5,-122.5"); 62 + expect(result).toHaveLength(1); 63 + expect(result[0].postText).toBe("Test Post"); 64 + expect(Array.isArray(result[0].embeddedMedia)).toBe(true); 65 + expect(result[0].embeddedMedia).toHaveLength(2); 92 66 }); 93 67 94 - test("should handle missing media file", async () => { 95 - (fs.readFileSync as jest.Mock).mockImplementation(() => { 96 - throw new Error("File not found"); 97 - }); 68 + test("should process a post with a single video", async () => { 69 + const mockPost: InstagramExportedPost = { 70 + creation_timestamp: 1234567890, 71 + title: "Test Video Post", 72 + media: { 73 + uri: "video.mp4", 74 + title: "Test Video", 75 + creation_timestamp: 1234567890, 76 + media_metadata: {}, 77 + cross_post_source: { source_app: "Instagram" }, 78 + backup_uri: "backup_video.mp4", 79 + dubbing_info: [], 80 + media_variants: [], 81 + } as VideoMedia, 82 + }; 98 83 99 - const result = await processMedia( 100 - testMedia, 101 - path.join(__dirname, "../transfer/test_videos") 102 - ); 84 + const processor = new InstagramMediaProcessor([mockPost], mockArchiveFolder); 85 + const result = await processor.process(); 103 86 104 - expect(result.mimeType).toBeNull(); 105 - expect(result.mediaBuffer).toBeNull(); 87 + expect(result).toHaveLength(1); 88 + expect(result[0].postText).toBe("Test Video Post"); 89 + expect(Array.isArray(result[0].embeddedMedia)).toBe(false); 106 90 }); 107 91 }); 108 92 109 - describe("processPost", () => { 110 - const testPost = { 111 - creation_timestamp: Date.now() / 1000, 112 - title: "Test Post", 113 - media: [ 93 + describe("InstagramImageProcessor", () => { 94 + test("should process multiple images", async () => { 95 + const mockImages: ImageMedia[] = [ 114 96 { 115 - uri: "test.mp4", 116 - creation_timestamp: Date.now() / 1000, 117 - title: "Test Media", 97 + uri: "photo1.jpg", 98 + title: "Image 1", 99 + creation_timestamp: 1234567890, 100 + media_metadata: {}, 101 + cross_post_source: { source_app: "Instagram" }, 102 + backup_uri: "backup1.jpg", 118 103 }, 119 - ], 120 - }; 104 + { 105 + uri: "photo2.jpg", 106 + title: "Image 2", 107 + creation_timestamp: 1234567890, 108 + media_metadata: {}, 109 + cross_post_source: { source_app: "Instagram" }, 110 + backup_uri: "backup2.jpg", 111 + }, 112 + ]; 113 + 114 + const processor = new InstagramImageProcessor(mockImages, "/test/archive"); 115 + const result = await processor.process(); 121 116 122 - const mockBluesky = new BlueskyClient('user', 'pass'); 123 - const simulate = false; 117 + expect(result).toHaveLength(2); 118 + expect(result[0].mimeType).toBe("image/jpeg"); 119 + expect(result[1].mimeType).toBe("image/jpeg"); 120 + }); 124 121 125 - test("should process post correctly", async () => { 126 - const result = await processPost( 127 - testPost, 128 - path.join(__dirname, "../transfer/test_videos"), 129 - mockBluesky, 130 - simulate 131 - ); 122 + test("should handle unsupported image types", async () => { 123 + const mockImages: ImageMedia[] = [{ 124 + uri: "photo.xyz", 125 + title: "Invalid Image", 126 + creation_timestamp: 1234567890, 127 + media_metadata: {}, 128 + cross_post_source: { source_app: "Instagram" }, 129 + backup_uri: "backup_invalid.jpg", 130 + }]; 132 131 133 - expect(result.postDate).toBeTruthy(); 134 - expect(result.postText).toBe("Test Post"); 135 - // Video media should only be a single embedded object. 136 - expect(Array.isArray(result.embeddedMedia)).toBe(false); 137 - expect(result.mediaCount).toBe(1); 132 + const processor = new InstagramImageProcessor(mockImages, "/test/archive"); 133 + 134 + await expect(processor.process()).rejects.toThrow("Unsupported file type"); 138 135 }); 136 + }); 139 137 140 - test("should handle post with no media", async () => { 141 - const emptyPost = { 142 - creation_timestamp: Date.now() / 1000, 143 - title: "Empty Post", 144 - media: [], 138 + describe("InstagramVideoProcessor", () => { 139 + test("should process a video", async () => { 140 + const mockVideo: VideoMedia = { 141 + uri: "video.mp4", 142 + title: "Test Video", 143 + creation_timestamp: 1234567890, 144 + media_metadata: {}, 145 + cross_post_source: { source_app: "Instagram" }, 146 + backup_uri: "backup_video.mp4", 147 + dubbing_info: [], 148 + media_variants: [], 145 149 }; 146 150 147 - const result = await processPost( 148 - emptyPost, 149 - path.join(__dirname, "../transfer/test_videos"), 150 - mockBluesky, 151 - simulate 152 - ); 151 + const processor = new InstagramVideoProcessor(mockVideo, "/test/archive"); 152 + const result = await processor.process(); 153 153 154 - expect(result.postDate).toBeTruthy(); 155 - expect(result.postText).toBe("Empty Post"); 156 - expect(result.embeddedMedia).toHaveLength(0); 157 - expect(result.mediaCount).toBe(0); 154 + expect(result.mimeType).toBe("video/mp4"); 155 + expect(result.mediaText).toBe("Test Video"); 158 156 }); 159 157 160 - test("should truncate long post text", async () => { 161 - const longPost = { 162 - creation_timestamp: Date.now() / 1000, 163 - title: "A".repeat(400), // Create a string longer than POST_TEXT_LIMIT 164 - media: [], 158 + test("should handle unsupported video types", async () => { 159 + const mockVideo: VideoMedia = { 160 + uri: "video.xyz", 161 + title: "Invalid Video", 162 + creation_timestamp: 1234567890, 163 + media_metadata: {}, 164 + cross_post_source: { source_app: "Instagram" }, 165 + backup_uri: "backup_invalid.mp4", 166 + dubbing_info: [], 167 + media_variants: [], 165 168 }; 166 169 167 - const result = await processPost( 168 - longPost, 169 - path.join(__dirname, "../transfer/test_videos"), 170 - mockBluesky, 171 - simulate 172 - ); 173 - 174 - expect(result.postText.length).toBeLessThanOrEqual(300); 175 - expect(result.postText.endsWith("...")).toBe(true); 170 + const processor = new InstagramVideoProcessor(mockVideo, "/test/archive"); 171 + 172 + await expect(processor.process()).rejects.toThrow("Unsupported file type"); 176 173 }); 174 + }); 177 175 178 - test("should handle post with jpg media as array", async () => { 179 - const jpgPost = { 180 - creation_timestamp: Date.now() / 1000, 181 - title: "Image Post", 176 + describe("Custom MediaProcessorFactory", () => { 177 + test("should allow custom processor injection", async () => { 178 + const mockFactory = { 179 + createProcessor: jest.fn().mockImplementation((media) => ({ 180 + process: jest.fn().mockResolvedValue([{ 181 + mediaText: "Mock Result", 182 + mimeType: "mock/type", 183 + mediaBuffer: Buffer.from("test") 184 + }]) 185 + })) 186 + }; 187 + 188 + const mockPost: InstagramExportedPost = { 189 + creation_timestamp: 1234567890, 190 + title: "Test Post", 182 191 media: [ 183 192 { 184 193 uri: "test.jpg", 185 - creation_timestamp: Date.now() / 1000, 186 - title: "Test Image", 187 - }, 188 - ], 189 - }; 190 - 191 - const result = await processPost( 192 - jpgPost, 193 - path.join(__dirname, "../transfer/test_videos"), 194 - mockBluesky, 195 - simulate 196 - ); 197 - 198 - expect(result.postDate).toBeTruthy(); 199 - expect(result.postText).toBe("Image Post"); 200 - // Image media should be an array 201 - expect(Array.isArray(result.embeddedMedia)).toBe(true); 202 - expect(result.embeddedMedia).toHaveLength(1); 203 - expect(result.mediaCount).toBe(1); 204 - }); 205 - 206 - test('should handle video posts correctly', async () => { 207 - const mockVideoPost = { 208 - title: "", 209 - media: [{ 210 - uri: "test.mp4", 211 - creation_timestamp: 1458732736, 212 - media_metadata: { 213 - video_metadata: { 214 - exif_data: [{ latitude: 53.141186112, longitude: 11.038734576 }] 215 - } 194 + title: "Test", 195 + creation_timestamp: 1234567890, 196 + media_metadata: {}, 197 + cross_post_source: { source_app: "Instagram" }, 198 + backup_uri: "backup_test.jpg", 216 199 }, 217 - title: "No filter needed. #waterfall #nature" 218 - }] 219 - }; 220 - 221 - const mockVideoEmbed = { 222 - $type: 'app.bsky.embed.video', 223 - video: { 224 - $type: 'blob', 225 - ref: { $link: 'test-ref' }, 226 - mimeType: 'video/mp4', 227 - size: 1000 228 - }, 229 - aspectRatio: { width: 640, height: 480 } 200 + ] as ImageMedia[], 230 201 }; 231 202 232 - (processVideoPost as jest.Mock).mockResolvedValue(mockVideoEmbed); 203 + const processor = new InstagramMediaProcessor([mockPost], "/test/archive", mockFactory); 204 + const result = await processor.process(); 233 205 234 - const result = await processPost( 235 - mockVideoPost, 236 - path.join(__dirname, "../transfer/test_videos"), 237 - mockBluesky, 238 - simulate 239 - ); 240 - 241 - expect(processVideoPost).toHaveBeenCalled(); 242 - expect(result.embeddedMedia).toEqual(mockVideoEmbed); 243 - expect(result.mediaCount).toBe(1); 206 + expect(mockFactory.createProcessor).toHaveBeenCalled(); 207 + expect(result).toHaveLength(1); 208 + expect(result[0].embeddedMedia).toBeDefined(); 244 209 }); 245 210 }); 246 211 });