@jskit-ai/connectors-catalog 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (399) hide show
  1. package/README.md +192 -0
  2. package/docs/ai-upstream-notices.md +51 -0
  3. package/docs/ai.md +191 -0
  4. package/docs/airtable.md +130 -0
  5. package/docs/algolia.md +190 -0
  6. package/docs/amazon-redshift.md +229 -0
  7. package/docs/amplitude.md +210 -0
  8. package/docs/apify.md +162 -0
  9. package/docs/apollo-io.md +200 -0
  10. package/docs/asana.md +126 -0
  11. package/docs/ashby.md +188 -0
  12. package/docs/atlassian.md +224 -0
  13. package/docs/attention.md +162 -0
  14. package/docs/aws-athena.md +150 -0
  15. package/docs/aws-credentials.md +75 -0
  16. package/docs/aws-s3.md +193 -0
  17. package/docs/bigquery.md +146 -0
  18. package/docs/brevo.md +122 -0
  19. package/docs/calendly.md +158 -0
  20. package/docs/canva.md +191 -0
  21. package/docs/chargebee.md +192 -0
  22. package/docs/clay.md +194 -0
  23. package/docs/clickhouse.md +242 -0
  24. package/docs/confidence-exp.md +74 -0
  25. package/docs/confidence-flags.md +75 -0
  26. package/docs/confidence-oauth.md +137 -0
  27. package/docs/contentful.md +123 -0
  28. package/docs/databricks.md +180 -0
  29. package/docs/dbt-semantic-layer.md +175 -0
  30. package/docs/elevenlabs.md +102 -0
  31. package/docs/figma.md +132 -0
  32. package/docs/firebase-cloud-messaging.md +262 -0
  33. package/docs/firecrawl.md +137 -0
  34. package/docs/fireflies.md +95 -0
  35. package/docs/fireworks-ai.md +53 -0
  36. package/docs/gatewayapi.md +113 -0
  37. package/docs/gemini-enterprise.md +224 -0
  38. package/docs/github-api.md +133 -0
  39. package/docs/gitlab-api.md +138 -0
  40. package/docs/gmail.md +133 -0
  41. package/docs/gong.md +205 -0
  42. package/docs/google-ads.md +156 -0
  43. package/docs/google-analytics.md +202 -0
  44. package/docs/google-docs.md +105 -0
  45. package/docs/google-drive.md +108 -0
  46. package/docs/google-maps-platform.md +208 -0
  47. package/docs/google-oauth.md +123 -0
  48. package/docs/google-search-console.md +124 -0
  49. package/docs/google-sheets.md +94 -0
  50. package/docs/google-slides.md +95 -0
  51. package/docs/granola.md +178 -0
  52. package/docs/hex.md +161 -0
  53. package/docs/heygen.md +124 -0
  54. package/docs/hubspot.md +142 -0
  55. package/docs/incident-io.md +118 -0
  56. package/docs/inngest.md +178 -0
  57. package/docs/klipy.md +149 -0
  58. package/docs/lexware.md +144 -0
  59. package/docs/lightspeed.md +193 -0
  60. package/docs/linear.md +137 -0
  61. package/docs/linkedin.md +196 -0
  62. package/docs/logo-dev.md +152 -0
  63. package/docs/mailgun.md +153 -0
  64. package/docs/mapbox.md +174 -0
  65. package/docs/microsoft-excel.md +97 -0
  66. package/docs/microsoft-fabric.md +249 -0
  67. package/docs/microsoft-oauth.md +143 -0
  68. package/docs/microsoft-onedrive.md +76 -0
  69. package/docs/microsoft-onenote.md +75 -0
  70. package/docs/microsoft-outlook.md +90 -0
  71. package/docs/microsoft-powerpoint.md +119 -0
  72. package/docs/microsoft-sharepoint.md +75 -0
  73. package/docs/microsoft-teams.md +72 -0
  74. package/docs/microsoft-word.md +119 -0
  75. package/docs/miro.md +117 -0
  76. package/docs/n8n.md +169 -0
  77. package/docs/notion.md +137 -0
  78. package/docs/oura.md +161 -0
  79. package/docs/paddle.md +168 -0
  80. package/docs/perplexity.md +85 -0
  81. package/docs/pipedrive.md +120 -0
  82. package/docs/polar.md +63 -0
  83. package/docs/posthog.md +227 -0
  84. package/docs/prestashop.md +151 -0
  85. package/docs/replicate.md +60 -0
  86. package/docs/resend.md +153 -0
  87. package/docs/salesforce.md +234 -0
  88. package/docs/sanity.md +122 -0
  89. package/docs/semrush.md +276 -0
  90. package/docs/sentry.md +112 -0
  91. package/docs/sevdesk.md +107 -0
  92. package/docs/shopify.md +206 -0
  93. package/docs/slack.md +257 -0
  94. package/docs/snowflake.md +271 -0
  95. package/docs/storyblok.md +130 -0
  96. package/docs/stripe.md +77 -0
  97. package/docs/supabase.md +51 -0
  98. package/docs/tally.md +105 -0
  99. package/docs/telegram.md +165 -0
  100. package/docs/tiktok.md +201 -0
  101. package/docs/twilio.md +284 -0
  102. package/docs/twitch.md +428 -0
  103. package/docs/wave.md +440 -0
  104. package/docs/wix.md +1249 -0
  105. package/docs/wiz.md +177 -0
  106. package/docs/woocommerce.md +400 -0
  107. package/docs/wordpress-com.md +358 -0
  108. package/docs/wordpress-self-hosted.md +252 -0
  109. package/docs/workday.md +292 -0
  110. package/docs/x-twitter.md +188 -0
  111. package/docs/xero.md +429 -0
  112. package/docs/zoho-books.md +214 -0
  113. package/docs/zoho-crm.md +192 -0
  114. package/package.json +153 -0
  115. package/patterns/ai-connections/PATTERN.md +84 -0
  116. package/patterns/ai-connections/example/ai-model.js +11 -0
  117. package/patterns/ai-connections/example/perplexity-answer.js +29 -0
  118. package/patterns/api-key-connection/PATTERN.md +368 -0
  119. package/patterns/api-key-connection/example/connections.js +18 -0
  120. package/patterns/api-key-connection/example/integrations.json +19 -0
  121. package/patterns/assistant-mcp/PATTERN.md +110 -0
  122. package/patterns/assistant-mcp/example/integrations.json +13 -0
  123. package/patterns/assistant-mcp-oauth/PATTERN.md +235 -0
  124. package/patterns/assistant-mcp-oauth/example/integrations.json +102 -0
  125. package/patterns/aws-storage-queries/PATTERN.md +169 -0
  126. package/patterns/aws-storage-queries/example/formats/data-formats.js +35 -0
  127. package/patterns/aws-storage-queries/example/formats/package-lock.json +49 -0
  128. package/patterns/aws-storage-queries/example/formats/package.json +12 -0
  129. package/patterns/aws-storage-queries/example/formats/verify-formats.mjs +37 -0
  130. package/patterns/aws-storage-queries/example/integrations.json +38 -0
  131. package/patterns/aws-storage-queries/example/s3-transfer.js +34 -0
  132. package/patterns/event-delivery/PATTERN.md +151 -0
  133. package/patterns/event-delivery/example/integrations.json +17 -0
  134. package/patterns/firebase-messaging/PATTERN.md +156 -0
  135. package/patterns/firebase-messaging/example/server/notifications.js +23 -0
  136. package/patterns/google-ads-search/PATTERN.md +92 -0
  137. package/patterns/google-ads-search/example/ads-setup.js +25 -0
  138. package/patterns/oauth-connection/PATTERN.md +724 -0
  139. package/patterns/oauth-connection/example/integrations.json +20 -0
  140. package/patterns/paddle-catalogue/PATTERN.md +78 -0
  141. package/patterns/paddle-catalogue/example/create-products.js +38 -0
  142. package/patterns/public-image/PATTERN.md +84 -0
  143. package/patterns/public-image/example/integrations.json +13 -0
  144. package/patterns/public-image/example/logo-url.js +16 -0
  145. package/patterns/redshift-queries/PATTERN.md +150 -0
  146. package/patterns/redshift-queries/example/integrations.json +33 -0
  147. package/patterns/source-scanning/PATTERN.md +83 -0
  148. package/patterns/source-scanning/example/source-scanner.js +13 -0
  149. package/src/client/firebase-cloud-messaging.js +10 -0
  150. package/src/client/google-analytics.js +72 -0
  151. package/src/client/google-maps-platform.js +39 -0
  152. package/src/client/logo-dev.js +36 -0
  153. package/src/server/ai.js +46 -0
  154. package/src/server/airtable.js +67 -0
  155. package/src/server/algolia.js +63 -0
  156. package/src/server/amazon-redshift.js +96 -0
  157. package/src/server/amplitude.js +31 -0
  158. package/src/server/apify.js +68 -0
  159. package/src/server/apollo-io.js +85 -0
  160. package/src/server/asana.js +64 -0
  161. package/src/server/ashby.js +61 -0
  162. package/src/server/atlassian.js +32 -0
  163. package/src/server/attention.js +93 -0
  164. package/src/server/aws-athena.js +82 -0
  165. package/src/server/aws-s3.js +49 -0
  166. package/src/server/awsTransport.js +75 -0
  167. package/src/server/bigquery.js +97 -0
  168. package/src/server/brevo.js +97 -0
  169. package/src/server/calendly.js +77 -0
  170. package/src/server/canva.js +13 -0
  171. package/src/server/chargebee.js +88 -0
  172. package/src/server/clay.js +78 -0
  173. package/src/server/clickhouse.js +66 -0
  174. package/src/server/confidence.js +29 -0
  175. package/src/server/contentful.js +40 -0
  176. package/src/server/databricks.js +55 -0
  177. package/src/server/dbt-semantic-layer.js +95 -0
  178. package/src/server/elevenlabs.js +81 -0
  179. package/src/server/figma.js +24 -0
  180. package/src/server/firebase-cloud-messaging.js +111 -0
  181. package/src/server/firecrawl.js +70 -0
  182. package/src/server/fireflies.js +31 -0
  183. package/src/server/fireworks-ai.js +16 -0
  184. package/src/server/gatewayapi.js +48 -0
  185. package/src/server/gemini-enterprise.js +47 -0
  186. package/src/server/github-api.js +90 -0
  187. package/src/server/gitlab-api.js +67 -0
  188. package/src/server/gmail.js +60 -0
  189. package/src/server/gong.js +52 -0
  190. package/src/server/google-ads-search.js +134 -0
  191. package/src/server/google-ads.js +80 -0
  192. package/src/server/google-docs.js +36 -0
  193. package/src/server/google-drive.js +71 -0
  194. package/src/server/google-maps-platform.js +90 -0
  195. package/src/server/google-search-console.js +31 -0
  196. package/src/server/google-sheets.js +60 -0
  197. package/src/server/google-slides.js +36 -0
  198. package/src/server/google.js +61 -0
  199. package/src/server/granola.js +115 -0
  200. package/src/server/graphqlOperation.js +15 -0
  201. package/src/server/hex.js +29 -0
  202. package/src/server/heygen.js +54 -0
  203. package/src/server/hubspot.js +85 -0
  204. package/src/server/incident-io.js +68 -0
  205. package/src/server/inngest.js +86 -0
  206. package/src/server/jsonOperation.js +34 -0
  207. package/src/server/klipy.js +34 -0
  208. package/src/server/lexware.js +59 -0
  209. package/src/server/lightspeed.js +82 -0
  210. package/src/server/linear.js +52 -0
  211. package/src/server/linkedin.js +74 -0
  212. package/src/server/mailgun.js +76 -0
  213. package/src/server/mapbox.js +46 -0
  214. package/src/server/mcpRegistration.js +41 -0
  215. package/src/server/mcpTransport.js +60 -0
  216. package/src/server/microsoft-excel.js +67 -0
  217. package/src/server/microsoft-fabric.js +81 -0
  218. package/src/server/microsoft-onedrive.js +58 -0
  219. package/src/server/microsoft-onenote.js +52 -0
  220. package/src/server/microsoft-outlook.js +58 -0
  221. package/src/server/microsoft-powerpoint.js +5 -0
  222. package/src/server/microsoft-sharepoint.js +46 -0
  223. package/src/server/microsoft-teams.js +29 -0
  224. package/src/server/microsoft-word.js +5 -0
  225. package/src/server/microsoft.js +88 -0
  226. package/src/server/miro.js +29 -0
  227. package/src/server/n8n.js +78 -0
  228. package/src/server/notion.js +76 -0
  229. package/src/server/oura.js +48 -0
  230. package/src/server/paddle.js +83 -0
  231. package/src/server/perplexity.js +15 -0
  232. package/src/server/pipedrive.js +95 -0
  233. package/src/server/polar.js +20 -0
  234. package/src/server/posthog.js +44 -0
  235. package/src/server/prestashop.js +42 -0
  236. package/src/server/replicate.js +42 -0
  237. package/src/server/resend.js +93 -0
  238. package/src/server/salesforce.js +84 -0
  239. package/src/server/sanity.js +31 -0
  240. package/src/server/semrush.js +359 -0
  241. package/src/server/sentry.js +30 -0
  242. package/src/server/sevdesk.js +20 -0
  243. package/src/server/shopify.js +99 -0
  244. package/src/server/slack.js +143 -0
  245. package/src/server/snowflake.js +139 -0
  246. package/src/server/storyblok.js +49 -0
  247. package/src/server/stripe.js +13 -0
  248. package/src/server/supabase.js +13 -0
  249. package/src/server/tally.js +78 -0
  250. package/src/server/telegram.js +55 -0
  251. package/src/server/tiktok.js +86 -0
  252. package/src/server/twilio.js +90 -0
  253. package/src/server/twitch.js +409 -0
  254. package/src/server/wave.js +520 -0
  255. package/src/server/wix.js +1048 -0
  256. package/src/server/wiz.js +117 -0
  257. package/src/server/woocommerce.js +412 -0
  258. package/src/server/wordpress-com.js +236 -0
  259. package/src/server/wordpress-self-hosted.js +134 -0
  260. package/src/server/workday.js +136 -0
  261. package/src/server/x-twitter.js +110 -0
  262. package/src/server/xero.js +397 -0
  263. package/src/server/zoho-books.js +90 -0
  264. package/src/server/zoho-crm.js +89 -0
  265. package/src/server/zohoOAuth.js +21 -0
  266. package/src/shared/ai-models.json +8049 -0
  267. package/src/shared/ai.js +63 -0
  268. package/src/shared/algolia.js +29 -0
  269. package/src/shared/amazon-redshift.js +51 -0
  270. package/src/shared/amplitude.js +28 -0
  271. package/src/shared/atlassian.js +43 -0
  272. package/src/shared/aws.js +72 -0
  273. package/src/shared/canva.js +66 -0
  274. package/src/shared/chargebee.js +26 -0
  275. package/src/shared/clickhouse.js +38 -0
  276. package/src/shared/confidence.js +28 -0
  277. package/src/shared/databricks.js +33 -0
  278. package/src/shared/dbt-semantic-layer.js +32 -0
  279. package/src/shared/definitions.js +140 -0
  280. package/src/shared/figma.js +21 -0
  281. package/src/shared/firebase-cloud-messaging.js +45 -0
  282. package/src/shared/gemini-enterprise.js +33 -0
  283. package/src/shared/gong.js +33 -0
  284. package/src/shared/google-ads-search.js +31 -0
  285. package/src/shared/google-ads.js +28 -0
  286. package/src/shared/google-maps-platform.js +28 -0
  287. package/src/shared/google.js +133 -0
  288. package/src/shared/granola.js +25 -0
  289. package/src/shared/hex.js +28 -0
  290. package/src/shared/inngest.js +35 -0
  291. package/src/shared/lightspeed.js +75 -0
  292. package/src/shared/linkedin.js +27 -0
  293. package/src/shared/logo-dev.js +21 -0
  294. package/src/shared/mapbox.js +38 -0
  295. package/src/shared/mcp.js +92 -0
  296. package/src/shared/microsoft-fabric.js +47 -0
  297. package/src/shared/microsoft.js +231 -0
  298. package/src/shared/miro.js +27 -0
  299. package/src/shared/oura.js +23 -0
  300. package/src/shared/posthog.js +33 -0
  301. package/src/shared/prestashop.js +23 -0
  302. package/src/shared/salesforce.js +36 -0
  303. package/src/shared/semrush.js +24 -0
  304. package/src/shared/shopify.js +51 -0
  305. package/src/shared/siteUrl.js +12 -0
  306. package/src/shared/slack.js +88 -0
  307. package/src/shared/snowflake.js +55 -0
  308. package/src/shared/tiktok.js +22 -0
  309. package/src/shared/tokens.js +747 -0
  310. package/src/shared/twilio.js +41 -0
  311. package/src/shared/twitch.js +49 -0
  312. package/src/shared/wave.js +52 -0
  313. package/src/shared/wix.js +40 -0
  314. package/src/shared/wiz.js +41 -0
  315. package/src/shared/wordpress-com.js +30 -0
  316. package/src/shared/wordpress.js +69 -0
  317. package/src/shared/workday.js +50 -0
  318. package/src/shared/x-twitter.js +19 -0
  319. package/src/shared/xero.js +46 -0
  320. package/src/shared/zoho-books.js +39 -0
  321. package/src/shared/zoho-crm.js +43 -0
  322. package/src/shared/zoho-regions.js +12 -0
  323. package/test/ai.test.js +136 -0
  324. package/test/airtable.test.js +125 -0
  325. package/test/algolia.test.js +207 -0
  326. package/test/amazon-redshift.test.js +272 -0
  327. package/test/amplitude.test.js +239 -0
  328. package/test/apify.test.js +93 -0
  329. package/test/apollo-io.test.js +125 -0
  330. package/test/asana.test.js +99 -0
  331. package/test/ashby.test.js +97 -0
  332. package/test/atlassian.test.js +249 -0
  333. package/test/attention.test.js +79 -0
  334. package/test/aws.test.js +405 -0
  335. package/test/brevo.test.js +71 -0
  336. package/test/calendly.test.js +136 -0
  337. package/test/canva.test.js +258 -0
  338. package/test/chargebee.test.js +195 -0
  339. package/test/clickhouse.test.js +278 -0
  340. package/test/confidence.test.js +205 -0
  341. package/test/databricks.test.js +253 -0
  342. package/test/dbt-semantic-layer.test.js +221 -0
  343. package/test/designMcp.test.js +336 -0
  344. package/test/elevenlabs.test.js +52 -0
  345. package/test/firebase-cloud-messaging.test.js +242 -0
  346. package/test/firecrawl.test.js +66 -0
  347. package/test/gemini-enterprise.test.js +218 -0
  348. package/test/github-api.test.js +167 -0
  349. package/test/gitlab-api.test.js +160 -0
  350. package/test/gong.test.js +192 -0
  351. package/test/google-ads.test.js +350 -0
  352. package/test/google-maps-platform.test.js +191 -0
  353. package/test/google.test.js +710 -0
  354. package/test/granola.test.js +240 -0
  355. package/test/hex.test.js +253 -0
  356. package/test/heygen.test.js +121 -0
  357. package/test/hubspot.test.js +103 -0
  358. package/test/inngest.test.js +219 -0
  359. package/test/lightspeed.test.js +250 -0
  360. package/test/linear.test.js +146 -0
  361. package/test/linkedin.test.js +243 -0
  362. package/test/logo-dev.test.js +79 -0
  363. package/test/mailgun.test.js +107 -0
  364. package/test/mapbox.test.js +181 -0
  365. package/test/mcp.test.js +227 -0
  366. package/test/microsoft-fabric.test.js +258 -0
  367. package/test/microsoft.test.js +554 -0
  368. package/test/n8nOAuth.test.js +188 -0
  369. package/test/notionOAuth.test.js +183 -0
  370. package/test/oura.test.js +211 -0
  371. package/test/paddleCatalogue.test.js +51 -0
  372. package/test/paddlePermissions.test.js +101 -0
  373. package/test/perplexityComposition.test.js +39 -0
  374. package/test/pipedriveOAuth.test.js +140 -0
  375. package/test/posthog.test.js +200 -0
  376. package/test/prestashop.test.js +207 -0
  377. package/test/resend.test.js +163 -0
  378. package/test/salesforce.test.js +276 -0
  379. package/test/semrush.test.js +479 -0
  380. package/test/sentry.test.js +35 -0
  381. package/test/shopify.test.js +247 -0
  382. package/test/slack.test.js +320 -0
  383. package/test/snowflake.test.js +391 -0
  384. package/test/tally.test.js +75 -0
  385. package/test/telegram.test.js +58 -0
  386. package/test/tiktok.test.js +243 -0
  387. package/test/tokens.test.js +1079 -0
  388. package/test/twilio.test.js +221 -0
  389. package/test/twitch.test.js +573 -0
  390. package/test/wave.test.js +678 -0
  391. package/test/wix.test.js +1099 -0
  392. package/test/wiz.test.js +185 -0
  393. package/test/wordpress-com.test.js +469 -0
  394. package/test/wordpress.test.js +660 -0
  395. package/test/workday.test.js +314 -0
  396. package/test/x-twitter.test.js +256 -0
  397. package/test/xero.test.js +700 -0
  398. package/test/zoho-books.test.js +253 -0
  399. package/test/zoho-crm.test.js +223 -0
@@ -0,0 +1,1099 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { createConnectionService } from "../../connectors-core/src/server/index.js";
7
+ import { createFileConnectionStore, createCredentialProtection } from "../../connectors-core/src/server/fileStorage.js";
8
+ import { validateIntegrationConfiguration } from "../../connectors-core/src/shared/configuration.js";
9
+ import { wixProvider as provider } from "../src/server/wix.js";
10
+
11
+ const accountId = "01234567-89ab-cdef-0123-456789abcdef";
12
+ const siteId = "12345678-9abc-def0-1234-56789abcdef0";
13
+ const context = { applicationId: "app-one", subjectId: "team-one" };
14
+ const input = { context, integrationId: "wix" };
15
+ const sites = { sites: [{ id: siteId, displayName: "Fixture site", published: false }], metadata: { count: 1, cursors: { next: "fixture +/cursor==" } } };
16
+
17
+ test("Wix retrieves service choices without converting native prices or crossing service identities", async t => {
18
+ const { service, state, requests } = await fixture(t, { siteId });
19
+ await service.connectApiKey(input);
20
+ const variants = { id: siteId, serviceId: accountId, revision: "9007199254740993",
21
+ options: { values: [{ id: siteId, type: "CUSTOM", customData: { name: "Customer type", choices: ["Adult", "Child"] } }] },
22
+ variants: { values: [{ choices: [{ optionId: siteId, custom: "Child" }], price: { value: "15.10", currency: "AUD" } }] } };
23
+ state.response = { serviceVariants: variants };
24
+ const call = { ...input, operation: "bookingServices.getVariants", input: { serviceId: accountId } };
25
+ assert.deepEqual(await service.invoke(call), state.response);
26
+ assert.equal(requests.at(-1).url.pathname, `/bookings/v1/serviceOptionsAndVariants/service_id/${accountId}`);
27
+ assert.equal(requests.at(-1).init.method, "GET");
28
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
29
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
30
+ state.response = { serviceVariants: { ...variants, serviceId: siteId } };
31
+ await assert.rejects(service.invoke(call), { code: "connector_response_invalid" });
32
+ state.response = { serviceOptionsAndVariants: variants };
33
+ await assert.rejects(service.invoke(call), { code: "connector_response_invalid" });
34
+ state.response = { serviceVariants: { ...variants, variants: { values: [null] } } };
35
+ await assert.rejects(service.invoke(call), { code: "connector_response_invalid" });
36
+ let count = requests.length;
37
+ await assert.rejects(service.invoke({ ...call, input: { serviceId: "wrong" } }), { code: "connector_input_invalid" });
38
+ state.deny = true;
39
+ await assert.rejects(service.invoke(call), /Host denied/);
40
+ assert.equal(requests.length, count);
41
+ state.deny = false; state.status = 404; state.response = { message: "Not found" };
42
+ await assert.rejects(service.invoke(call));
43
+ assert.equal(requests.length, count + 1);
44
+ });
45
+
46
+ async function fixture(t, settings = {}) {
47
+ const directory = await mkdtemp(path.join(tmpdir(), "wix-connector-"));
48
+ t.after(() => rm(directory, { recursive: true, force: true }));
49
+ const protection = createCredentialProtection({ keys: { current: new Uint8Array(32).fill(7) }, activeKeyId: "current" });
50
+ const requests = [];
51
+ const state = { key: "fixture-wix-key", status: 200, response: sites, stall: false, deny: false };
52
+ const configuration = { schemaVersion: 1, integrations: {
53
+ wix: { provider: "wix", accountMode: "shared", scopes: [], authentication: { method: "api-key", secretRef: "env:WIX_KEY" },
54
+ settings: { accountId, ...settings }, extensions: { keep: true } }
55
+ }, registrations: {}, extensions: { fromCli: true } };
56
+ const options = { configuration, providers: [provider], authorize: async (owner) => { if (state.deny) throw new Error("Host denied"); return owner; },
57
+ resolveReference: async (ref) => { assert.equal(ref, "env:WIX_KEY"); return state.key; },
58
+ store: createFileConnectionStore({ directory, protection }),
59
+ fetchImpl: async (address, init) => {
60
+ requests.push({ url: new URL(String(address)), init, headers: new Headers(init.headers) });
61
+ if (state.stall) return new Promise((resolve, reject) => {
62
+ init.signal.throwIfAborted(); init.signal.addEventListener("abort", () => reject(init.signal.reason), { once: true });
63
+ });
64
+ return Response.json(state.response, { status: state.status });
65
+ }
66
+ };
67
+ return { service: createConnectionService(options), options, configuration, directory, protection, requests, state };
68
+ }
69
+
70
+ test("Wix validates account settings and secret references without opening an OAuth journey", async (t) => {
71
+ const { service, configuration, requests } = await fixture(t);
72
+ const validate = (config) => validateIntegrationConfiguration(config, { providers: [provider] });
73
+ assert.deepEqual(validate(configuration), configuration);
74
+ for (const value of [undefined, "", "not-a-guid", "https://attacker.invalid", `${accountId}\r\nheader`]) {
75
+ const config = structuredClone(configuration); config.integrations.wix.settings.accountId = value;
76
+ assert.throws(() => validate(config), (error) => Boolean(error.fieldErrors["integrations.wix.settings.accountId"]));
77
+ }
78
+ for (const change of [{ accountMode: "per-user" }, { scopes: ["sites.write"] }, { authentication: { method: "api-key", secretRef: "raw-secret" } }]) {
79
+ const config = structuredClone(configuration); Object.assign(config.integrations.wix, change);
80
+ assert.throws(() => validate(config), { code: "integration_configuration_invalid" });
81
+ }
82
+ const config = structuredClone(configuration); config.integrations.wix.accountMode = "assistant";
83
+ config.integrations.wix.settings.accountId = ` ${accountId} `;
84
+ assert.equal(validate(config).integrations.wix.settings.accountId, accountId);
85
+ await assert.rejects(service.beginAuthorization(input), { code: "connector_mode_unavailable" });
86
+ assert.equal(requests.length, 0);
87
+ });
88
+
89
+ test("Wix verifies with account headers, encrypts its file grant and resolves rotated keys", async (t) => {
90
+ const { service, options, directory, protection, requests, state } = await fixture(t);
91
+ const connected = await service.connectApiKey(input);
92
+ assert.equal(connected.status, "connected"); assert.equal(JSON.stringify(connected).includes(state.key), false);
93
+ const first = requests[0];
94
+ assert.equal(first.url.href, "https://www.wixapis.com/site-list/v2/sites/query"); assert.equal(first.init.method, "POST");
95
+ assert.equal(first.headers.get("authorization"), state.key); assert.equal(first.headers.get("wix-account-id"), accountId);
96
+ assert.equal(first.headers.has("wix-site-id"), false); assert.equal(first.init.redirect, "error"); assert.equal(first.init.credentials, "omit");
97
+ assert.deepEqual(JSON.parse(first.init.body), { query: { cursorPaging: { limit: 20 } } });
98
+ for (const name of await readdir(directory)) assert.equal((await readFile(path.join(directory, name), "utf8")).includes(state.key), false);
99
+ state.key = "rotated-wix-key";
100
+ const restarted = createConnectionService({ ...options, store: createFileConnectionStore({ directory, protection }) });
101
+ assert.equal((await restarted.status(input)).status, "reconnect-required");
102
+ assert.deepEqual(await restarted.invoke({ ...input, operation: "sites.list" }), sites);
103
+ assert.equal(requests.at(-1).headers.get("authorization"), state.key);
104
+ assert.equal((await restarted.status(input)).status, "connected");
105
+ await restarted.disconnect(input); assert.equal((await service.status(input)).status, "disconnected");
106
+ });
107
+
108
+ test("Wix reads explicit cursor pages and accepts empty accounts without replaying requests", async (t) => {
109
+ const { service, requests, state } = await fixture(t);
110
+ state.response = { sites: [] }; await service.connectApiKey(input);
111
+ state.response = sites;
112
+ assert.deepEqual(await service.invoke({ ...input, operation: "sites.list", input: { limit: 1 } }), sites);
113
+ state.response = { sites: [], metadata: { count: 0, cursors: {} } };
114
+ assert.deepEqual(await service.invoke({ ...input, operation: "sites.list", input: { limit: 50, cursor: sites.metadata.cursors.next } }), state.response);
115
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { query: { cursorPaging: { limit: 50, cursor: "fixture +/cursor==" } } });
116
+ assert.equal(requests.length, 3);
117
+ });
118
+
119
+ test("Wix rejects unknown or invalid inputs before network access and validates returned site/page fields", async (t) => {
120
+ const { service, requests, state } = await fixture(t); await service.connectApiKey(input);
121
+ const before = requests.length;
122
+ for (const values of [{ limit: 0 }, { limit: 101 }, { limit: 1.5 }, { cursor: "" }, { cursor: "x\nheader" }, { cursor: "x".repeat(16001) },
123
+ { accountId: siteId }, { url: "https://attacker.invalid" }, { query: { filter: {} } }]) {
124
+ await assert.rejects(service.invoke({ ...input, operation: "sites.list", input: values }), { code: "connector_input_invalid" });
125
+ }
126
+ await assert.rejects(service.invoke({ ...input, operation: "sites.publish" }), { code: "connector_operation_unknown" });
127
+ assert.equal(requests.length, before);
128
+ for (const response of [{}, { sites: {} }, { sites: [{ id: 123 }] }, { sites: [{ _id: siteId }] }, { sites: [{ id: siteId, published: "yes" }] },
129
+ { sites: [{ id: siteId, displayName: 3 }] }, { sites: [], metadata: { count: 1 } }, { sites: [], metadata: [] },
130
+ { sites: [], metadata: { cursors: { next: 7 } } }, { sites: Array.from({ length: 101 }, () => ({ id: siteId })) }]) {
131
+ state.response = response;
132
+ await assert.rejects(service.invoke({ ...input, operation: "sites.list" }), { code: "connector_response_invalid" });
133
+ }
134
+ });
135
+
136
+ test("Wix preserves owner isolation, host policy and account binding", async (t) => {
137
+ const { service, options, configuration, requests, state } = await fixture(t); await service.connectApiKey(input);
138
+ const before = requests.length;
139
+ for (const owner of [{ ...context, applicationId: "other-app" }, { ...context, subjectId: "other-team" }]) {
140
+ await assert.rejects(service.invoke({ ...input, context: owner, operation: "sites.list" }), { code: "connector_reconnect_required" });
141
+ }
142
+ state.deny = true; await assert.rejects(service.invoke({ ...input, operation: "sites.list" })); state.deny = false;
143
+ const changed = structuredClone(configuration); changed.integrations.wix.settings.accountId = siteId;
144
+ const moved = createConnectionService({ ...options, configuration: changed });
145
+ assert.equal((await moved.status(input)).status, "reconnect-required");
146
+ await assert.rejects(moved.invoke({ ...input, operation: "sites.list" }), { code: "connector_reconnect_required" });
147
+ assert.equal(requests.length, before);
148
+ });
149
+
150
+ test("Wix sanitizes provider failures, rejects invalid verification and preserves grants on cancellation", async (t) => {
151
+ const { service, options, state } = await fixture(t);
152
+ state.response = {};
153
+ await assert.rejects(service.connectApiKey(input), { code: "connector_response_invalid" });
154
+ assert.equal((await service.status(input)).status, "disconnected");
155
+ state.response = sites; await service.connectApiKey(input);
156
+ for (const [status, code] of [[403, "connector_permission_denied"], [429, "connector_rate_limited"], [500, "connector_provider_failed"]]) {
157
+ state.status = status; state.response = { message: "private-provider-detail" };
158
+ await assert.rejects(service.invoke({ ...input, operation: "sites.list" }), (error) => error.code === code && !error.message.includes("private-provider-detail"));
159
+ assert.equal((await service.status(input)).status, "connected");
160
+ }
161
+ state.stall = true; const controller = new AbortController(); controller.abort();
162
+ await assert.rejects(service.invoke({ ...input, operation: "sites.list", signal: controller.signal }), { code: "connector_cancelled" });
163
+ const timed = createConnectionService({ ...options, providers: [{ ...provider, requestTimeoutMs: 20 }] });
164
+ const keepAlive = setTimeout(() => {}, 1000);
165
+ try { await assert.rejects(timed.invoke({ ...input, operation: "sites.list" }), { code: "connector_provider_timeout" }); }
166
+ finally { clearTimeout(keepAlive); }
167
+ state.stall = false; state.status = 401;
168
+ await assert.rejects(service.invoke({ ...input, operation: "sites.list" }), { code: "connector_reconnect_required" });
169
+ assert.equal((await service.status(input)).status, "reconnect-required");
170
+ });
171
+
172
+ test("Wix's guide JSON validates without UI-specific transformations", async () => {
173
+ const guide = await readFile(new URL("../docs/wix.md", import.meta.url), "utf8");
174
+ const config = JSON.parse(guide.match(/```json\n([\s\S]*?)\n```/u)[1]);
175
+ assert.deepEqual(validateIntegrationConfiguration(config, { providers: [provider] }), config);
176
+ });
177
+
178
+
179
+ test("Wix CRM reads use the configured site, never both identity headers or caller overrides", async t => {
180
+ const { service, state, requests } = await fixture(t, { siteId });
181
+ await service.connectApiKey(input);
182
+ assert.equal(requests[0].headers.get("wix-account-id"), accountId);
183
+ assert.equal(requests[0].headers.has("wix-site-id"), false);
184
+ state.response = { contacts: [{ id: accountId, info: { name: { first: "Fixture" } } }], pagingMetadata: { count: 1, offset: 20, total: 21 } };
185
+ assert.deepEqual(await service.invoke({ ...input, operation: "contacts.list", input: { limit: 20, offset: 20, search: "Fixture" } }), state.response);
186
+ assert.equal(requests.at(-1).url.pathname, "/contacts/v4/contacts/query");
187
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
188
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
189
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { query: { paging: { limit: 20, offset: 20 } }, search: "Fixture" });
190
+ const before = requests.length;
191
+ for (const args of [{ siteId: accountId }, { limit: 101 }, { offset: -1 }, { search: "x".repeat(101) }]) {
192
+ await assert.rejects(service.invoke({ ...input, operation: "contacts.list", input: args }), { code: "connector_input_invalid" });
193
+ }
194
+ assert.equal(requests.length, before);
195
+ state.response = { contacts: [{ id: 3 }] };
196
+ await assert.rejects(service.invoke({ ...input, operation: "contacts.list" }), { code: "connector_response_invalid" });
197
+ const missing = await fixture(t); await missing.service.connectApiKey(input);
198
+ const missingBefore = missing.requests.length;
199
+ await assert.rejects(missing.service.invoke({ ...input, operation: "contacts.list" }), { code: "connector_input_invalid" });
200
+ assert.equal(missing.requests.length, missingBefore);
201
+ });
202
+
203
+
204
+ test("Wix contact creation and revision updates preserve site ownership and deliberate writes", async t => {
205
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
206
+ const contact = { id: accountId, revision: 1, info: { name: { first: "Ada" }, emails: { items: [{ email: "ada@example.test" }] } } };
207
+ state.response = { contact };
208
+ await service.invoke({ ...input, operation: "contacts.create", input: { info: contact.info } });
209
+ assert.equal(requests.at(-1).init.method, "POST");
210
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { info: contact.info, allowDuplicates: false });
211
+ await service.invoke({ ...input, operation: "contacts.get", input: { contactId: accountId } });
212
+ assert.equal(requests.at(-1).init.method, "GET");
213
+ state.response = { contact: { ...contact, revision: 2 } };
214
+ await service.invoke({ ...input, operation: "contacts.update", input: { contactId: accountId, revision: 1, info: { company: "Grooming" } } });
215
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
216
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
217
+ assert.equal(requests.at(-1).init.method, "PATCH");
218
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { revision: 1, info: { company: "Grooming" }, allowDuplicates: false });
219
+ const before = requests.length;
220
+ for (const args of [{ contactId: accountId, info: { company: "X" } }, { contactId: "../other", revision: 1, info: { company: "X" } }, { contactId: accountId, revision: 1, info: {} }]) {
221
+ await assert.rejects(service.invoke({ ...input, operation: "contacts.update", input: args }), { code: "connector_input_invalid" });
222
+ }
223
+ await assert.rejects(service.invoke({ ...input, operation: "contacts.create", input: { info: { company: "X" } } }), { code: "connector_input_invalid" });
224
+ assert.equal(requests.length, before);
225
+ state.status = 409; state.response = { message: "private-conflict" };
226
+ await assert.rejects(service.invoke({ ...input, operation: "contacts.update", input: { contactId: accountId, revision: 1, info: { company: "Y" } } }), error => !error.message.includes("private-conflict"));
227
+ assert.equal(requests.length, before + 1);
228
+ state.status = 200; state.response = { contact: { ...contact, id: siteId } };
229
+ await assert.rejects(service.invoke({ ...input, operation: "contacts.get", input: { contactId: accountId } }), { code: "connector_response_invalid" });
230
+ });
231
+
232
+
233
+ test("Wix booking-service reads and edits retain exact revisions and the site boundary", async t => {
234
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
235
+ const record = { id: accountId, name: "Grooming", revision: "9007199254740993", type: "APPOINTMENT" };
236
+ state.response = { services: [record], pagingMetadata: { count: 1, offset: 20, total: 21 } };
237
+ await service.invoke({ ...input, operation: "bookingServices.list", input: { offset: 20, limit: 10 } });
238
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { query: { paging: { offset: 20, limit: 10 } } });
239
+ state.response = { service: record };
240
+ await service.invoke({ ...input, operation: "bookingServices.get", input: { serviceId: accountId } });
241
+ assert.equal(requests.at(-1).init.method, "GET");
242
+ await service.invoke({ ...input, operation: "bookingServices.update", input: { serviceId: accountId, revision: record.revision, name: "Full grooming" } });
243
+ assert.equal(requests.at(-1).init.method, "PATCH");
244
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { service: { id: accountId, revision: record.revision, name: "Full grooming" } });
245
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId); assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
246
+ const before = requests.length;
247
+ for (const args of [{ serviceId: accountId, revision: 1, name: "X" }, { serviceId: accountId, revision: "1" }, { serviceId: "../bad", revision: "1", name: "X" }]) {
248
+ await assert.rejects(service.invoke({ ...input, operation: "bookingServices.update", input: args }), { code: "connector_input_invalid" });
249
+ }
250
+ assert.equal(requests.length, before);
251
+ state.response = { service: { ...record, id: siteId } };
252
+ await assert.rejects(service.invoke({ ...input, operation: "bookingServices.get", input: { serviceId: accountId } }), { code: "connector_response_invalid" });
253
+ state.status = 409; state.response = { message: "private-revision-conflict" };
254
+ const conflict = requests.length;
255
+ await assert.rejects(service.invoke({ ...input, operation: "bookingServices.update", input: { serviceId: accountId, revision: "1", description: "Change" } }), error => !error.message.includes("private-revision-conflict"));
256
+ assert.equal(requests.length, conflict + 1);
257
+ });
258
+
259
+
260
+ test("Wix booking cancellation preserves policy, revisions and explicit charge decisions", async t => {
261
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
262
+ const args = { bookingId: accountId, revision: "9007199254740993" };
263
+ state.response = { booking: { id: accountId, revision: "9007199254740994", status: "CANCELED" } };
264
+ await service.invoke({ ...input, operation: "bookings.cancel", input: args });
265
+ assert.equal(requests.at(-1).url.pathname, `/_api/bookings-service/v2/bookings/${accountId}/cancel`);
266
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
267
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
268
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { revision: args.revision, waiveCharges: false, participantNotification: { notifyParticipants: false } });
269
+ await service.invoke({ ...input, operation: "bookings.cancel", input: { ...args, notifyParticipants: true, message: "Please rebook", waiveCharges: true } });
270
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { revision: args.revision, waiveCharges: true, participantNotification: { notifyParticipants: true, message: "Please rebook" } });
271
+ const before = requests.length;
272
+ for (const bad of [{ ...args, revision: 1 }, { ...args, revision: "9223372036854775808" }, { ...args, bookingId: "../other" }, { ...args, message: "Hidden" }, { ...args, flowControlSettings: { ignoreCancellationPolicy: true } }]) {
273
+ await assert.rejects(service.invoke({ ...input, operation: "bookings.cancel", input: bad }), { code: "connector_input_invalid" });
274
+ }
275
+ assert.equal(requests.length, before);
276
+ state.status = 409; state.response = { message: "private-conflict" };
277
+ await assert.rejects(service.invoke({ ...input, operation: "bookings.cancel", input: args }), error => !error.message.includes("private-conflict"));
278
+ assert.equal(requests.length, before + 1);
279
+ state.status = 200;
280
+ for (const booking of [{ id: siteId, revision: "2", status: "CANCELED" }, { id: accountId, revision: "2", status: "CONFIRMED" }]) {
281
+ state.response = { booking };
282
+ await assert.rejects(service.invoke({ ...input, operation: "bookings.cancel", input: args }), { code: "connector_response_invalid" });
283
+ }
284
+ });
285
+
286
+
287
+ test("Wix booking queries expose current revisions and reject mismatched ID results", async t => {
288
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
289
+ const booking = { id: accountId, revision: "9007199254740993", status: "CONFIRMED" };
290
+ state.response = { extendedBookings: [{ booking, allowedActions: { cancel: true, reschedule: false } }], pagingMetadata: { count: 1, offset: 20 } };
291
+ await service.invoke({ ...input, operation: "bookings.list", input: { limit: 10, offset: 20 } });
292
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { query: { paging: { limit: 10, offset: 20 } }, withBookingAllowedActions: true });
293
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
294
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
295
+ await service.invoke({ ...input, operation: "bookings.list", input: { bookingId: accountId } });
296
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).query.filter, { id: accountId });
297
+ state.response = { extendedBookings: [{ booking: { ...booking, id: siteId } }] };
298
+ await assert.rejects(service.invoke({ ...input, operation: "bookings.list", input: { bookingId: accountId } }), { code: "connector_response_invalid" });
299
+ state.response = { extendedBookings: [] };
300
+ assert.deepEqual(await service.invoke({ ...input, operation: "bookings.list", input: { bookingId: accountId } }), state.response);
301
+ state.response = { extendedBookings: [{ booking, allowedActions: { cancel: "yes" } }] };
302
+ await assert.rejects(service.invoke({ ...input, operation: "bookings.list" }), { code: "connector_response_invalid" });
303
+ const before = requests.length;
304
+ await assert.rejects(service.invoke({ ...input, operation: "bookings.list", input: { bookingId: "bad" } }), { code: "connector_input_invalid" });
305
+ assert.equal(requests.length, before);
306
+ });
307
+
308
+
309
+ test("Wix appointment availability preserves local times, cursors and selected-slot identity", async t => {
310
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
311
+ const slot = { serviceId: accountId, localStartDate: "2026-10-01T10:00:00", localEndDate: "2026-10-01T11:00:00", bookable: true };
312
+ const args = { serviceId: accountId, timeZone: "Australia/Perth", fromLocalDate: "2026-10-01T00:00:00", toLocalDate: "2026-10-02T00:00:00" };
313
+ state.response = { timeSlots: [slot], timeZone: args.timeZone, cursorPagingMetadata: { cursors: { next: "next-page" } } };
314
+ await service.invoke({ ...input, operation: "availability.list", input: args });
315
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { ...args, bookable: true, cursorPaging: { limit: 100 } });
316
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
317
+ await service.invoke({ ...input, operation: "availability.list", input: { cursor: "next-page" } });
318
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { cursorPaging: { limit: 100, cursor: "next-page" } });
319
+ const selected = { serviceId: accountId, timeZone: args.timeZone, localStartDate: slot.localStartDate, localEndDate: slot.localEndDate };
320
+ state.response = { timeSlot: { ...slot, bookable: false }, timeZone: args.timeZone };
321
+ assert.equal((await service.invoke({ ...input, operation: "availability.get", input: selected })).timeSlot.bookable, false);
322
+ assert.equal(requests.at(-1).url.pathname, "/_api/service-availability/v2/time-slots/get");
323
+ state.response.timeSlot.serviceId = siteId;
324
+ await assert.rejects(service.invoke({ ...input, operation: "availability.get", input: selected }), { code: "connector_response_invalid" });
325
+ const before = requests.length;
326
+ for (const bad of [{ ...args, fromLocalDate: "2026-02-30T00:00:00" }, { ...args, timeZone: "bad-zone" }, { ...args, cursor: "next" }, { ...args, toLocalDate: args.fromLocalDate }]) {
327
+ await assert.rejects(service.invoke({ ...input, operation: "availability.list", input: bad }), { code: "connector_input_invalid" });
328
+ }
329
+ assert.equal(requests.length, before);
330
+ });
331
+
332
+
333
+ test("Wix creates appointment, class and course bookings without payment or policy overrides", async t => {
334
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
335
+ state.response = { booking: { id: accountId, revision: "1", status: "CREATED" } };
336
+ const common = { serviceId: accountId, timeZone: "Australia/Perth", formSubmission: { name: "Ada", email: "ada@example.test", custom: ["grooming"] } };
337
+ const appointment = { ...common, kind: "APPOINTMENT", startDate: "2026-10-01T10:00:00", endDate: "2026-10-01T11:00:00", resourceId: siteId, location: { locationType: "OWNER_BUSINESS", id: accountId } };
338
+ await service.invoke({ ...input, operation: "bookings.create", input: appointment });
339
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { booking: { bookedEntity: { slot: { serviceId: accountId, timezone: common.timeZone, startDate: appointment.startDate, endDate: appointment.endDate, resource: { id: siteId }, location: appointment.location } }, totalParticipants: 1 }, formSubmission: common.formSubmission, participantNotification: { notifyParticipants: false }, sendSmsReminder: false });
340
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
341
+ await service.invoke({ ...input, operation: "bookings.create", input: { ...common, kind: "CLASS", eventId: siteId, totalParticipants: 2, notifyParticipants: true, sendSmsReminder: true } });
342
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).booking, { bookedEntity: { slot: { serviceId: accountId, timezone: common.timeZone, eventId: siteId } }, totalParticipants: 2 });
343
+ await service.invoke({ ...input, operation: "bookings.create", input: { ...common, kind: "COURSE", scheduleId: siteId } });
344
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).booking.bookedEntity, { schedule: { serviceId: accountId, timezone: common.timeZone, scheduleId: siteId } });
345
+ const before = requests.length;
346
+ for (const bad of [{ ...appointment, eventId: siteId }, { ...appointment, resourceId: undefined }, { ...appointment, location: { locationType: "OWNER_BUSINESS" } }, { ...appointment, flowControlSettings: { skipAvailabilityValidation: true } }, { ...appointment, formSubmission: { invalid: Infinity } }]) {
347
+ await assert.rejects(service.invoke({ ...input, operation: "bookings.create", input: bad }), { code: "connector_input_invalid" });
348
+ }
349
+ assert.equal(requests.length, before);
350
+ state.status = 428; state.response = { message: "private-unavailable" };
351
+ await assert.rejects(service.invoke({ ...input, operation: "bookings.create", input: appointment }), error => !error.message.includes("private-unavailable"));
352
+ assert.equal(requests.length, before + 1);
353
+ });
354
+
355
+
356
+ test("Wix form summaries expose native booking targets on the configured site", async t => {
357
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
358
+ const formSummary = { id: accountId, fields: [{ id: siteId, target: "customer_name", label: "Name", type: "STRING", deleted: false, options: [] }] };
359
+ state.response = { formSummary };
360
+ assert.deepEqual(await service.invoke({ ...input, operation: "forms.summary", input: { formId: accountId } }), state.response);
361
+ assert.equal(requests.at(-1).init.method, "GET");
362
+ assert.equal(requests.at(-1).url.pathname, `/form-schema-service/v4/forms/${accountId}/summary`);
363
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
364
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
365
+ for (const bad of [{ ...formSummary, id: siteId }, { ...formSummary, fields: [{ id: siteId, target: 4, type: "STRING" }] }]) {
366
+ state.response = { formSummary: bad };
367
+ await assert.rejects(service.invoke({ ...input, operation: "forms.summary", input: { formId: accountId } }), { code: "connector_response_invalid" });
368
+ }
369
+ const before = requests.length;
370
+ await assert.rejects(service.invoke({ ...input, operation: "forms.summary", input: { formId: "../other" } }), { code: "connector_input_invalid" });
371
+ assert.equal(requests.length, before);
372
+ });
373
+
374
+
375
+ test("Wix class availability uses event APIs and explicit party-size filtering", async t => {
376
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
377
+ const args = { serviceId: accountId, timeZone: "Australia/Perth", fromLocalDate: "2026-10-01T00:00:00", toLocalDate: "2026-10-02T00:00:00", minBookableCapacity: 2 };
378
+ const slot = { serviceId: accountId, eventInfo: { eventId: siteId }, localStartDate: "2026-10-01T10:00:00", localEndDate: "2026-10-01T11:00:00", bookable: true };
379
+ state.response = { timeSlots: [slot], timeZone: args.timeZone, pagingMetadata: { cursors: { next: "next" } } };
380
+ await service.invoke({ ...input, operation: "classAvailability.list", input: args });
381
+ assert.equal(requests.at(-1).url.pathname, "/_api/service-availability/v2/time-slots/event");
382
+ const { serviceId, ...rest } = args;
383
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { ...rest, serviceIds: [serviceId], includeNonBookable: false, cursorPaging: { limit: 100 } });
384
+ await service.invoke({ ...input, operation: "classAvailability.list", input: { cursor: "next" } });
385
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { cursorPaging: { limit: 100, cursor: "next" } });
386
+ state.response = { timeSlot: slot, timeZone: args.timeZone };
387
+ await service.invoke({ ...input, operation: "classAvailability.get", input: { eventId: siteId, timeZone: args.timeZone } });
388
+ assert.equal(requests.at(-1).init.method, "GET");
389
+ assert.equal(requests.at(-1).url.searchParams.get("timeZone"), args.timeZone);
390
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
391
+ state.response.timeSlot = { ...slot, eventInfo: { eventId: accountId } };
392
+ await assert.rejects(service.invoke({ ...input, operation: "classAvailability.get", input: { eventId: siteId, timeZone: args.timeZone } }), { code: "connector_response_invalid" });
393
+ });
394
+
395
+
396
+ test("Wix selected-resource availability sends native location and resource filters", async t => {
397
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
398
+ const slot = { serviceId: accountId, localStartDate: "2026-10-01T10:00:00", localEndDate: "2026-10-01T11:00:00", bookable: true };
399
+ const location = { locationType: "BUSINESS", id: siteId };
400
+ const selected = { serviceId: accountId, timeZone: "Australia/Perth", localStartDate: slot.localStartDate, localEndDate: slot.localEndDate, location, resourceTypes: [{ resourceTypeId: accountId, resourceIds: [siteId] }] };
401
+ state.response = { timeSlot: slot, timeZone: selected.timeZone };
402
+ await service.invoke({ ...input, operation: "availability.get", input: selected });
403
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), selected);
404
+ state.response = { timeSlots: [slot], timeZone: selected.timeZone };
405
+ const list = { serviceId: accountId, timeZone: selected.timeZone, fromLocalDate: "2026-10-01T00:00:00", toLocalDate: "2026-10-02T00:00:00", locations: [location], includeResourceTypeIds: [accountId] };
406
+ await service.invoke({ ...input, operation: "availability.list", input: list });
407
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { ...list, bookable: true, cursorPaging: { limit: 100 } });
408
+ const before = requests.length;
409
+ await assert.rejects(service.invoke({ ...input, operation: "availability.get", input: { ...selected, location: { locationType: "BUSINESS" } } }), { code: "connector_input_invalid" });
410
+ await assert.rejects(service.invoke({ ...input, operation: "availability.get", input: { ...selected, resourceTypes: [{ resourceTypeId: accountId, resourceIds: ["bad"] }] } }), { code: "connector_input_invalid" });
411
+ assert.equal(requests.length, before);
412
+ });
413
+
414
+
415
+ test("Wix order reads preserve native money, scoped filters and cursor pages", async t => {
416
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
417
+ const order = { id: accountId, status: "APPROVED", currency: "AUD", priceSummary: { total: { amount: "123456789.01" } }, lineItems: [] };
418
+ state.response = { orders: [order], metadata: { count: 1, hasNext: true, cursors: { next: "orders-next" } } };
419
+ const page = await service.invoke({ ...input, operation: "orders.list", input: { status: "PENDING", contactId: siteId } });
420
+ assert.equal(page.orders[0].priceSummary.total.amount, "123456789.01");
421
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { search: { cursorPaging: { limit: 20 }, filter: { status: "PENDING", "buyerInfo.contactId": siteId } } });
422
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
423
+ await service.invoke({ ...input, operation: "orders.list", input: { cursor: "orders-next" } });
424
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { search: { cursorPaging: { limit: 20, cursor: "orders-next" } } });
425
+ state.response = { order };
426
+ await service.invoke({ ...input, operation: "orders.get", input: { orderId: accountId } });
427
+ assert.equal(requests.at(-1).init.method, "GET");
428
+ state.response = { order: { ...order, id: siteId } };
429
+ await assert.rejects(service.invoke({ ...input, operation: "orders.get", input: { orderId: accountId } }), { code: "connector_response_invalid" });
430
+ const before = requests.length;
431
+ await assert.rejects(service.invoke({ ...input, operation: "orders.list", input: { cursor: "orders-next", status: "PENDING" } }), { code: "connector_input_invalid" });
432
+ assert.equal(requests.length, before);
433
+ });
434
+
435
+
436
+ test("Wix catalog discovery distinguishes native versions and missing Stores", async t => {
437
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
438
+ for (const catalogVersion of ["V1_CATALOG", "V3_CATALOG", "STORES_NOT_INSTALLED"]) {
439
+ state.response = { catalogVersion };
440
+ assert.deepEqual(await service.invoke({ ...input, operation: "catalog.version" }), state.response);
441
+ }
442
+ assert.equal(requests.at(-1).url.pathname, "/stores/v3/provision/version");
443
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
444
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
445
+ state.response = { catalogVersion: "UNKNOWN" };
446
+ await assert.rejects(service.invoke({ ...input, operation: "catalog.version" }), { code: "connector_response_invalid" });
447
+ const before = requests.length;
448
+ await assert.rejects(service.invoke({ ...input, operation: "catalog.version", input: { siteId: accountId } }), { code: "connector_input_invalid" });
449
+ assert.equal(requests.length, before);
450
+ });
451
+
452
+
453
+ test("Wix product reads preserve each catalog version's endpoint, pagination and data", async t => {
454
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
455
+ const product = { id: accountId, name: "Shampoo", variantsInfo: { variants: [{ price: { actualPrice: { amount: "12.34" } } }] } };
456
+ state.response = { products: [{ id: accountId, name: "Shampoo", priceData: { currency: "AUD", price: 12.34 } }], metadata: { items: 1, offset: 20 }, totalResults: 21 };
457
+ await service.invoke({ ...input, operation: "productsV1.list", input: { offset: 20, includeVariants: true } });
458
+ assert.equal(requests.at(-1).url.pathname, "/stores-reader/v1/products/query");
459
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { query: { paging: { limit: 20, offset: 20 } }, includeVariants: true });
460
+ state.response = { products: [{ id: accountId, name: "Shampoo" }], pagingMetadata: { cursors: { next: "products-next" } } };
461
+ const page = await service.invoke({ ...input, operation: "productsV3.list", input: { cursor: "products-next" } });
462
+ assert.deepEqual(page.products[0], { id: accountId, name: "Shampoo" });
463
+ assert.equal(requests.at(-1).url.pathname, "/stores/v3/products/query");
464
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { query: { cursorPaging: { limit: 20, cursor: "products-next" } } });
465
+ state.response = { product };
466
+ for (const version of [1, 3]) {
467
+ await service.invoke({ ...input, operation: `productsV${version}.get`, input: { productId: accountId } });
468
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
469
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
470
+ }
471
+ const before = requests.length;
472
+ await assert.rejects(service.invoke({ ...input, operation: "productsV3.list", input: { offset: 20 } }), { code: "connector_input_invalid" });
473
+ await assert.rejects(service.invoke({ ...input, operation: "productsV1.list", input: { cursor: "next" } }), { code: "connector_input_invalid" });
474
+ assert.equal(requests.length, before);
475
+ state.response = { product: { ...product, id: siteId } };
476
+ await assert.rejects(service.invoke({ ...input, operation: "productsV3.get", input: { productId: accountId } }), { code: "connector_response_invalid" });
477
+ });
478
+
479
+
480
+ test("Wix V3 product edits preserve omitted fields and reject stale or uncertain writes without retry", async (t) => {
481
+ const { service, requests, state } = await fixture(t, { siteId });
482
+ await service.connectApiKey(input);
483
+ state.response = { product: { id: accountId, revision: "9007199254740994", name: "Renamed", visible: false } };
484
+ const change = { productId: accountId, revision: "9007199254740993", name: "Renamed", visible: false };
485
+ const result = await service.invoke({ ...input, operation: "productsV3.update", input: change });
486
+ assert.equal(result.product.revision, "9007199254740994");
487
+ assert.equal(requests.at(-1).init.method, "PATCH");
488
+ assert.equal(requests.at(-1).url.pathname, `/stores/v3/products/${accountId}`);
489
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { product: { id: accountId, revision: change.revision, name: "Renamed", visible: false } });
490
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
491
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
492
+ const count = requests.length;
493
+ for (const invalid of [{ productId: accountId, revision: "1" }, { ...change, revision: 1 }, { ...change, revision: "9223372036854775808" }, { ...change, name: "x".repeat(81) }, { ...change, variantsInfo: { variants: [] } }]) {
494
+ await assert.rejects(service.invoke({ ...input, operation: "productsV3.update", input: invalid }), { code: "connector_input_invalid" });
495
+ }
496
+ assert.equal(requests.length, count);
497
+ state.status = 409; state.response = { message: "Revision conflict" };
498
+ await assert.rejects(service.invoke({ ...input, operation: "productsV3.update", input: change }));
499
+ assert.equal(requests.length, count + 1);
500
+ state.status = 503; state.response = { message: "Unavailable" };
501
+ await assert.rejects(service.invoke({ ...input, operation: "productsV3.update", input: change }));
502
+ assert.equal(requests.length, count + 2);
503
+ state.status = 200; state.response = { product: { id: siteId, name: "Wrong", revision: "2" } };
504
+ await assert.rejects(service.invoke({ ...input, operation: "productsV3.update", input: change }), { code: "connector_response_invalid" });
505
+ });
506
+
507
+
508
+ test("Wix V1 updates use the write endpoint and retain zero price and false visibility", async t => {
509
+ const { service, requests, state } = await fixture(t, { siteId }); await service.connectApiKey(input);
510
+ state.response = { product: { id: accountId, name: "Free sample", visible: false, priceData: { price: 0 } } };
511
+ await service.invoke({ ...input, operation: "productsV1.update", input: { productId: accountId, name: "Free sample", visible: false, price: 0 } });
512
+ assert.equal(requests.at(-1).init.method, "PATCH");
513
+ assert.equal(requests.at(-1).url.pathname, `/stores/v1/products/${accountId}`);
514
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { product: { name: "Free sample", visible: false, priceData: { price: 0 } } });
515
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
516
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
517
+ await service.invoke({ ...input, operation: "productsV1.update", input: { productId: accountId, visible: true } });
518
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { product: { visible: true } });
519
+ const before = requests.length;
520
+ for (const change of [{}, { price: -1 }, { price: Infinity }, { price: "12.50" }, { price: 1000000000 }, { revision: "1", name: "Rename" }]) {
521
+ await assert.rejects(service.invoke({ ...input, operation: "productsV1.update", input: { productId: accountId, ...change } }), { code: "connector_input_invalid" });
522
+ }
523
+ assert.equal(requests.length, before);
524
+ state.response = { product: { id: siteId, name: "Wrong" } };
525
+ await assert.rejects(service.invoke({ ...input, operation: "productsV1.update", input: { productId: accountId, visible: false } }), { code: "connector_response_invalid" });
526
+ state.status = 503; state.response = { message: "Unavailable" };
527
+ await assert.rejects(service.invoke({ ...input, operation: "productsV1.update", input: { productId: accountId, price: 12.5 } }));
528
+ assert.equal(requests.length, before + 2);
529
+ });
530
+
531
+
532
+ test("Wix business locations preserve site identity, archived filtering and offset pages", async t => {
533
+ const { service, requests, state } = await fixture(t, { siteId }); await service.connectApiKey(input);
534
+ const location = { id: accountId, name: "Studio", timeZone: "Australia/Perth", archived: false, address: { formattedAddress: "Fixture address" } };
535
+ state.response = { locations: [location], pagingMetadata: { count: 1, offset: 20, total: 21 } };
536
+ const page = await service.invoke({ ...input, operation: "locations.list", input: { limit: 20, offset: 20 } });
537
+ assert.equal(page.locations[0].timeZone, "Australia/Perth");
538
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { query: { filter: { archived: false }, paging: { limit: 20, offset: 20 } }, filterAuthorizedLocationEntities: true });
539
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
540
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
541
+ state.response = { locations: [] };
542
+ assert.deepEqual((await service.invoke({ ...input, operation: "locations.list", input: { archived: true } })).locations, []);
543
+ state.response = { location };
544
+ await service.invoke({ ...input, operation: "locations.get", input: { locationId: accountId } });
545
+ assert.equal(requests.at(-1).url.pathname, `/locations/v1/locations/${accountId}`);
546
+ state.response = { location: { ...location, id: siteId } };
547
+ await assert.rejects(service.invoke({ ...input, operation: "locations.get", input: { locationId: accountId } }), { code: "connector_response_invalid" });
548
+ const count = requests.length;
549
+ await assert.rejects(service.invoke({ ...input, operation: "locations.list", input: { siteId } }), { code: "connector_input_invalid" });
550
+ assert.equal(requests.length, count);
551
+ });
552
+
553
+
554
+ test("Wix CMS collection reads preserve app collection paths and field metadata", async t => {
555
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
556
+ const collection = { id: "AppName/CollectionName", displayName: "Editorial content", fields: [{ key: "title", type: "TEXT" }] };
557
+ state.response = { collections: [collection], pagingMetadata: { count: 1, offset: 20 } };
558
+ const page = await service.invoke({ ...input, operation: "cmsCollections.list", input: { offset: 20, consistentRead: true } });
559
+ assert.deepEqual(page.collections[0].fields, collection.fields);
560
+ assert.equal(requests.at(-1).url.searchParams.get("paging.offset"), "20");
561
+ assert.equal(requests.at(-1).url.searchParams.get("consistentRead"), "true");
562
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
563
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
564
+ state.response = { collection };
565
+ await service.invoke({ ...input, operation: "cmsCollections.get", input: { collectionId: collection.id } });
566
+ assert.equal(requests.at(-1).url.pathname, "/wix-data/v2/collections/AppName/CollectionName");
567
+ state.response = { collection: { id: "Wrong" } };
568
+ await assert.rejects(service.invoke({ ...input, operation: "cmsCollections.get", input: { collectionId: collection.id } }), { code: "connector_response_invalid" });
569
+ const count = requests.length;
570
+ for (const id of ["", "../other", "App//Collection", "App/../Collection"]) await assert.rejects(service.invoke({ ...input, operation: "cmsCollections.get", input: { collectionId: id } }), { code: "connector_input_invalid" });
571
+ assert.equal(requests.length, count);
572
+ state.response = { collections: [] };
573
+ assert.deepEqual((await service.invoke({ ...input, operation: "cmsCollections.list" })).collections, []);
574
+ });
575
+
576
+
577
+ test("Wix CMS item pages retain native content and reject cross-collection results", async t => {
578
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
579
+ state.response = { dataItems: [{ id: "item-one", dataCollectionId: "Articles", data: { title: "Example", published: { $date: "2026-09-13T00:00:00.000Z" } } }], pagingMetadata: { count: 1, offset: 20 } };
580
+ const result = await service.invoke({ ...input, operation: "cmsItems.list", input: { collectionId: "Articles", offset: 20, consistentRead: true } });
581
+ assert.deepEqual(result.dataItems[0].data.published, { $date: "2026-09-13T00:00:00.000Z" });
582
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { dataCollectionId: "Articles", consistentRead: true, query: { paging: { limit: 20, offset: 20 } } });
583
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
584
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
585
+ state.response.dataItems[0].dataCollectionId = "PrivateRecords";
586
+ await assert.rejects(service.invoke({ ...input, operation: "cmsItems.list", input: { collectionId: "Articles" } }), { code: "connector_response_invalid" });
587
+ state.response = { dataItems: [] };
588
+ assert.deepEqual((await service.invoke({ ...input, operation: "cmsItems.list", input: { collectionId: "Articles" } })).dataItems, []);
589
+ const count = requests.length;
590
+ await assert.rejects(service.invoke({ ...input, operation: "cmsItems.list", input: { collectionId: "Articles", siteId } }), { code: "connector_input_invalid" });
591
+ assert.equal(requests.length, count);
592
+ });
593
+
594
+
595
+ test("Wix runtime site selection is bounded by project configuration and host authorization", async t => {
596
+ const { service, state, requests, configuration } = await fixture(t, { siteId, selectableSiteIds: accountId });
597
+ validateIntegrationConfiguration(configuration, { providers: [provider] });
598
+ await service.connectApiKey(input);
599
+ state.response = { collections: [] };
600
+ await service.invoke({ ...input, operation: "cmsCollections.list", input: { siteId: accountId } });
601
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), accountId);
602
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
603
+ await service.invoke({ ...input, operation: "cmsCollections.list" });
604
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
605
+ const before = requests.length;
606
+ await assert.rejects(service.invoke({ ...input, operation: "cmsCollections.list", input: { siteId } }), { code: "connector_input_invalid" });
607
+ await assert.rejects(service.invoke({ ...input, operation: "sites.list", input: { siteId: accountId } }), { code: "connector_input_invalid" });
608
+ assert.equal(requests.length, before);
609
+ state.deny = true;
610
+ await assert.rejects(service.invoke({ ...input, operation: "cmsCollections.list", input: { siteId: accountId } }));
611
+ assert.equal(requests.length, before);
612
+ for (const selectableSiteIds of ["", "bad-id", `${accountId},${accountId}`, `${accountId},`]) {
613
+ const bad = structuredClone(configuration); bad.integrations.wix.settings.selectableSiteIds = selectableSiteIds;
614
+ assert.throws(() => validateIntegrationConfiguration(bad, { providers: [provider] }));
615
+ }
616
+ });
617
+
618
+
619
+ test("Wix CMS queries preserve native filters, projections and bounded reference expansion", async t => {
620
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
621
+ const query = { collectionId: "Articles", filter: { $and: [{ published: true }, { rating: { $gte: 0 } }] },
622
+ sort: [{ fieldName: "title", order: "ASC" }], fields: ["title", "author"], returnTotalCount: true,
623
+ includeReferences: [{ field: "author", limit: 1 }], limit: 10, offset: 10 };
624
+ const author = { _id: "author-one", name: "Example author" };
625
+ state.response = { dataItems: [{ id: "article-one", dataCollectionId: "Articles", data: { title: "Example", author } }],
626
+ pagingMetadata: { count: 1, offset: 10, total: 11, tooManyToCount: false } };
627
+ const result = await service.invoke({ ...input, operation: "cmsItems.list", input: query });
628
+ assert.deepEqual(result.dataItems[0].data.author, author);
629
+ assert.equal(result.pagingMetadata.total, 11);
630
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { dataCollectionId: "Articles", consistentRead: false,
631
+ returnTotalCount: true, includeReferences: query.includeReferences,
632
+ query: { paging: { limit: 10, offset: 10 }, filter: query.filter, sort: query.sort, fields: query.fields } });
633
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
634
+ const count = requests.length;
635
+ for (const invalid of [{ filter: [] }, { filter: { value: Infinity } }, { filter: { value: "x".repeat(100001) } },
636
+ { sort: [{ fieldName: "title", order: "DOWN" }] }, { fields: [""] },
637
+ { includeReferences: [{ field: "author", limit: 1001 }] }, { includeReferencedItems: ["author"] }]) {
638
+ await assert.rejects(service.invoke({ ...input, operation: "cmsItems.list", input: { collectionId: "Articles", ...invalid } }), { code: "connector_input_invalid" });
639
+ }
640
+ assert.equal(requests.length, count);
641
+ state.response = { dataItems: [], pagingMetadata: { count: 0, tooManyToCount: true } };
642
+ const empty = await service.invoke({ ...input, operation: "cmsItems.list", input: { collectionId: "Articles", filter: {}, returnTotalCount: false } });
643
+ assert.equal(empty.pagingMetadata.total, undefined);
644
+ assert.equal(JSON.parse(requests.at(-1).init.body).returnTotalCount, false);
645
+ });
646
+
647
+
648
+ test("Wix CMS item reads and conditional patches retain item identity and never replay writes", async t => {
649
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
650
+ const dataItem = { id: "article-one", dataCollectionId: "Articles", data: { title: "Revised", enabled: false, count: 0, untouched: "kept" } };
651
+ state.response = { dataItem };
652
+ await service.invoke({ ...input, operation: "cmsItems.get", input: { collectionId: "Articles", itemId: dataItem.id, consistentRead: true } });
653
+ assert.equal(requests.at(-1).url.searchParams.get("dataCollectionId"), "Articles");
654
+ assert.equal(requests.at(-1).url.searchParams.get("consistentRead"), "true");
655
+ const fieldModifications = [
656
+ { fieldPath: "title", action: "SET_FIELD", setFieldOptions: { value: "Revised" } },
657
+ { fieldPath: "enabled", action: "SET_FIELD", setFieldOptions: { value: false } },
658
+ { fieldPath: "count", action: "INCREMENT_FIELD", incrementFieldOptions: { value: 0 } },
659
+ { fieldPath: "optional", action: "SET_FIELD", setFieldOptions: { value: null } },
660
+ { fieldPath: "old", action: "REMOVE_FIELD" },
661
+ { fieldPath: "tags", action: "APPEND_TO_ARRAY", appendToArrayOptions: { value: "new" } },
662
+ { fieldPath: "tags", action: "REMOVE_FROM_ARRAY", removeFromArrayOptions: { value: "old" } }
663
+ ];
664
+ const patch = { collectionId: "Articles", itemId: dataItem.id, fieldModifications, conditionFilter: { title: "Original" } };
665
+ const result = await service.invoke({ ...input, operation: "cmsItems.patch", input: patch });
666
+ assert.equal(result.dataItem.data.untouched, "kept");
667
+ assert.equal(requests.at(-1).init.method, "PATCH");
668
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
669
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
670
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { dataCollectionId: "Articles", patch: { dataItemId: dataItem.id, fieldModifications }, condition: { filter: { title: "Original" } } });
671
+ let count = requests.length;
672
+ for (const fieldModifications of [[], [{ fieldPath: "title", action: "SET_FIELD" }],
673
+ [{ fieldPath: "title", action: "REMOVE_FIELD", setFieldOptions: { value: "wrong" } }],
674
+ [{ fieldPath: "count", action: "INCREMENT_FIELD", incrementFieldOptions: { value: "1" } }],
675
+ [{ fieldPath: "title", action: "SET_FIELD", setFieldOptions: { value: Infinity } }]]) {
676
+ await assert.rejects(service.invoke({ ...input, operation: "cmsItems.patch", input: { ...patch, fieldModifications } }), { code: "connector_input_invalid" });
677
+ }
678
+ assert.equal(requests.length, count);
679
+ for (const status of [409, 503]) {
680
+ state.status = status; state.response = { message: "private-provider-error" }; count = requests.length;
681
+ await assert.rejects(service.invoke({ ...input, operation: "cmsItems.patch", input: patch }));
682
+ assert.equal(requests.length, count + 1);
683
+ }
684
+ state.status = 200;
685
+ for (const different of [{ ...dataItem, id: "other" }, { ...dataItem, dataCollectionId: "Private" }]) {
686
+ state.response = { dataItem: different };
687
+ await assert.rejects(service.invoke({ ...input, operation: "cmsItems.get", input: { collectionId: "Articles", itemId: dataItem.id } }), { code: "connector_response_invalid" });
688
+ await assert.rejects(service.invoke({ ...input, operation: "cmsItems.patch", input: patch }), { code: "connector_response_invalid" });
689
+ }
690
+ state.deny = true; count = requests.length;
691
+ await assert.rejects(service.invoke({ ...input, operation: "cmsItems.patch", input: patch }));
692
+ assert.equal(requests.length, count);
693
+ });
694
+
695
+
696
+ test("Wix location writes use native bodies, exact revisions and site-scoped single attempts", async t => {
697
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
698
+ const values = { name: "Perth studio", timeZone: "Australia/Perth", description: "Appointments", phone: "+61855550100",
699
+ businessSchedule: { periods: [], specialHourPeriod: [] }, extendedFields: { namespaces: { custom: { retained: true } } }, locationTypes: ["BRANCH"], status: "ACTIVE",
700
+ address: { country: "AU", city: "Perth", postalCode: "6000", geocode: { latitude: -31.95, longitude: 115.86 } } };
701
+ const location = { id: accountId, ...values, revision: "9007199254740993", archived: false };
702
+ state.response = { location };
703
+ await service.invoke({ ...input, operation: "locations.create", input: values });
704
+ assert.equal(requests.at(-1).init.method, "POST");
705
+ assert.equal(requests.at(-1).url.pathname, "/locations/v1/locations");
706
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { location: values });
707
+ const update = { ...values, locationId: accountId, revision: location.revision, description: "" };
708
+ await service.invoke({ ...input, operation: "locations.update", input: update });
709
+ assert.equal(requests.at(-1).init.method, "PUT");
710
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { location: { ...values, id: accountId, revision: location.revision, description: "" } });
711
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
712
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
713
+ let count = requests.length;
714
+ for (const bad of [{ revision: 7 }, { address: {} }, { timeZone: "bad-zone" },
715
+ { address: { geocode: { latitude: 91, longitude: 0 } } }, { archived: true }]) {
716
+ await assert.rejects(service.invoke({ ...input, operation: "locations.update", input: { ...update, ...bad } }), { code: "connector_input_invalid" });
717
+ }
718
+ assert.equal(requests.length, count);
719
+ state.status = 409; state.response = { message: "private-revision-conflict" };
720
+ await assert.rejects(service.invoke({ ...input, operation: "locations.update", input: update }));
721
+ assert.equal(requests.length, count + 1);
722
+ state.status = 503; count = requests.length;
723
+ await assert.rejects(service.invoke({ ...input, operation: "locations.create", input: values }));
724
+ assert.equal(requests.length, count + 1);
725
+ state.status = 200; state.response = { location: { ...location, id: siteId } };
726
+ await assert.rejects(service.invoke({ ...input, operation: "locations.update", input: update }), { code: "connector_response_invalid" });
727
+ });
728
+
729
+
730
+ test("Wix archive and default actions require explicit IDs and validate identity and resulting state", async t => {
731
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
732
+ for (const [operation, suffix, flag] of [["locations.archive", "archive", "archived"], ["locations.setDefault", "set-default", "default"]]) {
733
+ state.response = { location: { id: accountId, name: "Studio", [flag]: true } };
734
+ await service.invoke({ ...input, operation, input: { locationId: accountId } });
735
+ assert.equal(requests.at(-1).url.pathname, `/locations/v1/locations/${accountId}/${suffix}`);
736
+ assert.equal(requests.at(-1).init.method, "POST");
737
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), {});
738
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
739
+ state.response.location[flag] = false;
740
+ await assert.rejects(service.invoke({ ...input, operation, input: { locationId: accountId } }), { code: "connector_response_invalid" });
741
+ state.response.location[flag] = true; state.response.location.id = siteId;
742
+ await assert.rejects(service.invoke({ ...input, operation, input: { locationId: accountId } }), { code: "connector_response_invalid" });
743
+ }
744
+ let count = requests.length;
745
+ await assert.rejects(service.invoke({ ...input, operation: "locations.archive", input: { locationId: "bad" } }), { code: "connector_input_invalid" });
746
+ assert.equal(requests.length, count);
747
+ state.status = 400; state.response = { message: "Cannot archive default location" };
748
+ await assert.rejects(service.invoke({ ...input, operation: "locations.archive", input: { locationId: accountId } }));
749
+ assert.equal(requests.length, count + 1);
750
+ state.deny = true; count = requests.length;
751
+ await assert.rejects(service.invoke({ ...input, operation: "locations.setDefault", input: { locationId: accountId } }));
752
+ assert.equal(requests.length, count);
753
+ });
754
+
755
+
756
+ test("Wix creates services with explicit payment and appointment prerequisites", async t => {
757
+ const { service: connection, state, requests } = await fixture(t, { siteId }); await connection.connectApiKey(input);
758
+ const service = { type: "APPOINTMENT", name: "Consultation", defaultCapacity: 1,
759
+ onlineBooking: { enabled: true, requireManualApproval: false, allowMultipleRequests: false },
760
+ payment: { rateType: "FIXED", fixed: { price: { value: "50.25", currency: "AUD" } }, options: { inPerson: true, online: false } },
761
+ staffMemberIds: [accountId], schedule: { availabilityConstraints: { sessionDurations: [30] } }, category: { id: accountId } };
762
+ state.response = { service: { ...service, id: accountId, revision: "1" } };
763
+ await connection.invoke({ ...input, operation: "bookingServices.create", input: { service } });
764
+ assert.equal(requests.at(-1).url.pathname, "/_api/bookings/v2/services");
765
+ assert.equal(requests.at(-1).init.method, "POST");
766
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { service });
767
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
768
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
769
+ for (const type of ["CLASS", "COURSE"]) {
770
+ const group = { type, name: "Workshop", defaultCapacity: 12, onlineBooking: { enabled: false }, payment: { rateType: "NO_FEE", options: { inPerson: true } } };
771
+ state.response = { service: { ...group, id: accountId, revision: "1" } };
772
+ await connection.invoke({ ...input, operation: "bookingServices.create", input: { service: group } });
773
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { service: group });
774
+ }
775
+ let count = requests.length;
776
+ for (const changed of [{ defaultCapacity: 2 }, { staffMemberIds: [] }, { schedule: {} }, { id: accountId },
777
+ { payment: { rateType: "NO_FEE", options: {} } },
778
+ { payment: { rateType: "NO_FEE", options: { online: true } } },
779
+ { payment: { rateType: "FIXED", fixed: { price: { value: 50.25, currency: "AUD" } }, options: { inPerson: true } } },
780
+ { onlineBooking: { enabled: true, requireManualApproval: true }, payment: { ...service.payment, options: { inPerson: true, pricingPlan: true } } }]) {
781
+ await assert.rejects(connection.invoke({ ...input, operation: "bookingServices.create", input: { service: { ...service, ...changed } } }), { code: "connector_input_invalid" });
782
+ }
783
+ assert.equal(requests.length, count);
784
+ state.status = 503; state.response = { message: "uncertain-create" };
785
+ await assert.rejects(connection.invoke({ ...input, operation: "bookingServices.create", input: { service } }));
786
+ assert.equal(requests.length, count + 1);
787
+ state.deny = true; count = requests.length;
788
+ await assert.rejects(connection.invoke({ ...input, operation: "bookingServices.create", input: { service } }));
789
+ assert.equal(requests.length, count);
790
+ });
791
+
792
+
793
+ test("Wix booking setup discovers categories and staff resource IDs with native cursor pages", async t => {
794
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
795
+ state.response = { categories: [{ id: accountId, name: "Consultations" }], pagingMetadata: { hasNext: true, cursors: { next: "category-next" } } };
796
+ await service.invoke({ ...input, operation: "bookingCategories.list", input: { cursor: "category-next", limit: 5 } });
797
+ assert.equal(requests.at(-1).url.pathname, "/bookings/v2/categories/query");
798
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { query: { cursorPaging: { cursor: "category-next", limit: 5 }, sort: [{ fieldName: "id", order: "ASC" }] } });
799
+ const member = { id: accountId, name: "Provider", resourceId: siteId, resource: { eventsSchedule: { id: accountId } } };
800
+ state.response = { staffMembers: [member], pagingMetadata: { hasNext: false, cursors: {} } };
801
+ const result = await service.invoke({ ...input, operation: "bookingStaff.list" });
802
+ assert.equal(result.staffMembers[0].resourceId, siteId);
803
+ assert.deepEqual(result.staffMembers[0].resource, member.resource);
804
+ assert.equal(requests.at(-1).url.pathname, "/bookings/v1/staff-members/query");
805
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { query: { cursorPaging: { limit: 20 }, sort: [{ fieldName: "id", order: "ASC" }], filter: { serviceProvider: true } }, fields: ["RESOURCE_DETAILS"] });
806
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
807
+ delete state.response.staffMembers[0].resourceId;
808
+ await assert.rejects(service.invoke({ ...input, operation: "bookingStaff.list" }), { code: "connector_response_invalid" });
809
+ state.response = { staffMembers: [], pagingMetadata: { hasNext: false } };
810
+ assert.deepEqual((await service.invoke({ ...input, operation: "bookingStaff.list" })).staffMembers, []);
811
+ const count = requests.length;
812
+ await assert.rejects(service.invoke({ ...input, operation: "bookingStaff.list", input: { filter: { serviceProvider: false } } }), { code: "connector_input_invalid" });
813
+ assert.equal(requests.length, count);
814
+ });
815
+
816
+
817
+ test("Wix service configuration updates preserve payment strings, false settings and exact revisions", async t => {
818
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
819
+ const changes = { serviceId: accountId, revision: "9007199254740993",
820
+ payment: { rateType: "FIXED", fixed: { price: { value: "75.10", currency: "AUD" } }, options: { inPerson: true, online: false } },
821
+ onlineBooking: { enabled: true, requireManualApproval: false, allowMultipleRequests: false }, defaultCapacity: 1,
822
+ staffMemberIds: [siteId], schedule: { availabilityConstraints: { sessionDurations: [45], timeBetweenSessions: 0 } } };
823
+ state.response = { service: { id: accountId, name: "Consultation", type: "APPOINTMENT", revision: "9007199254740994" } };
824
+ await service.invoke({ ...input, operation: "bookingServices.update", input: changes });
825
+ const { serviceId, ...settings } = changes;
826
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { service: { id: serviceId, ...settings } });
827
+ assert.equal(requests.at(-1).init.method, "PATCH");
828
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
829
+ let count = requests.length;
830
+ for (const bad of [{ payment: {} }, { onlineBooking: {} }, { schedule: { availabilityConstraints: {} } },
831
+ { staffMemberIds: ["bad-id"] }, { payment: { invalid: Infinity } }, { locations: [] }, { addOnGroups: [] }]) {
832
+ await assert.rejects(service.invoke({ ...input, operation: "bookingServices.update", input: { serviceId: accountId, revision: "2", ...bad } }), { code: "connector_input_invalid" });
833
+ }
834
+ assert.equal(requests.length, count);
835
+ state.status = 409; state.response = { message: "Stale revision" };
836
+ await assert.rejects(service.invoke({ ...input, operation: "bookingServices.update", input: changes }));
837
+ assert.equal(requests.length, count + 1);
838
+ });
839
+
840
+
841
+ test("Wix service locations require an explicit session decision and opt-in notifications", async t => {
842
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
843
+ const location = { type: "BUSINESS", business: { id: siteId } };
844
+ const values = { serviceId: accountId, locations: [location], removedLocationAction: "KEEP_AT_CURRENT_LOCATION" };
845
+ state.response = { service: { id: accountId, type: "CLASS", name: "Class", revision: "2", locations: [location] } };
846
+ await service.invoke({ ...input, operation: "bookingServices.setLocations", input: values });
847
+ assert.equal(requests.at(-1).url.pathname, `/_api/bookings/v2/services/${accountId}/locations`);
848
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { locations: [location], removedLocationSessionsAction: { action: "KEEP_AT_CURRENT_LOCATION" }, participantNotification: { notifyParticipants: false } });
849
+ const destination = { type: "CUSTOM", custom: { address: { country: "AU", city: "Perth", formattedAddress: "Fixture address" } } };
850
+ await service.invoke({ ...input, operation: "bookingServices.setLocations", input: { ...values, removedLocationAction: "MOVE_TO_LOCATION", moveToLocation: destination, notifyParticipants: true, notificationMessage: "New venue" } });
851
+ const body = JSON.parse(requests.at(-1).init.body);
852
+ assert.deepEqual(body.removedLocationSessionsAction, { action: "MOVE_TO_LOCATION", moveToLocationOptions: { newLocation: destination } });
853
+ assert.deepEqual(body.participantNotification, { notifyParticipants: true, message: "New venue" });
854
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
855
+ let count = requests.length;
856
+ for (const bad of [{ removedLocationAction: undefined }, { removedLocationAction: "MOVE_TO_LOCATION" },
857
+ { moveToLocation: destination }, { notificationMessage: "Do not send implicitly" },
858
+ { locations: [{ type: "BUSINESS" }] }, { locations: [{ type: "CUSTOM", custom: { address: {} } }] }]) {
859
+ await assert.rejects(service.invoke({ ...input, operation: "bookingServices.setLocations", input: { ...values, ...bad } }), { code: "connector_input_invalid" });
860
+ }
861
+ assert.equal(requests.length, count);
862
+ state.response.service.id = siteId;
863
+ await assert.rejects(service.invoke({ ...input, operation: "bookingServices.setLocations", input: values }), { code: "connector_response_invalid" });
864
+ state.status = 503; state.response = { message: "Uncertain location change" }; count = requests.length;
865
+ await assert.rejects(service.invoke({ ...input, operation: "bookingServices.setLocations", input: values }));
866
+ assert.equal(requests.length, count + 1);
867
+ });
868
+
869
+ test("Wix forwards duration and add-on availability choices and mutually exclusive booking participants", async t => {
870
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
871
+ const slot = { serviceId: accountId, localStartDate: "2026-10-01T10:00:00", localEndDate: "2026-10-01T11:30:00", bookable: true };
872
+ const args = { serviceId: accountId, timeZone: "Australia/Perth", fromLocalDate: "2026-10-01T00:00:00", toLocalDate: "2026-10-02T00:00:00" };
873
+ const customerChoices = { durationInMinutes: 90, addOnIds: [siteId] };
874
+ state.response = { timeSlots: [slot], timeZone: args.timeZone };
875
+ await service.invoke({ ...input, operation: "availability.list", input: { ...args, customerChoices } });
876
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).customerChoices, customerChoices);
877
+ state.response = { timeSlot: slot, timeZone: args.timeZone };
878
+ await service.invoke({ ...input, operation: "availability.get", input: { serviceId: accountId, timeZone: args.timeZone,
879
+ localStartDate: slot.localStartDate, localEndDate: slot.localEndDate, customerChoices: { addOnIds: [siteId] } } });
880
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).customerChoices, { addOnIds: [siteId] });
881
+ let count = requests.length;
882
+ for (const bad of [{}, { durationInMinutes: 0 }, { durationInMinutes: 57601 }, { addOnIds: [siteId, siteId] }, { addOnIds: ["bad"] }]) {
883
+ await assert.rejects(service.invoke({ ...input, operation: "availability.list", input: { ...args, customerChoices: bad } }), { code: "connector_input_invalid" });
884
+ }
885
+ assert.equal(requests.length, count);
886
+ const participantsChoices = { serviceChoices: [
887
+ { numberOfParticipants: 2, choices: [{ optionId: siteId, custom: "Child" }] },
888
+ { numberOfParticipants: 1, choices: [{ optionId: siteId, custom: "Adult" }] }
889
+ ] };
890
+ const booking = { serviceId: accountId, kind: "CLASS", eventId: siteId, timeZone: args.timeZone, formSubmission: { name: "Ada" },
891
+ participantsChoices, bookedAddOns: [{ id: siteId, groupId: accountId, quantity: 2 }] };
892
+ state.response = { booking: { id: accountId, revision: "1", status: "CREATED" } };
893
+ await service.invoke({ ...input, operation: "bookings.create", input: booking });
894
+ const sent = JSON.parse(requests.at(-1).init.body);
895
+ assert.deepEqual(sent.booking.participantsChoices, participantsChoices);
896
+ assert.deepEqual(sent.booking.bookedAddOns, booking.bookedAddOns);
897
+ assert.equal(Object.hasOwn(sent.booking, "totalParticipants"), false);
898
+ assert.deepEqual(sent.participantNotification, { notifyParticipants: false });
899
+ const duration = { serviceChoices: [{ numberOfParticipants: 1, choices: [{ optionId: siteId, duration: { minutes: 90 } }] }] };
900
+ await service.invoke({ ...input, operation: "bookings.create", input: { ...booking, participantsChoices: duration, bookedAddOns: [{ id: siteId }] } });
901
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).booking.participantsChoices, duration);
902
+ count = requests.length;
903
+ for (const bad of [{ totalParticipants: 3 }, { participantsChoices: { serviceChoices: [] } }, { bookedAddOns: [null] },
904
+ { bookedAddOns: [{ id: siteId, quantity: 0 }] }, { bookedAddOns: [{ id: siteId }, { id: siteId }] },
905
+ { participantsChoices: { serviceChoices: [{ numberOfParticipants: 1, choices: [{ optionId: siteId }] }] } },
906
+ { participantsChoices: { serviceChoices: [{ numberOfParticipants: 1, choices: [{ optionId: siteId, custom: "Child", duration: { minutes: 90 } }] }] } }]) {
907
+ await assert.rejects(service.invoke({ ...input, operation: "bookings.create", input: { ...booking, ...bad } }), { code: "connector_input_invalid" });
908
+ }
909
+ assert.equal(requests.length, count);
910
+ state.status = 400; state.response = { message: "Invalid service choice" };
911
+ await assert.rejects(service.invoke({ ...input, operation: "bookings.create", input: booking }));
912
+ assert.equal(requests.length, count + 1);
913
+ });
914
+
915
+ test("Wix add-on discovery preserves group prompts, order and selection limits", async t => {
916
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
917
+ const groups = [{ groupId: accountId, groupName: "Extras", prompt: "Choose one", maxNumberOfAddOns: 1,
918
+ addOns: [{ addOnId: siteId, name: "Extra time", durationInMinutes: 30, price: { value: "12.50", currency: "AUD" } }] }];
919
+ state.response = { addOnGroupsDetails: groups };
920
+ const call = { ...input, operation: "bookingServices.listAddOnGroups", input: { serviceId: accountId } };
921
+ assert.deepEqual(await service.invoke(call), state.response);
922
+ assert.equal(requests.at(-1).url.pathname, "/_api/bookings/v2/services/add-on-groups/list-add-on-groups-by-service-id");
923
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { serviceId: accountId });
924
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
925
+ state.response = { addOnGroupsDetails: [] };
926
+ assert.deepEqual((await service.invoke(call)).addOnGroupsDetails, []);
927
+ state.response = { addOnGroupsDetails: [{ ...groups[0], addOns: [null] }] };
928
+ await assert.rejects(service.invoke(call), { code: "connector_response_invalid" });
929
+ const before = requests.length;
930
+ await assert.rejects(service.invoke({ ...call, input: { serviceId: "bad" } }), { code: "connector_input_invalid" });
931
+ state.deny = true;
932
+ await assert.rejects(service.invoke(call), /Host denied/);
933
+ assert.equal(requests.length, before);
934
+ });
935
+
936
+ test("Wix custom-checkout confirmation distinguishes automatic checks from explicit decisions", async t => {
937
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
938
+ const decision = { bookingId: accountId, revision: "9007199254740993" };
939
+ state.response = { booking: { id: accountId, revision: "9007199254740994", status: "CONFIRMED" } };
940
+ await service.invoke({ ...input, operation: "bookings.confirm", input: decision });
941
+ assert.equal(requests.at(-1).url.pathname, `/_api/bookings-service/v2/bookings/${accountId}/confirm`);
942
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { revision: decision.revision, participantNotification: { notifyParticipants: false } });
943
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
944
+ state.response.booking.status = "DECLINED";
945
+ await service.invoke({ ...input, operation: "bookings.decline", input: { ...decision, notifyParticipants: true, message: "Please choose another time." } });
946
+ assert.equal(requests.at(-1).url.pathname, `/_api/bookings-service/v2/bookings/${accountId}/decline`);
947
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body).participantNotification, { notifyParticipants: true, message: "Please choose another time." });
948
+ const automatic = { bookingId: accountId, paymentStatus: "EXEMPT" };
949
+ for (const status of ["CONFIRMED", "PENDING", "DECLINED"]) {
950
+ state.response.booking.status = status;
951
+ assert.equal((await service.invoke({ ...input, operation: "bookings.confirmOrDecline", input: automatic })).booking.status, status);
952
+ assert.equal(requests.at(-1).url.pathname, `/bookings/v2/confirmation/${accountId}:confirmOrDecline`);
953
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { paymentStatus: "EXEMPT" });
954
+ }
955
+ let count = requests.length;
956
+ for (const bad of [{ revision: 5 }, { revision: "9223372036854775808" }, { message: "Do not send implicitly" }, { paymentStatus: "PAID" }, { skipAvailabilityValidation: true }]) {
957
+ await assert.rejects(service.invoke({ ...input, operation: "bookings.confirm", input: { ...decision, ...bad } }), { code: "connector_input_invalid" });
958
+ }
959
+ await assert.rejects(service.invoke({ ...input, operation: "bookings.confirmOrDecline", input: { bookingId: accountId } }), { code: "connector_input_invalid" });
960
+ state.deny = true;
961
+ await assert.rejects(service.invoke({ ...input, operation: "bookings.confirm", input: decision }), /Host denied/);
962
+ assert.equal(requests.length, count);
963
+ state.deny = false; state.response.booking.id = siteId;
964
+ await assert.rejects(service.invoke({ ...input, operation: "bookings.decline", input: decision }), { code: "connector_response_invalid" });
965
+ state.response.booking.id = accountId; state.response.booking.status = "CREATED";
966
+ await assert.rejects(service.invoke({ ...input, operation: "bookings.confirmOrDecline", input: automatic }), { code: "connector_response_invalid" });
967
+ state.status = 428; state.response = { message: "Booking cannot be confirmed" }; count = requests.length;
968
+ await assert.rejects(service.invoke({ ...input, operation: "bookings.confirm", input: decision }));
969
+ assert.equal(requests.length, count + 1);
970
+ });
971
+
972
+ test("Wix rescheduling preserves selected appointment slots or class events without policy overrides", async t => {
973
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
974
+ state.response = { booking: { id: accountId, revision: "9007199254740994", status: "PENDING" } };
975
+ const slot = { serviceId: accountId, scheduleId: siteId, startDate: "2026-10-01T10:00:00", endDate: "2026-10-01T11:00:00",
976
+ timezone: "Australia/Perth", resource: { id: siteId }, location: { locationType: "OWNER_BUSINESS", id: accountId } };
977
+ const args = { bookingId: accountId, revision: "9007199254740993", slot };
978
+ await service.invoke({ ...input, operation: "bookings.reschedule", input: args });
979
+ assert.equal(requests.at(-1).url.pathname, `/_api/bookings-service/v2/bookings/${accountId}/reschedule`);
980
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { revision: args.revision, slot, participantNotification: { notifyParticipants: false } });
981
+ await service.invoke({ ...input, operation: "bookings.reschedule", input: { ...args, slot: { eventId: siteId }, notifyParticipants: true, message: "New class time" } });
982
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { revision: args.revision, slot: { eventId: siteId }, participantNotification: { notifyParticipants: true, message: "New class time" } });
983
+ let count = requests.length;
984
+ for (const bad of [{ slot: {} }, { slot: { eventId: siteId, serviceId: accountId } }, { slot: { ...slot, timezone: "bad" } },
985
+ { slot: { ...slot, endDate: slot.startDate } }, { message: "No implicit notification" }, { flowControlSettings: { ignoreReschedulePolicy: true } }]) {
986
+ await assert.rejects(service.invoke({ ...input, operation: "bookings.reschedule", input: { ...args, ...bad } }), { code: "connector_input_invalid" });
987
+ }
988
+ assert.equal(requests.length, count);
989
+ state.response.booking.id = siteId;
990
+ await assert.rejects(service.invoke({ ...input, operation: "bookings.reschedule", input: args }), { code: "connector_response_invalid" });
991
+ state.status = 428; state.response = { message: "Slot not available" }; count = requests.length;
992
+ await assert.rejects(service.invoke({ ...input, operation: "bookings.reschedule", input: args }));
993
+ assert.equal(requests.length, count + 1);
994
+ });
995
+
996
+ test("Wix Cart V2 creates catalogue carts and returns HTTPS hosted checkout without collecting payment", async t => {
997
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
998
+ const catalogItems = [{ quantity: 1, catalogReference: { appId: "13d21c63-b5ec-5912-8397-c3a5ddb27a97", catalogItemId: accountId,
999
+ options: { customText: "Fixture booking", enabled: false } } }];
1000
+ state.response = { cart: { id: accountId, revision: "1", lineItems: [{ id: siteId, pricing: { totalPrice: { amount: "30.10" } } }] } };
1001
+ assert.deepEqual(await service.invoke({ ...input, operation: "carts.create", input: { catalogItems, note: "" } }), state.response);
1002
+ assert.equal(requests.at(-1).url.pathname, "/ecom/v2/carts");
1003
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { cart: { source: { channelType: "WEB" }, note: "" }, catalogItems });
1004
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
1005
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
1006
+ state.response = { checkoutUrl: `https://shop.example.test/checkout?checkoutId=${accountId}` };
1007
+ const urlCall = { ...input, operation: "carts.getCheckoutUrl", input: { cartId: accountId } };
1008
+ assert.equal((await service.invoke(urlCall)).checkoutUrl, state.response.checkoutUrl);
1009
+ assert.equal(requests.at(-1).url.pathname, `/ecom/v2/carts/${accountId}/get-checkout-url`);
1010
+ assert.equal(requests.at(-1).init.method, "POST");
1011
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), {});
1012
+ let count = requests.length;
1013
+ for (const bad of [{ catalogItems: [] }, { catalogItems: [{ ...catalogItems[0], quantity: 0 }] },
1014
+ { catalogItems: [{ quantity: 1, catalogReference: { appId: "bad", catalogItemId: accountId } }] },
1015
+ { catalogItems, customItems: [{ price: "0" }] }, { catalogItems, paymentStatus: "PAID" }]) {
1016
+ await assert.rejects(service.invoke({ ...input, operation: "carts.create", input: bad }), { code: "connector_input_invalid" });
1017
+ }
1018
+ state.deny = true;
1019
+ await assert.rejects(service.invoke(urlCall), /Host denied/);
1020
+ assert.equal(requests.length, count);
1021
+ state.deny = false;
1022
+ for (const checkoutUrl of ["http://shop.example.test", "javascript:alert(1)", "https://secret@shop.example.test", null]) {
1023
+ state.response = { checkoutUrl };
1024
+ await assert.rejects(service.invoke(urlCall), { code: "connector_response_invalid" });
1025
+ }
1026
+ state.status = 428; state.response = { message: "Insufficient inventory" }; count = requests.length;
1027
+ await assert.rejects(service.invoke({ ...input, operation: "carts.create", input: { catalogItems } }));
1028
+ assert.equal(requests.length, count + 1);
1029
+ });
1030
+
1031
+
1032
+ test("Wix reads order fulfillments without mixing order identities or losing native tracking details", async t => {
1033
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
1034
+ const fulfillment = { id: siteId, lineItems: [{ id: accountId, quantity: 2 }], status: "In_Delivery", completed: false,
1035
+ trackingInfo: { trackingNumber: "fixture-123", shippingProvider: "ups", trackingLink: "https://tracking.example.test/fixture-123" } };
1036
+ state.response = { orderWithFulfillments: { orderId: accountId, fulfillments: [fulfillment] } };
1037
+ const call = { ...input, operation: "orderFulfillments.list", input: { orderId: accountId } };
1038
+ assert.deepEqual(await service.invoke(call), state.response);
1039
+ assert.equal(requests.at(-1).url.pathname, `/ecom/v1/fulfillments/orders/${accountId}`);
1040
+ assert.equal(requests.at(-1).init.method, "GET");
1041
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
1042
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
1043
+ state.response.orderWithFulfillments.fulfillments = [];
1044
+ assert.deepEqual((await service.invoke(call)).orderWithFulfillments.fulfillments, []);
1045
+ state.response.orderWithFulfillments.orderId = siteId;
1046
+ await assert.rejects(service.invoke(call), { code: "connector_response_invalid" });
1047
+ state.response.orderWithFulfillments.orderId = accountId;
1048
+ for (const malformed of [null, { ...fulfillment, lineItems: [null] }, { ...fulfillment, lineItems: [{ id: accountId, quantity: -1 }] }]) {
1049
+ state.response.orderWithFulfillments.fulfillments = [malformed];
1050
+ await assert.rejects(service.invoke(call), { code: "connector_response_invalid" });
1051
+ }
1052
+ let count = requests.length;
1053
+ await assert.rejects(service.invoke({ ...call, input: { orderId: ".." } }), { code: "connector_input_invalid" });
1054
+ state.deny = true;
1055
+ await assert.rejects(service.invoke(call), /Host denied/);
1056
+ assert.equal(requests.length, count);
1057
+ state.deny = false; state.status = 404; state.response = { message: "Order not found" };
1058
+ await assert.rejects(service.invoke(call));
1059
+ assert.equal(requests.length, count + 1);
1060
+ });
1061
+
1062
+
1063
+ test("Wix fulfillment writes preserve explicit quantities and partial tracking updates without replay", async t => {
1064
+ const { service, state, requests } = await fixture(t, { siteId }); await service.connectApiKey(input);
1065
+ const lineItems = [{ id: accountId, quantity: 2 }];
1066
+ state.response = { fulfillmentId: siteId, orderWithFulfillments: { orderId: accountId, fulfillments: [{ id: siteId, lineItems }] } };
1067
+ const args = { orderId: accountId, lineItems, trackingInfo: { trackingNumber: "fixture-123", shippingProvider: "ups" } };
1068
+ const call = { ...input, operation: "orderFulfillments.create", input: args };
1069
+ assert.deepEqual(await service.invoke(call), state.response);
1070
+ assert.equal(requests.at(-1).url.pathname, `/ecom/v1/fulfillments/orders/${accountId}/create-fulfillment`);
1071
+ assert.equal(requests.at(-1).init.method, "POST");
1072
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { fulfillment: { lineItems, trackingInfo: args.trackingInfo } });
1073
+ assert.equal(requests.at(-1).headers.get("wix-site-id"), siteId);
1074
+ assert.equal(requests.at(-1).headers.has("wix-account-id"), false);
1075
+ const update = { ...input, operation: "orderFulfillments.update", input: { orderId: accountId, fulfillmentId: siteId,
1076
+ trackingInfo: { trackingNumber: "updated" }, completed: false, status: "Ready" } };
1077
+ await service.invoke(update);
1078
+ assert.equal(requests.at(-1).url.pathname, `/ecom/v1/fulfillments/${siteId}/orders/${accountId}`);
1079
+ assert.equal(requests.at(-1).init.method, "PATCH");
1080
+ assert.deepEqual(JSON.parse(requests.at(-1).init.body), { fulfillment: { trackingInfo: { trackingNumber: "updated" }, completed: false, status: "Ready" } });
1081
+ let count = requests.length;
1082
+ for (const bad of [{ lineItems: [] }, { lineItems: [{ id: accountId }] }, { lineItems: [...lineItems, ...lineItems] },
1083
+ { trackingInfo: { trackingNumber: "x", shippingProvider: "Custom" } }, { trackingInfo: {} }, { fulfillmentId: siteId }, { status: "SHIPPED" }]) {
1084
+ await assert.rejects(service.invoke({ ...call, input: { ...args, ...bad } }), { code: "connector_input_invalid" });
1085
+ }
1086
+ state.deny = true;
1087
+ await assert.rejects(service.invoke(call), /Host denied/);
1088
+ assert.equal(requests.length, count);
1089
+ state.deny = false; state.response.orderWithFulfillments.orderId = siteId;
1090
+ await assert.rejects(service.invoke(call), { code: "connector_response_invalid" });
1091
+ state.response.orderWithFulfillments.orderId = accountId;
1092
+ state.response.orderWithFulfillments.fulfillments = [{ id: accountId, lineItems }];
1093
+ await assert.rejects(service.invoke(update), { code: "connector_response_invalid" });
1094
+ state.response.orderWithFulfillments.fulfillments = [{ id: siteId, lineItems: [{ id: accountId, quantity: 0 }] }];
1095
+ await assert.rejects(service.invoke(update), { code: "connector_response_invalid" });
1096
+ state.status = 409; state.response = { message: "Tracking number exists" }; count = requests.length;
1097
+ await assert.rejects(service.invoke(call));
1098
+ assert.equal(requests.length, count + 1);
1099
+ });