@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.
- package/README.md +192 -0
- package/docs/ai-upstream-notices.md +51 -0
- package/docs/ai.md +191 -0
- package/docs/airtable.md +130 -0
- package/docs/algolia.md +190 -0
- package/docs/amazon-redshift.md +229 -0
- package/docs/amplitude.md +210 -0
- package/docs/apify.md +162 -0
- package/docs/apollo-io.md +200 -0
- package/docs/asana.md +126 -0
- package/docs/ashby.md +188 -0
- package/docs/atlassian.md +224 -0
- package/docs/attention.md +162 -0
- package/docs/aws-athena.md +150 -0
- package/docs/aws-credentials.md +75 -0
- package/docs/aws-s3.md +193 -0
- package/docs/bigquery.md +146 -0
- package/docs/brevo.md +122 -0
- package/docs/calendly.md +158 -0
- package/docs/canva.md +191 -0
- package/docs/chargebee.md +192 -0
- package/docs/clay.md +194 -0
- package/docs/clickhouse.md +242 -0
- package/docs/confidence-exp.md +74 -0
- package/docs/confidence-flags.md +75 -0
- package/docs/confidence-oauth.md +137 -0
- package/docs/contentful.md +123 -0
- package/docs/databricks.md +180 -0
- package/docs/dbt-semantic-layer.md +175 -0
- package/docs/elevenlabs.md +102 -0
- package/docs/figma.md +132 -0
- package/docs/firebase-cloud-messaging.md +262 -0
- package/docs/firecrawl.md +137 -0
- package/docs/fireflies.md +95 -0
- package/docs/fireworks-ai.md +53 -0
- package/docs/gatewayapi.md +113 -0
- package/docs/gemini-enterprise.md +224 -0
- package/docs/github-api.md +133 -0
- package/docs/gitlab-api.md +138 -0
- package/docs/gmail.md +133 -0
- package/docs/gong.md +205 -0
- package/docs/google-ads.md +156 -0
- package/docs/google-analytics.md +202 -0
- package/docs/google-docs.md +105 -0
- package/docs/google-drive.md +108 -0
- package/docs/google-maps-platform.md +208 -0
- package/docs/google-oauth.md +123 -0
- package/docs/google-search-console.md +124 -0
- package/docs/google-sheets.md +94 -0
- package/docs/google-slides.md +95 -0
- package/docs/granola.md +178 -0
- package/docs/hex.md +161 -0
- package/docs/heygen.md +124 -0
- package/docs/hubspot.md +142 -0
- package/docs/incident-io.md +118 -0
- package/docs/inngest.md +178 -0
- package/docs/klipy.md +149 -0
- package/docs/lexware.md +144 -0
- package/docs/lightspeed.md +193 -0
- package/docs/linear.md +137 -0
- package/docs/linkedin.md +196 -0
- package/docs/logo-dev.md +152 -0
- package/docs/mailgun.md +153 -0
- package/docs/mapbox.md +174 -0
- package/docs/microsoft-excel.md +97 -0
- package/docs/microsoft-fabric.md +249 -0
- package/docs/microsoft-oauth.md +143 -0
- package/docs/microsoft-onedrive.md +76 -0
- package/docs/microsoft-onenote.md +75 -0
- package/docs/microsoft-outlook.md +90 -0
- package/docs/microsoft-powerpoint.md +119 -0
- package/docs/microsoft-sharepoint.md +75 -0
- package/docs/microsoft-teams.md +72 -0
- package/docs/microsoft-word.md +119 -0
- package/docs/miro.md +117 -0
- package/docs/n8n.md +169 -0
- package/docs/notion.md +137 -0
- package/docs/oura.md +161 -0
- package/docs/paddle.md +168 -0
- package/docs/perplexity.md +85 -0
- package/docs/pipedrive.md +120 -0
- package/docs/polar.md +63 -0
- package/docs/posthog.md +227 -0
- package/docs/prestashop.md +151 -0
- package/docs/replicate.md +60 -0
- package/docs/resend.md +153 -0
- package/docs/salesforce.md +234 -0
- package/docs/sanity.md +122 -0
- package/docs/semrush.md +276 -0
- package/docs/sentry.md +112 -0
- package/docs/sevdesk.md +107 -0
- package/docs/shopify.md +206 -0
- package/docs/slack.md +257 -0
- package/docs/snowflake.md +271 -0
- package/docs/storyblok.md +130 -0
- package/docs/stripe.md +77 -0
- package/docs/supabase.md +51 -0
- package/docs/tally.md +105 -0
- package/docs/telegram.md +165 -0
- package/docs/tiktok.md +201 -0
- package/docs/twilio.md +284 -0
- package/docs/twitch.md +428 -0
- package/docs/wave.md +440 -0
- package/docs/wix.md +1249 -0
- package/docs/wiz.md +177 -0
- package/docs/woocommerce.md +400 -0
- package/docs/wordpress-com.md +358 -0
- package/docs/wordpress-self-hosted.md +252 -0
- package/docs/workday.md +292 -0
- package/docs/x-twitter.md +188 -0
- package/docs/xero.md +429 -0
- package/docs/zoho-books.md +214 -0
- package/docs/zoho-crm.md +192 -0
- package/package.json +153 -0
- package/patterns/ai-connections/PATTERN.md +84 -0
- package/patterns/ai-connections/example/ai-model.js +11 -0
- package/patterns/ai-connections/example/perplexity-answer.js +29 -0
- package/patterns/api-key-connection/PATTERN.md +368 -0
- package/patterns/api-key-connection/example/connections.js +18 -0
- package/patterns/api-key-connection/example/integrations.json +19 -0
- package/patterns/assistant-mcp/PATTERN.md +110 -0
- package/patterns/assistant-mcp/example/integrations.json +13 -0
- package/patterns/assistant-mcp-oauth/PATTERN.md +235 -0
- package/patterns/assistant-mcp-oauth/example/integrations.json +102 -0
- package/patterns/aws-storage-queries/PATTERN.md +169 -0
- package/patterns/aws-storage-queries/example/formats/data-formats.js +35 -0
- package/patterns/aws-storage-queries/example/formats/package-lock.json +49 -0
- package/patterns/aws-storage-queries/example/formats/package.json +12 -0
- package/patterns/aws-storage-queries/example/formats/verify-formats.mjs +37 -0
- package/patterns/aws-storage-queries/example/integrations.json +38 -0
- package/patterns/aws-storage-queries/example/s3-transfer.js +34 -0
- package/patterns/event-delivery/PATTERN.md +151 -0
- package/patterns/event-delivery/example/integrations.json +17 -0
- package/patterns/firebase-messaging/PATTERN.md +156 -0
- package/patterns/firebase-messaging/example/server/notifications.js +23 -0
- package/patterns/google-ads-search/PATTERN.md +92 -0
- package/patterns/google-ads-search/example/ads-setup.js +25 -0
- package/patterns/oauth-connection/PATTERN.md +724 -0
- package/patterns/oauth-connection/example/integrations.json +20 -0
- package/patterns/paddle-catalogue/PATTERN.md +78 -0
- package/patterns/paddle-catalogue/example/create-products.js +38 -0
- package/patterns/public-image/PATTERN.md +84 -0
- package/patterns/public-image/example/integrations.json +13 -0
- package/patterns/public-image/example/logo-url.js +16 -0
- package/patterns/redshift-queries/PATTERN.md +150 -0
- package/patterns/redshift-queries/example/integrations.json +33 -0
- package/patterns/source-scanning/PATTERN.md +83 -0
- package/patterns/source-scanning/example/source-scanner.js +13 -0
- package/src/client/firebase-cloud-messaging.js +10 -0
- package/src/client/google-analytics.js +72 -0
- package/src/client/google-maps-platform.js +39 -0
- package/src/client/logo-dev.js +36 -0
- package/src/server/ai.js +46 -0
- package/src/server/airtable.js +67 -0
- package/src/server/algolia.js +63 -0
- package/src/server/amazon-redshift.js +96 -0
- package/src/server/amplitude.js +31 -0
- package/src/server/apify.js +68 -0
- package/src/server/apollo-io.js +85 -0
- package/src/server/asana.js +64 -0
- package/src/server/ashby.js +61 -0
- package/src/server/atlassian.js +32 -0
- package/src/server/attention.js +93 -0
- package/src/server/aws-athena.js +82 -0
- package/src/server/aws-s3.js +49 -0
- package/src/server/awsTransport.js +75 -0
- package/src/server/bigquery.js +97 -0
- package/src/server/brevo.js +97 -0
- package/src/server/calendly.js +77 -0
- package/src/server/canva.js +13 -0
- package/src/server/chargebee.js +88 -0
- package/src/server/clay.js +78 -0
- package/src/server/clickhouse.js +66 -0
- package/src/server/confidence.js +29 -0
- package/src/server/contentful.js +40 -0
- package/src/server/databricks.js +55 -0
- package/src/server/dbt-semantic-layer.js +95 -0
- package/src/server/elevenlabs.js +81 -0
- package/src/server/figma.js +24 -0
- package/src/server/firebase-cloud-messaging.js +111 -0
- package/src/server/firecrawl.js +70 -0
- package/src/server/fireflies.js +31 -0
- package/src/server/fireworks-ai.js +16 -0
- package/src/server/gatewayapi.js +48 -0
- package/src/server/gemini-enterprise.js +47 -0
- package/src/server/github-api.js +90 -0
- package/src/server/gitlab-api.js +67 -0
- package/src/server/gmail.js +60 -0
- package/src/server/gong.js +52 -0
- package/src/server/google-ads-search.js +134 -0
- package/src/server/google-ads.js +80 -0
- package/src/server/google-docs.js +36 -0
- package/src/server/google-drive.js +71 -0
- package/src/server/google-maps-platform.js +90 -0
- package/src/server/google-search-console.js +31 -0
- package/src/server/google-sheets.js +60 -0
- package/src/server/google-slides.js +36 -0
- package/src/server/google.js +61 -0
- package/src/server/granola.js +115 -0
- package/src/server/graphqlOperation.js +15 -0
- package/src/server/hex.js +29 -0
- package/src/server/heygen.js +54 -0
- package/src/server/hubspot.js +85 -0
- package/src/server/incident-io.js +68 -0
- package/src/server/inngest.js +86 -0
- package/src/server/jsonOperation.js +34 -0
- package/src/server/klipy.js +34 -0
- package/src/server/lexware.js +59 -0
- package/src/server/lightspeed.js +82 -0
- package/src/server/linear.js +52 -0
- package/src/server/linkedin.js +74 -0
- package/src/server/mailgun.js +76 -0
- package/src/server/mapbox.js +46 -0
- package/src/server/mcpRegistration.js +41 -0
- package/src/server/mcpTransport.js +60 -0
- package/src/server/microsoft-excel.js +67 -0
- package/src/server/microsoft-fabric.js +81 -0
- package/src/server/microsoft-onedrive.js +58 -0
- package/src/server/microsoft-onenote.js +52 -0
- package/src/server/microsoft-outlook.js +58 -0
- package/src/server/microsoft-powerpoint.js +5 -0
- package/src/server/microsoft-sharepoint.js +46 -0
- package/src/server/microsoft-teams.js +29 -0
- package/src/server/microsoft-word.js +5 -0
- package/src/server/microsoft.js +88 -0
- package/src/server/miro.js +29 -0
- package/src/server/n8n.js +78 -0
- package/src/server/notion.js +76 -0
- package/src/server/oura.js +48 -0
- package/src/server/paddle.js +83 -0
- package/src/server/perplexity.js +15 -0
- package/src/server/pipedrive.js +95 -0
- package/src/server/polar.js +20 -0
- package/src/server/posthog.js +44 -0
- package/src/server/prestashop.js +42 -0
- package/src/server/replicate.js +42 -0
- package/src/server/resend.js +93 -0
- package/src/server/salesforce.js +84 -0
- package/src/server/sanity.js +31 -0
- package/src/server/semrush.js +359 -0
- package/src/server/sentry.js +30 -0
- package/src/server/sevdesk.js +20 -0
- package/src/server/shopify.js +99 -0
- package/src/server/slack.js +143 -0
- package/src/server/snowflake.js +139 -0
- package/src/server/storyblok.js +49 -0
- package/src/server/stripe.js +13 -0
- package/src/server/supabase.js +13 -0
- package/src/server/tally.js +78 -0
- package/src/server/telegram.js +55 -0
- package/src/server/tiktok.js +86 -0
- package/src/server/twilio.js +90 -0
- package/src/server/twitch.js +409 -0
- package/src/server/wave.js +520 -0
- package/src/server/wix.js +1048 -0
- package/src/server/wiz.js +117 -0
- package/src/server/woocommerce.js +412 -0
- package/src/server/wordpress-com.js +236 -0
- package/src/server/wordpress-self-hosted.js +134 -0
- package/src/server/workday.js +136 -0
- package/src/server/x-twitter.js +110 -0
- package/src/server/xero.js +397 -0
- package/src/server/zoho-books.js +90 -0
- package/src/server/zoho-crm.js +89 -0
- package/src/server/zohoOAuth.js +21 -0
- package/src/shared/ai-models.json +8049 -0
- package/src/shared/ai.js +63 -0
- package/src/shared/algolia.js +29 -0
- package/src/shared/amazon-redshift.js +51 -0
- package/src/shared/amplitude.js +28 -0
- package/src/shared/atlassian.js +43 -0
- package/src/shared/aws.js +72 -0
- package/src/shared/canva.js +66 -0
- package/src/shared/chargebee.js +26 -0
- package/src/shared/clickhouse.js +38 -0
- package/src/shared/confidence.js +28 -0
- package/src/shared/databricks.js +33 -0
- package/src/shared/dbt-semantic-layer.js +32 -0
- package/src/shared/definitions.js +140 -0
- package/src/shared/figma.js +21 -0
- package/src/shared/firebase-cloud-messaging.js +45 -0
- package/src/shared/gemini-enterprise.js +33 -0
- package/src/shared/gong.js +33 -0
- package/src/shared/google-ads-search.js +31 -0
- package/src/shared/google-ads.js +28 -0
- package/src/shared/google-maps-platform.js +28 -0
- package/src/shared/google.js +133 -0
- package/src/shared/granola.js +25 -0
- package/src/shared/hex.js +28 -0
- package/src/shared/inngest.js +35 -0
- package/src/shared/lightspeed.js +75 -0
- package/src/shared/linkedin.js +27 -0
- package/src/shared/logo-dev.js +21 -0
- package/src/shared/mapbox.js +38 -0
- package/src/shared/mcp.js +92 -0
- package/src/shared/microsoft-fabric.js +47 -0
- package/src/shared/microsoft.js +231 -0
- package/src/shared/miro.js +27 -0
- package/src/shared/oura.js +23 -0
- package/src/shared/posthog.js +33 -0
- package/src/shared/prestashop.js +23 -0
- package/src/shared/salesforce.js +36 -0
- package/src/shared/semrush.js +24 -0
- package/src/shared/shopify.js +51 -0
- package/src/shared/siteUrl.js +12 -0
- package/src/shared/slack.js +88 -0
- package/src/shared/snowflake.js +55 -0
- package/src/shared/tiktok.js +22 -0
- package/src/shared/tokens.js +747 -0
- package/src/shared/twilio.js +41 -0
- package/src/shared/twitch.js +49 -0
- package/src/shared/wave.js +52 -0
- package/src/shared/wix.js +40 -0
- package/src/shared/wiz.js +41 -0
- package/src/shared/wordpress-com.js +30 -0
- package/src/shared/wordpress.js +69 -0
- package/src/shared/workday.js +50 -0
- package/src/shared/x-twitter.js +19 -0
- package/src/shared/xero.js +46 -0
- package/src/shared/zoho-books.js +39 -0
- package/src/shared/zoho-crm.js +43 -0
- package/src/shared/zoho-regions.js +12 -0
- package/test/ai.test.js +136 -0
- package/test/airtable.test.js +125 -0
- package/test/algolia.test.js +207 -0
- package/test/amazon-redshift.test.js +272 -0
- package/test/amplitude.test.js +239 -0
- package/test/apify.test.js +93 -0
- package/test/apollo-io.test.js +125 -0
- package/test/asana.test.js +99 -0
- package/test/ashby.test.js +97 -0
- package/test/atlassian.test.js +249 -0
- package/test/attention.test.js +79 -0
- package/test/aws.test.js +405 -0
- package/test/brevo.test.js +71 -0
- package/test/calendly.test.js +136 -0
- package/test/canva.test.js +258 -0
- package/test/chargebee.test.js +195 -0
- package/test/clickhouse.test.js +278 -0
- package/test/confidence.test.js +205 -0
- package/test/databricks.test.js +253 -0
- package/test/dbt-semantic-layer.test.js +221 -0
- package/test/designMcp.test.js +336 -0
- package/test/elevenlabs.test.js +52 -0
- package/test/firebase-cloud-messaging.test.js +242 -0
- package/test/firecrawl.test.js +66 -0
- package/test/gemini-enterprise.test.js +218 -0
- package/test/github-api.test.js +167 -0
- package/test/gitlab-api.test.js +160 -0
- package/test/gong.test.js +192 -0
- package/test/google-ads.test.js +350 -0
- package/test/google-maps-platform.test.js +191 -0
- package/test/google.test.js +710 -0
- package/test/granola.test.js +240 -0
- package/test/hex.test.js +253 -0
- package/test/heygen.test.js +121 -0
- package/test/hubspot.test.js +103 -0
- package/test/inngest.test.js +219 -0
- package/test/lightspeed.test.js +250 -0
- package/test/linear.test.js +146 -0
- package/test/linkedin.test.js +243 -0
- package/test/logo-dev.test.js +79 -0
- package/test/mailgun.test.js +107 -0
- package/test/mapbox.test.js +181 -0
- package/test/mcp.test.js +227 -0
- package/test/microsoft-fabric.test.js +258 -0
- package/test/microsoft.test.js +554 -0
- package/test/n8nOAuth.test.js +188 -0
- package/test/notionOAuth.test.js +183 -0
- package/test/oura.test.js +211 -0
- package/test/paddleCatalogue.test.js +51 -0
- package/test/paddlePermissions.test.js +101 -0
- package/test/perplexityComposition.test.js +39 -0
- package/test/pipedriveOAuth.test.js +140 -0
- package/test/posthog.test.js +200 -0
- package/test/prestashop.test.js +207 -0
- package/test/resend.test.js +163 -0
- package/test/salesforce.test.js +276 -0
- package/test/semrush.test.js +479 -0
- package/test/sentry.test.js +35 -0
- package/test/shopify.test.js +247 -0
- package/test/slack.test.js +320 -0
- package/test/snowflake.test.js +391 -0
- package/test/tally.test.js +75 -0
- package/test/telegram.test.js +58 -0
- package/test/tiktok.test.js +243 -0
- package/test/tokens.test.js +1079 -0
- package/test/twilio.test.js +221 -0
- package/test/twitch.test.js +573 -0
- package/test/wave.test.js +678 -0
- package/test/wix.test.js +1099 -0
- package/test/wiz.test.js +185 -0
- package/test/wordpress-com.test.js +469 -0
- package/test/wordpress.test.js +660 -0
- package/test/workday.test.js +314 -0
- package/test/x-twitter.test.js +256 -0
- package/test/xero.test.js +700 -0
- package/test/zoho-books.test.js +253 -0
- package/test/zoho-crm.test.js +223 -0
package/test/wix.test.js
ADDED
|
@@ -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
|
+
});
|