Files
product_inventory/src/mediaManager.integration.test.ts
Ben Miller 19b3d5de2b Fix Drive video upload to Shopify
- Detect video mime types in MediaService to set correct resource ('VIDEO') and mediaContentType.

- Add fileSize to stagedUploadsCreate payload as required by Shopify for videos.

- Add safety check for missing upload targets to prevent crashes.

- Implement getSize in MockDriveService.

- Add unit test in MediaService.test.ts to verify correct resource and fileSize handling for video uploads.

- Update mock in mediaManager.integration.test.ts to support getSize().
2025-12-29 01:17:06 -07:00

245 lines
9.7 KiB
TypeScript

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()
}
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({})
})
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" })
})
})
})