👁️
6
fork

Configure Feed

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

at dev 387 lines 13 kB view raw
1/** 2 * Contract tests for CardDataProvider implementations 3 * 4 * Ensures ClientCardProvider and ServerCardProvider return identical data 5 */ 6 7import { beforeAll, describe, expect, it, vi } from "vitest"; 8import type { CardDataProvider } from "../card-data-provider"; 9import { ClientCardProvider } from "../cards-client-provider"; 10import { ServerCardProvider } from "../cards-server-provider"; 11import type { ScryfallId } from "../scryfall-types"; 12import { asOracleId, asScryfallId } from "../scryfall-types"; 13import { getTestCardOracleId } from "./test-card-lookup"; 14import { mockFetchFromPublicDir } from "./test-helpers"; 15 16// Mock cards-worker-client to use real worker code without Comlink/Worker 17vi.mock("../cards-worker-client", () => { 18 // biome-ignore lint/suspicious/noExplicitAny: mock needs to accept any worker instance 19 let workerInstance: any = null; 20 21 return { 22 initializeWorker: async () => { 23 const { __CardsWorkerForTestingOnly } = await import( 24 "../../workers/cards.worker" 25 ); 26 workerInstance = new __CardsWorkerForTestingOnly(); 27 await workerInstance.initialize(); 28 }, 29 getCardsWorker: () => 30 new Proxy(workerInstance, { 31 get(target, prop) { 32 const value = target[prop]; 33 if (typeof value === "function") { 34 // biome-ignore lint/suspicious/noExplicitAny: proxy needs to handle any function signature 35 return async (...args: any[]) => { 36 const result = await value.apply(target, args); 37 return structuredClone(result); 38 }; 39 } 40 return value; 41 }, 42 }), 43 }; 44}); 45 46// Test card from fixture 47const TEST_CARD_NAME = "Forest"; 48const TEST_CARD_ORACLE = getTestCardOracleId(TEST_CARD_NAME); 49 50// Resolved at runtime in beforeAll (needs provider) 51let TEST_CARD_ID: ScryfallId; 52 53const INVALID_ID = asScryfallId("00000000-0000-0000-0000-000000000000"); 54const INVALID_ORACLE = asOracleId("00000000-0000-0000-0000-000000000000"); 55 56// Sample of 50 card IDs evenly distributed across dataset for bulk testing 57const SAMPLE_CARD_IDS = [ 58 "0000419b-0bba-4488-8f7a-6194544ce91e", 59 "050ee59e-23fc-4476-8d1f-3f29d3ec9e74", 60 "0a05ead0-5e82-4af9-9c7e-78a6de425673", 61 "0eed3360-1634-4e60-b24d-4128c4896994", 62 "142479d8-8956-44a2-8c54-9dd6dc1774c0", 63 "19439420-9e0a-4fd1-bae9-f1c698edee1c", 64 "1e6e80ec-68a4-4cfb-a712-2ea0d26dc6a1", 65 "23a48f4a-96eb-47bd-9384-3bcc959a7c4b", 66 "28a6b23f-a854-469a-9b06-119507dd9d42", 67 "2dde460a-208f-4758-b172-64123ac69d75", 68 "32eb6ab0-831e-4a30-a2fc-5ea1cb40930c", 69 "381097a1-aac8-449b-bed5-ec0d9879a2c8", 70 "3d5529ca-5c20-4dfd-8595-96d6dfa6debe", 71 "427e31b2-2c53-46c8-af51-16a1ad6c66fd", 72 "47a469be-e43a-48cb-b216-bb39ade32acb", 73 "4cc636b1-5fc5-422d-9722-5fa12c754d6c", 74 "520d3b08-bd71-422c-ae6b-d67360a36aca", 75 "570a8272-7ca1-47db-834b-82f603d1417a", 76 "5bf655ce-c841-42b2-9578-56ab401bf4de", 77 "610def80-1303-488e-bfd9-4a27f031d20e", 78 "6620ead7-4499-4a19-b3b8-edd263067c02", 79 "6b3eb24e-bba4-463b-ad7e-e3daebda1e74", 80 "7083fa8a-a841-4c69-9443-af35676a7817", 81 "75a62b31-986c-4722-97da-d984272f0f05", 82 "7ae76409-40d7-4a54-ad58-5c67996b1a0c", 83 "8010cc08-7035-4daf-a4c4-e8d8959e1e82", 84 "853d15cd-a1a2-47a8-89c4-81b7ca663fff", 85 "8a238f08-f7c3-46be-b999-77b1c310cb1a", 86 "8f5427b1-f1c2-4bb3-8736-701667ac2256", 87 "949d42fb-72ff-40f9-8aff-7b0937fcdedf", 88 "998d0cc8-ca2a-41c3-ab65-d05c26ab8278", 89 "9e997f78-22a2-4b66-ac10-1adc9a72ce3b", 90 "a39d6484-6530-4237-84b9-68ab8a056e7c", 91 "a898939c-fb28-40cb-9c48-49763c0771a1", 92 "ada2b522-219a-44be-98c1-83a02efdd709", 93 "b2d51bdf-f118-4a1e-9060-bdf3c78697f2", 94 "b7e92c82-840f-4c75-b617-7b58a07be5b4", 95 "bd139009-fe5d-4189-8cde-a68ede6283fc", 96 "c233f64e-179f-4783-a6a4-d3fe2c718a39", 97 "c7552208-fd7c-4dc0-b7dd-acfcd3f78841", 98 "cc738025-a771-4186-b08c-7b37c0e9713b", 99 "d15adc93-1a71-453b-8277-4a525a9cbc7b", 100 "d691cd3b-afe5-4f28-95a9-125475515126", 101 "dbb0df36-8467-4a41-8e1c-6c3584d4fd10", 102 "e0d4b681-9f20-4bb5-8a6d-552f069e577f", 103 "e62d3bcc-7bb4-42be-90a9-caf3c1caa29d", 104 "eb58d7ba-ba86-433e-8f1e-3f492c380796", 105 "f08383ed-bf90-474a-97fb-9d7f8b3fb70a", 106 "f5a65d3b-83e7-4f32-89b3-d152e66f1868", 107 "fac38053-817d-4e0c-b6cc-81b6b92e652f", 108]; 109 110describe("CardDataProvider contract", () => { 111 let clientProvider: ClientCardProvider; 112 let serverProvider: ServerCardProvider; 113 114 beforeAll(async () => { 115 mockFetchFromPublicDir(); 116 117 serverProvider = new ServerCardProvider(); 118 clientProvider = new ClientCardProvider(); 119 await clientProvider.initialize(); 120 121 // Resolve test card ID from oracle ID 122 const cardId = await serverProvider.getCanonicalPrinting(TEST_CARD_ORACLE); 123 if (!cardId) { 124 throw new Error(`Failed to resolve test card ID for ${TEST_CARD_NAME}`); 125 } 126 TEST_CARD_ID = cardId; 127 }, 20_000); 128 129 describe.each([ 130 ["ServerCardProvider", () => serverProvider], 131 ["ClientCardProvider", () => clientProvider], 132 ])("%s", (_name, getProvider) => { 133 let provider: CardDataProvider; 134 135 beforeAll(() => { 136 provider = getProvider(); 137 }); 138 139 describe("getCardById", () => { 140 it("returns a valid card for known ID", async () => { 141 const card = await provider.getCardById(TEST_CARD_ID); 142 143 expect(card).toBeDefined(); 144 expect(card?.id).toBe(TEST_CARD_ID); 145 expect(card?.name).toBe(TEST_CARD_NAME); 146 expect(card?.oracle_id).toBe(TEST_CARD_ORACLE); 147 }); 148 149 it("returns undefined for invalid ID", async () => { 150 const card = await provider.getCardById(INVALID_ID); 151 expect(card).toBeUndefined(); 152 }); 153 154 it("returns undefined for missing ID", async () => { 155 const missingId = asScryfallId("ffffffff-ffff-ffff-ffff-ffffffffffff"); 156 const card = await provider.getCardById(missingId); 157 expect(card).toBeUndefined(); 158 }); 159 }); 160 161 describe("getPrintingsByOracleId", () => { 162 it("returns printing IDs for known oracle ID", async () => { 163 const printings = 164 await provider.getPrintingsByOracleId(TEST_CARD_ORACLE); 165 166 expect(Array.isArray(printings)).toBe(true); 167 expect(printings.length).toBeGreaterThan(0); 168 expect(printings).toContain(TEST_CARD_ID); 169 }); 170 171 it("returns empty array for invalid oracle ID", async () => { 172 const printings = await provider.getPrintingsByOracleId(INVALID_ORACLE); 173 expect(printings).toEqual([]); 174 }); 175 176 it("returns empty array for missing oracle ID", async () => { 177 const missingOracle = asOracleId( 178 "ffffffff-ffff-ffff-ffff-ffffffffffff", 179 ); 180 const printings = await provider.getPrintingsByOracleId(missingOracle); 181 expect(printings).toEqual([]); 182 }); 183 }); 184 185 describe("getMetadata", () => { 186 it("returns version and card count", async () => { 187 const metadata = await provider.getMetadata(); 188 189 expect(metadata).toHaveProperty("version"); 190 expect(metadata).toHaveProperty("cardCount"); 191 expect(typeof metadata.version).toBe("string"); 192 expect(typeof metadata.cardCount).toBe("number"); 193 expect(metadata.cardCount).toBeGreaterThan(0); 194 }); 195 }); 196 197 describe("getCanonicalPrinting", () => { 198 it("returns canonical printing ID for known oracle ID", async () => { 199 const canonicalId = 200 await provider.getCanonicalPrinting(TEST_CARD_ORACLE); 201 202 expect(canonicalId).toBeDefined(); 203 expect(typeof canonicalId).toBe("string"); 204 }); 205 206 it("returns undefined for invalid oracle ID", async () => { 207 const canonicalId = await provider.getCanonicalPrinting(INVALID_ORACLE); 208 expect(canonicalId).toBeUndefined(); 209 }); 210 211 it("returns undefined for missing oracle ID", async () => { 212 const missingOracle = asOracleId( 213 "ffffffff-ffff-ffff-ffff-ffffffffffff", 214 ); 215 const canonicalId = await provider.getCanonicalPrinting(missingOracle); 216 expect(canonicalId).toBeUndefined(); 217 }); 218 }); 219 }); 220 221 describe("Cross-provider consistency", () => { 222 it("returns identical card data", async () => { 223 const [clientCard, serverCard] = await Promise.all([ 224 clientProvider.getCardById(TEST_CARD_ID), 225 serverProvider.getCardById(TEST_CARD_ID), 226 ]); 227 228 expect(clientCard).toEqual(serverCard); 229 }); 230 231 it("returns identical printing lists", async () => { 232 const [clientPrintings, serverPrintings] = await Promise.all([ 233 clientProvider.getPrintingsByOracleId(TEST_CARD_ORACLE), 234 serverProvider.getPrintingsByOracleId(TEST_CARD_ORACLE), 235 ]); 236 237 expect(clientPrintings).toEqual(serverPrintings); 238 }); 239 240 it("returns identical metadata", async () => { 241 const [clientMetadata, serverMetadata] = await Promise.all([ 242 clientProvider.getMetadata(), 243 serverProvider.getMetadata(), 244 ]); 245 246 expect(clientMetadata).toEqual(serverMetadata); 247 }); 248 249 it("returns identical canonical printings", async () => { 250 const [clientCanonical, serverCanonical] = await Promise.all([ 251 clientProvider.getCanonicalPrinting(TEST_CARD_ORACLE), 252 serverProvider.getCanonicalPrinting(TEST_CARD_ORACLE), 253 ]); 254 255 expect(clientCanonical).toEqual(serverCanonical); 256 }); 257 258 it("returns identical results for missing IDs", async () => { 259 const missingId = asScryfallId("ffffffff-ffff-ffff-ffff-ffffffffffff"); 260 const [clientCard, serverCard] = await Promise.all([ 261 clientProvider.getCardById(missingId), 262 serverProvider.getCardById(missingId), 263 ]); 264 265 expect(clientCard).toBeUndefined(); 266 expect(serverCard).toBeUndefined(); 267 expect(clientCard).toEqual(serverCard); 268 }); 269 270 it.each(SAMPLE_CARD_IDS)( 271 "returns identical data for card %s", 272 async (cardId) => { 273 const id = asScryfallId(cardId); 274 const [clientCard, serverCard] = await Promise.all([ 275 clientProvider.getCardById(id), 276 serverProvider.getCardById(id), 277 ]); 278 279 expect(clientCard).toEqual(serverCard); 280 }, 281 ); 282 }); 283 284 describe("ClientCardProvider specific", () => { 285 it("supports searchCards", async () => { 286 expect(clientProvider.searchCards).toBeDefined(); 287 288 const results = await clientProvider.searchCards("forest", undefined, 10); 289 expect(Array.isArray(results)).toBe(true); 290 expect(results.length).toBeGreaterThan(0); 291 292 const forest = results.find((c) => c.name === TEST_CARD_NAME); 293 expect(forest).toBeDefined(); 294 }); 295 }); 296 297 describe("ServerCardProvider specific", () => { 298 it("does not support searchCards", () => { 299 expect(serverProvider.searchCards).toBeUndefined(); 300 }); 301 302 describe("getCardsByIds (batch fetch)", () => { 303 it("returns cards for valid IDs", async () => { 304 const ids = SAMPLE_CARD_IDS.slice(0, 5).map(asScryfallId); 305 const result = await serverProvider.getCardsByIds(ids); 306 307 expect(result.size).toBe(5); 308 for (const id of ids) { 309 expect(result.has(id)).toBe(true); 310 expect(result.get(id)?.id).toBe(id); 311 } 312 }); 313 314 it("handles mixed valid and invalid IDs", async () => { 315 const validIds = SAMPLE_CARD_IDS.slice(0, 3).map(asScryfallId); 316 const invalidIds = [INVALID_ID]; 317 const result = await serverProvider.getCardsByIds([ 318 ...validIds, 319 ...invalidIds, 320 ]); 321 322 expect(result.size).toBe(3); 323 for (const id of validIds) { 324 expect(result.has(id)).toBe(true); 325 } 326 expect(result.has(INVALID_ID)).toBe(false); 327 }); 328 329 it("returns same data as individual getCardById calls", async () => { 330 const ids = SAMPLE_CARD_IDS.slice(0, 10).map(asScryfallId); 331 const batchResult = await serverProvider.getCardsByIds(ids); 332 const individualResults = await Promise.all( 333 ids.map((id) => serverProvider.getCardById(id)), 334 ); 335 336 for (let i = 0; i < ids.length; i++) { 337 expect(batchResult.get(ids[i])).toEqual(individualResults[i]); 338 } 339 }); 340 341 it("handles empty array", async () => { 342 const result = await serverProvider.getCardsByIds([]); 343 expect(result.size).toBe(0); 344 }); 345 }); 346 }); 347 348 describe("Volatile data", () => { 349 describe.each([ 350 ["ServerCardProvider", () => serverProvider], 351 ["ClientCardProvider", () => clientProvider], 352 ])("%s", (_name, getProvider) => { 353 it("returns volatile data for known card", async () => { 354 const provider = getProvider(); 355 const volatileData = await provider.getVolatileData(TEST_CARD_ID); 356 357 expect(volatileData).not.toBeNull(); 358 expect(volatileData).toHaveProperty("edhrecRank"); 359 expect(volatileData).toHaveProperty("usd"); 360 expect(volatileData).toHaveProperty("usdFoil"); 361 expect(volatileData).toHaveProperty("usdEtched"); 362 expect(volatileData).toHaveProperty("eur"); 363 expect(volatileData).toHaveProperty("eurFoil"); 364 expect(volatileData).toHaveProperty("tix"); 365 }); 366 367 it("returns null for invalid card ID", async () => { 368 const provider = getProvider(); 369 const volatileData = await provider.getVolatileData(INVALID_ID); 370 expect(volatileData).toBeNull(); 371 }); 372 }); 373 374 it.each(SAMPLE_CARD_IDS)( 375 "returns identical volatile data for card %s", 376 async (cardId) => { 377 const id = asScryfallId(cardId); 378 const [clientData, serverData] = await Promise.all([ 379 clientProvider.getVolatileData(id), 380 serverProvider.getVolatileData(id), 381 ]); 382 383 expect(clientData).toEqual(serverData); 384 }, 385 ); 386 }); 387});