···11+import { getImageSize } from "./image";
22+33+// Mock the file system
44+jest.mock("fs", () => ({
55+ readFileSync: jest.fn(),
66+}));
77+88+// Mock sharp
99+jest.mock("sharp", () => {
1010+ return function (filePath: string) {
1111+ // Mock different behavior based on file path for testing different scenarios
1212+ if (filePath && filePath.includes("missing.jpg")) {
1313+ throw new Error("Input file is missing");
1414+ }
1515+1616+ if (filePath && filePath.includes("invalid.jpg")) {
1717+ return {
1818+ metadata: jest.fn().mockResolvedValue({}),
1919+ };
2020+ }
2121+2222+ if (filePath && filePath.includes("landscape.jpg")) {
2323+ return {
2424+ metadata: jest.fn().mockResolvedValue({ width: 1920, height: 1080 }),
2525+ };
2626+ }
2727+2828+ if (filePath && filePath.includes("portrait.jpg")) {
2929+ return {
3030+ metadata: jest.fn().mockResolvedValue({ width: 1080, height: 1920 }),
3131+ };
3232+ }
3333+3434+ // Default square image
3535+ return {
3636+ metadata: jest.fn().mockResolvedValue({ width: 1080, height: 1080 }),
3737+ };
3838+ };
3939+});
4040+4141+// Mock the logger
4242+jest.mock("../logger/logger", () => ({
4343+ logger: {
4444+ error: jest.fn(),
4545+ warn: jest.fn(),
4646+ info: jest.fn(),
4747+ debug: jest.fn(),
4848+ },
4949+}));
5050+5151+describe("getImageSize", () => {
5252+ test("should return correct dimensions for a square image", async () => {
5353+ const result = await getImageSize("/path/to/square.jpg");
5454+ expect(result).toEqual({ width: 1080, height: 1080 });
5555+ });
5656+5757+ test("should return correct dimensions for a landscape image", async () => {
5858+ const result = await getImageSize("/path/to/landscape.jpg");
5959+ expect(result).toEqual({ width: 1920, height: 1080 });
6060+ });
6161+6262+ test("should return correct dimensions for a portrait image", async () => {
6363+ const result = await getImageSize("/path/to/portrait.jpg");
6464+ expect(result).toEqual({ width: 1080, height: 1920 });
6565+ });
6666+6767+ test("should return null when metadata is missing width or height", async () => {
6868+ const result = await getImageSize("/path/to/invalid.jpg");
6969+ expect(result).toBeNull();
7070+ });
7171+7272+ test("should log error when image processing fails", async () => {
7373+ // Import the logger mock
7474+ const { logger } = require("../logger/logger");
7575+7676+ // Call the function with a path that will trigger an error
7777+ const result = await getImageSize("/path/to/missing.jpg");
7878+7979+ // Verify the logger.error was called with the expected message pattern
8080+ expect(logger.error).toHaveBeenCalled();
8181+ expect(logger.error).toHaveBeenCalledWith(
8282+ expect.stringMatching(
8383+ /Failed to get image aspect ratio; image path: \/path\/to\/missing\.jpg, error:/
8484+ )
8585+ );
8686+8787+ // Verify the function returns null when an error occurs
8888+ expect(result).toBeNull();
8989+ });
9090+});
+1-1
src/image/image.ts
···11import sharp from "sharp";
22import byteSize from "byte-size";
33import { logger } from "../logger/logger";
44-import { Ratio } from "src/media";
44+import { Ratio } from "../media";
5566/**
77 * Image lexicon maxSize 1mb
+10-8
src/instagram-to-bluesky.test.ts
···66 calculateEstimatedTime,
77 uploadMediaAndEmbed,
88} from "../src/instagram-to-bluesky";
99-import { BlueskyClient } from "../src/bluesky/bluesky";
1010-import { logger } from "../src/logger/logger";
1111-import { InstagramMediaProcessor } from "../src/media/media";
1212-import { ImagesEmbedImpl, VideoEmbedImpl } from "../src/bluesky/index";
1313-import type { InstagramExportedPost } from "../src/media/InstagramExportedPost";
1414-import { ImageMediaProcessResultImpl } from "./media";
99+import { BlueskyClient } from "./bluesky/bluesky";
1010+import { logger } from "./logger/logger";
1111+import { InstagramMediaProcessor, ImageMediaProcessResultImpl, VideoMediaProcessResultImpl } from "./media";
1212+import { ImagesEmbedImpl, VideoEmbedImpl } from "./bluesky/index";
1313+import type { InstagramExportedPost } from "./media/InstagramExportedPost";
15141615// Mock all dependencies
1716jest.mock("fs");
1818-jest.mock("../src/bluesky/bluesky", () => {
1717+jest.mock("./bluesky/bluesky", () => {
1918 return {
2019 BlueskyClient: jest.fn().mockImplementation(() => ({
2120 login: jest.fn().mockResolvedValue(undefined),
···2827 })),
2928 };
3029});
3131-jest.mock("../src/media/media", () => {
3030+jest.mock("./media", () => {
3131+ const actual = jest.requireActual("./media")
3232 const mockProcess = jest.fn().mockResolvedValue([
3333 {
3434 postDate: new Date(),
···6565 process: mockProcess,
6666 })),
6767 decodeUTF8: jest.fn((x) => x),
6868+ ImageMediaProcessResultImpl: actual.ImageMediaProcessResultImpl,
6969+ VideoMediaProcessResultImpl: actual.VideoMediaProcessResultImpl
6870 };
6971});
7072jest.mock("../src/logger/logger", () => ({
+35-27
src/instagram-to-bluesky.ts
···11-import FS from 'fs';
22-import path from 'path';
11+import FS from "fs";
22+import path from "path";
3344-import { BlobRef } from '@atproto/api';
44+import { BlobRef } from "@atproto/api";
5566-import { BlueskyClient } from './bluesky/bluesky';
66+import { BlueskyClient } from "./bluesky/bluesky";
77+import {
88+ EmbeddedMedia,
99+ ImageEmbed,
1010+ ImageEmbedImpl,
1111+ ImagesEmbedImpl,
1212+ VideoEmbedImpl,
1313+} from "./bluesky/index";
1414+import { AppConfig } from "./config";
1515+import { logger } from "./logger/logger";
716import {
88- EmbeddedMedia, ImageEmbed, ImageEmbedImpl, ImagesEmbedImpl, VideoEmbedImpl
99-} from './bluesky/index';
1010-import { AppConfig } from './config';
1111-import { logger } from './logger/logger';
1212-import { ImageMediaProcessResultImpl, MediaProcessResult, VideoMediaProcessResultImpl } from './media';
1313-import { InstagramExportedPost } from './media/InstagramExportedPost';
1414-import { decodeUTF8, InstagramMediaProcessor } from './media/media';
1515-1717+ ImageMediaProcessResultImpl,
1818+ MediaProcessResult,
1919+ VideoMediaProcessResultImpl,
2020+ decodeUTF8,
2121+ InstagramMediaProcessor,
2222+ InstagramExportedPost,
2323+} from "./media";
16241725const API_RATE_LIMIT_DELAY = 3000; // https://docs.bsky.app/docs/advanced-guides/rate-limits
1826···30383139/**
3240 * Uploads media files to Bluesky and creates appropriate embed objects
3333- *
4141+ *
3442 * This function processes an array of media files of the same type (either all images or all videos)
3543 * and uploads them to Bluesky's servers. For images, it collects them into a single ImagesEmbed object.
3644 * For videos, it creates a VideoEmbed object. If mixed media types are provided, only the first type
3745 * encountered will be processed.
3838- *
4646+ *
3947 * @param postText - The text content of the post to be associated with the media
4048 * @param embeddedMedia - Array of media objects to be processed and uploaded (should be same type)
4149 * @param bluesky - The BlueskyClient instance used for uploading media
4242- *
5050+ *
4351 * @returns {Promise<{
4452 * importedMediaCount: number, // Number of successfully uploaded media files
4553 * uploadedMedia: EmbeddedMedia | undefined // The final embed object for the post
4654 * }>}
4747- *
5555+ *
4856 * @throws Will log but not throw errors from failed media uploads
4949- *
5757+ *
5058 * @example
5159 * const result = await uploadMedia(
5260 * "My vacation photos",
···6977 for (const media of embeddedMedia) {
7078 try {
7179 if (media.getType() === "image") {
7272- const { mediaBuffer, mimeType, aspectRatio } = media as ImageMediaProcessResultImpl;
8080+ const { mediaBuffer, mimeType, aspectRatio } =
8181+ media as ImageMediaProcessResultImpl;
73827483 const blobRef: BlobRef = await bluesky.uploadMedia(
7584 mediaBuffer!,
7685 mimeType!
7786 );
7878- embeddedImages.push(new ImageEmbedImpl(postText, blobRef, mimeType!, aspectRatio));
8787+ embeddedImages.push(
8888+ new ImageEmbedImpl(postText, blobRef, mimeType!, aspectRatio)
8989+ );
7990 uploadedMedia = new ImagesEmbedImpl(embeddedImages);
8091 } else if (media.getType() === "video") {
8192 const { mediaBuffer, mimeType, aspectRatio } =
···143154 // Decide where to fetch post data to process from.
144155 let postsJsonPath: string;
145156 if (config.isTestModeEnabled()) {
146146- postsJsonPath = path.join(archivalFolder, 'posts.json');
157157+ postsJsonPath = path.join(archivalFolder, "posts.json");
147158 logger.info(
148159 `--- TEST mode is enabled, using content from ${archivalFolder} ---`
149160 );
150161 } else {
151162 postsJsonPath = path.join(
152163 archivalFolder,
153153- 'your_instagram_activity/content/posts_1.json'
164164+ "your_instagram_activity/content/posts_1.json"
154165 );
155166 }
156167···237248 );
238249 try {
239250 // Upload all the embedded media
240240- const { uploadedMedia, importedMediaCount } = await uploadMediaAndEmbed(
241241- postText,
242242- embeddedMedia,
243243- bluesky
244244- );
251251+ const { uploadedMedia, importedMediaCount } =
252252+ await uploadMediaAndEmbed(postText, embeddedMedia, bluesky);
245253 // Added uploaded media to the counter.
246254 importedMedia += importedMediaCount;
247255···259267 importedPosts++;
260268 }
261269 } else {
262262- logger.warn('No media uploaded! Check Error logs.');
270270+ logger.warn("No media uploaded! Check Error logs.");
263271 }
264272 } catch (error) {
265273 logger.error(
+12-1
src/media/index.ts
···11export * from './MediaProcessResult';
22export * from './ProcessedPost';
33-export * from './media';33+export * from './InstagramExportedPost';
44+export * from './processors/InstagramMediaProcessor';
55+export * from './processors/InstagramImageProcessor';
66+export * from './processors/InstagramVideoProcessor';
77+export * from './processors/DefaultMediaProcessorFactory';
88+export * from './interfaces/ProcessStrategy';
99+export * from './interfaces/MIMEType';
1010+export * from './interfaces/InstagramPostProcessingStrategy';
1111+export * from './interfaces/ImageMediaProcessingStrategy';
1212+export * from './interfaces/VideoMediaProcessingStrategy';
1313+export * from './interfaces/MediaProcessorFactory';
1414+export * from './utils';
···11-export * from './processors/InstagramMediaProcessor';
22-export * from './processors/InstagramImageProcessor';
33-export * from './processors/InstagramVideoProcessor';
44-export * from './processors/DefaultMediaProcessorFactory';
55-export * from './interfaces/ProcessStrategy';
66-export * from './interfaces/MIMEType';
77-export * from './interfaces/InstagramPostProcessingStrategy';
88-export * from './interfaces/ImageMediaProcessingStrategy';
99-export * from './interfaces/VideoMediaProcessingStrategy';
1010-export * from './interfaces/MediaProcessorFactory';
1111-export * from './utils';