- 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().
245 lines
9.7 KiB
TypeScript
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" })
|
|
})
|
|
})
|
|
})
|