import { MediaService } from "./services/MediaService" // Unmock MediaService so we test the real class logic jest.unmock("./services/MediaService") // Mock dependencies const mockDrive = { getOrCreateFolder: jest.fn(), getFiles: jest.fn(), createFile: jest.fn(), renameFile: jest.fn(), trashFile: jest.fn(), updateFileProperties: jest.fn(), getFileProperties: jest.fn(), getFileById: jest.fn(), getFilesWithProperties: jest.fn() } const mockShopify = { getProductMedia: jest.fn(), productCreateMedia: jest.fn(), productDeleteMedia: jest.fn(), productReorderMedia: jest.fn(), stagedUploadsCreate: jest.fn() } const mockNetwork = { fetch: jest.fn() } const mockConfig = { productPhotosFolderId: "root_folder" } // Mock Utilities global.Utilities = { base64Encode: jest.fn().mockReturnValue("base64encoded"), newBlob: jest.fn() } as any // Mock Advanced Drive Service global.Drive = { Files: { get: jest.fn().mockImplementation((id) => { if (id === "drive_1") return { appProperties: { shopify_media_id: "gid://shopify/Media/100" } } return { appProperties: {} } }) } } as any global.DriveApp = { getRootFolder: jest.fn().mockReturnValue({ removeFile: jest.fn() }) } as any describe("MediaService V2 Integration Logic", () => { let service: MediaService const dummyPid = "gid://shopify/Product/123" beforeEach(() => { jest.clearAllMocks() // Instantiate the REAL service with MOCKED delegates service = new MediaService(mockDrive as any, mockShopify as any, mockNetwork as any, mockConfig as any) // Setup Network mock for Blob download // MediaService calls networkService.fetch(...).getBlob() // so fetch matches MUST return an object with getBlob mockNetwork.fetch.mockReturnValue({ getBlob: jest.fn().mockReturnValue({ getDataAsString: () => "fake_blob_data", getContentType: () => "image/jpeg", getBytes: () => [], setName: jest.fn() }) }) // Setup default File mock behaviors mockDrive.getFileById.mockImplementation((id: string) => ({ setName: jest.fn(), getName: () => "file_name.jpg", moveTo: jest.fn(), getMimeType: () => "image/jpeg", getBlob: () => ({}), getSize: () => 1024, getId: () => id })) mockDrive.createFile.mockReturnValue({ getId: () => "new_created_file_id" }) mockDrive.getFileProperties.mockReturnValue({}) mockDrive.getFilesWithProperties.mockImplementation((folderId: string) => { const files = mockDrive.getFiles(folderId) || [] return files.map(f => ({ file: f, properties: mockDrive.getFileProperties(f.getId()) })) }) }) describe("getUnifiedMediaState (Phase A)", () => { test("should match Drive and Shopify items by ID (Strong Link)", () => { // Setup Drive const driveFile = { getId: () => "drive_1", getName: () => "IMG_001.jpg", getAppProperty: (k: string) => k === 'shopify_media_id' ? "gid://shopify/Media/100" : null, getThumbnail: () => ({ getBytes: () => [] }), getMimeType: () => "image/jpeg" } mockDrive.getOrCreateFolder.mockReturnValue({ getId: () => "folder_1" }) mockDrive.getFiles.mockReturnValue([driveFile]) // Setup Shopify mockDrive.getFileProperties.mockReturnValue({ 'shopify_media_id': 'gid://shopify/Media/100' }) const shopMedia = { id: "gid://shopify/Media/100", mediaContentType: "IMAGE", preview: { image: { originalSrc: "http://shopify.com/img.jpg" } } } mockShopify.getProductMedia.mockReturnValue([shopMedia]) // Act const result = service.getUnifiedMediaState("SKU-123", dummyPid) // Expect expect(result).toHaveLength(1) expect(result[0].driveId).toBe("drive_1") expect(result[0].shopifyId).toBe("gid://shopify/Media/100") expect(result[0].source).toBe("synced") }) test("should identify Drive-Only items (New Uploads)", () => { const driveFile = { getId: () => "drive_new", getName: () => "new.jpg", getAppProperty: () => null, getThumbnail: () => ({ getBytes: () => [] }), getMimeType: () => "image/jpeg" } mockDrive.getOrCreateFolder.mockReturnValue({ getId: () => "folder_1" }) mockDrive.getFiles.mockReturnValue([driveFile]) mockShopify.getProductMedia.mockReturnValue([]) const result = service.getUnifiedMediaState("SKU-123", dummyPid) expect(result).toHaveLength(1) expect(result[0].source).toBe("drive_only") }) test("should identify Shopify-Only items", () => { mockDrive.getOrCreateFolder.mockReturnValue({ getId: () => "folder_1", addFile: jest.fn() }) mockDrive.getFiles.mockReturnValue([]) const shopMedia = { id: "gid://shopify/Media/555", mediaContentType: "IMAGE", preview: { image: { originalSrc: "url" } } } mockShopify.getProductMedia.mockReturnValue([shopMedia]) const result = service.getUnifiedMediaState("SKU-123", dummyPid) expect(result).toHaveLength(1) expect(result[0].source).toBe("shopify_only") }) }) describe("processMediaChanges (Phase B)", () => { test("should rename Drive files sequentially", () => { const finalState = [ { id: "1", driveId: "d1", shopifyId: "s1", source: "synced", filename: "foo.jpg" }, { id: "2", driveId: "d2", shopifyId: "s2", source: "synced", filename: "bar.jpg" } ] // Mock getUnifiedMediaState to return empty to skip delete logic interference? // Or return something consistent. jest.spyOn(service, "getUnifiedMediaState").mockReturnValue([]) // Act service.processMediaChanges("SKU-123", finalState, dummyPid) // Assert expect(mockDrive.renameFile).toHaveBeenCalledWith("d1", expect.stringMatching(/SKU-123_\d+\.jpg/)) expect(mockDrive.renameFile).toHaveBeenCalledWith("d2", expect.stringMatching(/SKU-123_\d+\.jpg/)) }) test("should call Shopify Reorder Mutation", () => { const finalState = [ { id: "1", shopifyId: "s10", sortOrder: 0, driveId: "d1" }, { id: "2", shopifyId: "s20", sortOrder: 1, driveId: "d2" } ] jest.spyOn(service, "getUnifiedMediaState").mockReturnValue([]) service.processMediaChanges("SKU-123", finalState, dummyPid) expect(mockShopify.productReorderMedia).toHaveBeenCalledWith(dummyPid, [ { id: "s10", newPosition: "0" }, { id: "s20", newPosition: "1" } ]) }) test("should backfill Shopify-Only items to Drive", () => { const finalState = [ { id: "3", driveId: null, shopifyId: "s99", source: "shopify_only", thumbnail: "http://url.jpg", filename: "backfill.jpg" } ] jest.spyOn(service, "getUnifiedMediaState").mockReturnValue([]) // Mock file creation mockDrive.getOrCreateFolder.mockReturnValue({ getId: () => "folder_1", addFile: jest.fn() }) // We set default mockDrive.createFile above but we can specialize if needed // Default returns "new_created_file_id" // Act service.processMediaChanges("SKU-123", finalState, dummyPid) expect(mockDrive.createFile).toHaveBeenCalled() expect(mockDrive.updateFileProperties).toHaveBeenCalledWith("new_created_file_id", { shopify_media_id: "s99" }) }) test("should delete removed items", () => { // Mock current state has items const current = [ { id: "del_1", driveId: "d_del", shopifyId: "s_del", filename: "delete_me.jpg" } ] jest.spyOn(service, "getUnifiedMediaState").mockReturnValue(current) // Final state empty const finalState: any[] = [] service.processMediaChanges("SKU-123", finalState, dummyPid) expect(mockShopify.productDeleteMedia).toHaveBeenCalledWith(dummyPid, "s_del") expect(mockDrive.trashFile).toHaveBeenCalledWith("d_del") }) test("should upload Drive-Only items", () => { const finalState = [ { id: "new_1", driveId: "d_new", shopifyId: null, source: "drive_only", filename: "new.jpg" } ] jest.spyOn(service, "getUnifiedMediaState").mockReturnValue([]) // Mock staged uploads flow mockShopify.stagedUploadsCreate.mockReturnValue({ stagedTargets: [{ url: "http://upload", resourceUrl: "http://resource", parameters: [] }] }) // Mock Create Media returning ID mockShopify.productCreateMedia.mockReturnValue({ media: [{ id: "new_shopify_id", status: "READY" }] }) service.processMediaChanges("SKU-123", finalState, dummyPid) expect(mockShopify.stagedUploadsCreate).toHaveBeenCalled() expect(mockShopify.productCreateMedia).toHaveBeenCalled() // Check property update expect(mockDrive.updateFileProperties).toHaveBeenCalledWith("d_new", { shopify_media_id: "new_shopify_id" }) }) }) })