@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
|
@@ -0,0 +1,1079 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { createHmac } from "node:crypto";
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import { mkdtemp, readdir, readFile, rm } from "node:fs/promises";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { createConnectionService } from "../../connectors-core/src/server/index.js";
|
|
8
|
+
import { createFileConnectionStore, createCredentialProtection } from "../../connectors-core/src/server/fileStorage.js";
|
|
9
|
+
import { airtableProvider } from "../src/server/airtable.js";
|
|
10
|
+
import { notionProvider } from "../src/server/notion.js";
|
|
11
|
+
import { brevoProvider } from "../src/server/brevo.js";
|
|
12
|
+
import { elevenlabsProvider } from "../src/server/elevenlabs.js";
|
|
13
|
+
import { githubApiProvider } from "../src/server/github-api.js";
|
|
14
|
+
import { apifyProvider } from "../src/server/apify.js";
|
|
15
|
+
import { calendlyProvider } from "../src/server/calendly.js";
|
|
16
|
+
import { hubspotProvider } from "../src/server/hubspot.js";
|
|
17
|
+
import { linearProvider } from "../src/server/linear.js";
|
|
18
|
+
import { pipedriveProvider } from "../src/server/pipedrive.js";
|
|
19
|
+
import { gitlabApiProvider } from "../src/server/gitlab-api.js";
|
|
20
|
+
import { tallyProvider } from "../src/server/tally.js";
|
|
21
|
+
import { contentfulProvider } from "../src/server/contentful.js";
|
|
22
|
+
import { asanaProvider } from "../src/server/asana.js";
|
|
23
|
+
import { ashbyProvider } from "../src/server/ashby.js";
|
|
24
|
+
import { lexwareProvider } from "../src/server/lexware.js";
|
|
25
|
+
import { sevdeskProvider } from "../src/server/sevdesk.js";
|
|
26
|
+
import { apolloIoProvider } from "../src/server/apollo-io.js";
|
|
27
|
+
import { attentionProvider } from "../src/server/attention.js";
|
|
28
|
+
import { telegramProvider } from "../src/server/telegram.js";
|
|
29
|
+
import { klipyProvider } from "../src/server/klipy.js";
|
|
30
|
+
import { clayProvider } from "../src/server/clay.js";
|
|
31
|
+
|
|
32
|
+
import { stripeProvider } from "../src/server/stripe.js";
|
|
33
|
+
import { replicateProvider } from "../src/server/replicate.js";
|
|
34
|
+
import { incidentIoProvider } from "../src/server/incident-io.js";
|
|
35
|
+
import { firefliesProvider } from "../src/server/fireflies.js";
|
|
36
|
+
|
|
37
|
+
import { heygenProvider } from "../src/server/heygen.js";
|
|
38
|
+
import { perplexityProvider } from "../src/server/perplexity.js";
|
|
39
|
+
import { supabaseProvider } from "../src/server/supabase.js";
|
|
40
|
+
import { paddleProvider } from "../src/server/paddle.js";
|
|
41
|
+
import { mailgunProvider } from "../src/server/mailgun.js";
|
|
42
|
+
import { fireworksAiProvider } from "../src/server/fireworks-ai.js";
|
|
43
|
+
import { gatewayApiProvider, verifyGatewayApiEvent } from "../src/server/gatewayapi.js";
|
|
44
|
+
import { polarProvider } from "../src/server/polar.js";
|
|
45
|
+
import { storyblokProvider } from "../src/server/storyblok.js";
|
|
46
|
+
|
|
47
|
+
const cases = [
|
|
48
|
+
{ provider: clayProvider, check: "/public/v0/me", header: "clay-api-key", prefix: "",
|
|
49
|
+
response: { user: { id: "user-1", name: null }, workspace: { id: "workspace-1", name: "Fixture" } },
|
|
50
|
+
operation: "searches.create", operationPath: "/public/v0/search/query-mode",
|
|
51
|
+
input: { query: 'select from companies where people.exists(is_current = true and job_title is_similar_to ("VP Sales"))' }, body: { query: 'select from companies where people.exists(is_current = true and job_title is_similar_to ("VP Sales"))' },
|
|
52
|
+
operationResponse: { search_id: "search-1", source_type: "companies" } },
|
|
53
|
+
{ provider: telegramProvider, check: "/getMe", key: "123456:fixture_token", rotatedKey: "123456:rotated_token",
|
|
54
|
+
pathPrefix: "/bot123456:fixture_token", rotatedPathPrefix: "/bot123456:rotated_token",
|
|
55
|
+
response: { ok: true, result: { id: 123456, is_bot: true, first_name: "Fixture bot", username: "fixture_bot" } },
|
|
56
|
+
operation: "webhook.read", operationPath: "/getWebhookInfo",
|
|
57
|
+
operationResponse: { ok: true, result: { url: "https://example.test/bot", has_custom_certificate: false, pending_update_count: 12 } } },
|
|
58
|
+
{ provider: klipyProvider, check: "/clips/trending", pathPrefix: "/api/v1/private-fixture-key", rotatedPathPrefix: "/api/v1/rotated-fixture-key",
|
|
59
|
+
response: { result: true, data: { data: [{ slug: "fixture-clip", title: "Fixture", file: {} }], next: true } },
|
|
60
|
+
operation: "clips.search", operationPath: "/clips/search",
|
|
61
|
+
input: { q: "Cats & dogs", page: 2, per_page: 8, locale: "us", customer_id: "subject & 2", content_filter: "medium" },
|
|
62
|
+
query: { q: "Cats & dogs", page: "2", per_page: "8", locale: "us", customer_id: "subject & 2", content_filter: "medium" } },
|
|
63
|
+
{ provider: apolloIoProvider, check: "/api/v1/accounts/search", header: "x-api-key", prefix: "", method: "POST",
|
|
64
|
+
response: { accounts: [{ id: "account-1", name: "Saved company" }] },
|
|
65
|
+
input: { page: 2, per_page: 10, q_organization_name: "Design & partners" }, body: { page: 2, per_page: 10, q_organization_name: "Design & partners" } },
|
|
66
|
+
{ provider: attentionProvider, check: "/v2/conversations/list", header: "authorization", prefix: "Bearer ",
|
|
67
|
+
response: { data: [{ id: "conversation-1", type: "conversations", attributes: { title: "Review" } }], meta: { totalRecords: 150 }, links: { related: "/v2/conversations?page=2" } },
|
|
68
|
+
input: { page: 2, size: 10, "filter[title]": "Design & review", "filter[hide_internal]": false, detailedTranscript: true },
|
|
69
|
+
query: { page: "2", size: "10", "filter[title]": "Design & review", "filter[hide_internal]": "false", detailedTranscript: "true" } },
|
|
70
|
+
{ provider: ashbyProvider, check: "/job.list", header: "authorization", method: "POST",
|
|
71
|
+
initialHeader: `Basic ${Buffer.from("private-fixture-key:").toString("base64")}`, rotatedHeader: `Basic ${Buffer.from("rotated-fixture-key:").toString("base64")}`,
|
|
72
|
+
response: { success: true, results: [{ id: "job-1", title: "Developer" }], moreDataAvailable: true, nextCursor: "next-cursor", syncToken: "current-sync" },
|
|
73
|
+
input: { limit: 2, cursor: "cursor & 2", syncToken: "sync & 1" }, body: { limit: 2, cursor: "cursor & 2", syncToken: "sync & 1" }, extraHeaders: { Accept: "application/json; version=1" } },
|
|
74
|
+
{ provider: lexwareProvider, check: "/v1/contacts", header: "authorization", prefix: "Bearer ",
|
|
75
|
+
response: { content: [{ id: "contact-1", roles: { customer: { number: 1 } } }], first: true, last: false, totalPages: 2, totalElements: 26, numberOfElements: 1, size: 25, number: 0 },
|
|
76
|
+
input: { size: 2, page: 1, customer: false, vendor: true }, query: { size: "2", page: "1", customer: "false", vendor: "true" } },
|
|
77
|
+
{ provider: sevdeskProvider, check: "/api/v1/Contact", header: "authorization", prefix: "",
|
|
78
|
+
response: { objects: [{ id: "1", objectName: "Contact", name: "Fixture" }], total: "101" },
|
|
79
|
+
input: { limit: 2, offset: 4, countAll: true, depth: 0 }, query: { limit: "2", offset: "4", countAll: "true", depth: "0" } },
|
|
80
|
+
{ provider: storyblokProvider, check: "/v2/cdn/spaces/me", queryCredential: "token", response: { space: { id: 123, name: "Fixture", language_codes: ["de"], version: 1544117388 } }, operation: "stories.list", operationPath: "/v2/cdn/stories", operationResponse: { stories: [{ id: 456, content: { title: "Fixture" } }], cv: 1544117388, rels: [], links: [] }, input: { page: 2, per_page: 10, starts_with: "blog/", search_term: "Notes & plans", language: "de", cv: 1544117388 }, query: { version: "published", page: "2", per_page: "10", starts_with: "blog/", search_term: "Notes & plans", language: "de", cv: "1544117388" } },
|
|
81
|
+
{ provider: fireworksAiProvider, check: "/v1/accounts", header: "authorization", prefix: "Bearer ", extraHeaders: { "content-type": "application/json" }, response: { accounts: [{ name: "accounts/fixture", displayName: "Fixture" }], nextPageToken: "next & 2", totalSize: 3 }, input: { pageSize: 2, pageToken: "next & 2", filter: "displayName=Fixture" }, query: { pageSize: "2", pageToken: "next & 2", filter: "displayName=Fixture" } },
|
|
82
|
+
{ provider: gatewayApiProvider, check: "/rest/me", header: "authorization", prefix: "Token ", response: { id: 123, credit: "1234.56", currency: "DKK" } },
|
|
83
|
+
{ provider: polarProvider, check: "/v1/products/", header: "authorization", prefix: "Bearer ", response: { items: [{ id: "product-1", name: "Fixture" }], pagination: { total_count: 21, max_page: 3 } }, input: { page: 2, limit: 10, query: "Fixture & plans", is_archived: false, is_recurring: true }, query: { page: "2", limit: "10", query: "Fixture & plans", is_archived: "false", is_recurring: "true" } },
|
|
84
|
+
{ provider: paddleProvider, check: "/products", header: "authorization", prefix: "Bearer ", extraHeaders: { "paddle-version": "1" }, response: { data: [{ id: "pro_fixture", name: "Fixture" }], meta: { request_id: "request-1", pagination: { per_page: 50, has_more: true, next: "https://sandbox-api.paddle.com/products?after=pro_fixture" } } }, input: { per_page: 2, after: "pro_fixture", status: "archived" }, query: { per_page: "2", after: "pro_fixture", status: "archived" } },
|
|
85
|
+
{ provider: mailgunProvider, check: "/v4/domains", header: "authorization", prefix: "Basic ", initialHeader: "Basic YXBpOnByaXZhdGUtZml4dHVyZS1rZXk=", rotatedHeader: "Basic YXBpOnJvdGF0ZWQtZml4dHVyZS1rZXk=", response: { total_count: 1, items: [{ name: "example.com", state: "active" }] }, input: { limit: 2, skip: 4, state: "active", sort: "name:asc", search: "example.com", include_subaccounts: true }, query: { limit: "2", skip: "4", state: "active", sort: "name:asc", search: "example.com", include_subaccounts: "true" } },
|
|
86
|
+
{ provider: heygenProvider, check: "/v3/users/me", header: "x-api-key", prefix: "", response: { data: { username: "fixture", email: "fixture@example.com", billing_type: "wallet" } }, operation: "voices.list", operationPath: "/v3/voices", operationResponse: { data: [{ voice_id: "voice-1", name: "Fixture" }], has_more: true, next_token: "page & 2" }, input: { limit: 2, token: "page & 2", type: "private", engine: "starfish", language: "English", gender: "female" }, query: { limit: "2", token: "page & 2", type: "private", engine: "starfish", language: "English", gender: "female" } },
|
|
87
|
+
{ provider: perplexityProvider, check: "/v1/async/sonar", header: "authorization", prefix: "Bearer ", response: { requests: [{ id: "request-1", created_at: 123, model: "sonar", status: "COMPLETED" }], next_token: null }, operation: "models.list", operationPath: "/v1/models", operationResponse: { object: "list", data: [{ id: "example/model", object: "model", created: 0, owned_by: "example" }] } },
|
|
88
|
+
{ provider: supabaseProvider, check: "/v1/projects", header: "authorization", prefix: "Bearer ", response: [{ id: "project-1", ref: "project-reference", name: "Fixture", organization_id: "organization-1", region: "ap-southeast-2", status: "ACTIVE_HEALTHY" }] },
|
|
89
|
+
{ provider: stripeProvider, check: "/v1/balance", header: "authorization", prefix: "Bearer ", response: { object: "balance", available: [{ amount: 1500, currency: "aud", source_types: { card: 1500 } }], pending: [], livemode: false } },
|
|
90
|
+
{ provider: replicateProvider, check: "/v1/account", header: "authorization", prefix: "Bearer ", response: { type: "organization", username: "fixture", name: "Fixture" }, operation: "hardware.list", operationPath: "/v1/hardware", operationResponse: [{ name: "CPU", sku: "cpu" }] },
|
|
91
|
+
{ provider: incidentIoProvider, check: "/v2/incidents", header: "authorization", prefix: "Bearer ", response: { incidents: [{ id: "incident-1", name: "Fixture" }], pagination_meta: { page_size: 25, after: "next", total_record_count: 42 } }, input: { page_size: 2, after: "next & 2", sort_by: "created_at_oldest_first" }, query: { page_size: "2", after: "next & 2", sort_by: "created_at_oldest_first" } },
|
|
92
|
+
{ provider: firefliesProvider, check: "/graphql", header: "authorization", prefix: "Bearer ", method: "POST", response: { data: { user: { user_id: "user-1", name: "Fixture", email: "test@example.com" } } }, operation: "transcripts.list", operationResponse: { data: { transcripts: [{ id: "meeting-1", title: "Fixture" }] } }, input: { limit: 2, skip: 4, mine: false }, body: { query: "query ConnectorTranscripts($limit: Int!, $skip: Int!, $mine: Boolean!) { transcripts(limit: $limit, skip: $skip, mine: $mine) { id title } }", variables: { limit: 2, skip: 4, mine: false } } },
|
|
93
|
+
{ provider: airtableProvider, check: "/v0/meta/bases", header: "authorization", prefix: "Bearer ", response: { bases: [{ id: "app1", name: "Example" }], offset: "next/page" }, input: { offset: "next/page & 2" }, query: { offset: "next/page & 2" } },
|
|
94
|
+
{ provider: notionProvider, check: "/v1/search", header: "authorization", prefix: "Bearer ", response: { object: "list", results: [], next_cursor: "next", has_more: true }, method: "POST", input: { query: "Notes & plans", start_cursor: "cursor", page_size: 2 }, body: { query: "Notes & plans", start_cursor: "cursor", page_size: 2 }, extraHeaders: { "Notion-Version": "2026-03-11" } },
|
|
95
|
+
{ provider: brevoProvider, check: "/v3/contacts", header: "api-key", prefix: "", response: { contacts: [], count: 12 }, input: { limit: 2, offset: 4, sort: "asc" }, query: { limit: "2", offset: "4", sort: "asc" } },
|
|
96
|
+
{ provider: elevenlabsProvider, check: "/v1/user", header: "xi-api-key", prefix: "", response: { user_id: "user-1" }, operation: "voices.list", operationPath: "/v2/voices", operationResponse: { voices: [], next_page_token: "page", has_more: true }, input: { page_size: 2, next_page_token: "page & 2", search: "Voice" }, query: { page_size: "2", next_page_token: "page & 2", search: "Voice" } },
|
|
97
|
+
{ provider: githubApiProvider, check: "/user", header: "authorization", prefix: "Bearer ", response: { id: 42, login: "example" }, operation: "repositories.list", operationPath: "/user/repos", operationResponse: [{ id: 10, full_name: "example/repository" }], input: { per_page: 2, page: 3 }, query: { per_page: "2", page: "3" }, extraHeaders: { "X-GitHub-Api-Version": "2026-03-10", "User-Agent": "jskit-connectors", Accept: "application/vnd.github+json" } },
|
|
98
|
+
{ provider: apifyProvider, check: "/v2/actors", header: "authorization", prefix: "Bearer ", response: { data: { items: [], total: 12, offset: 0, count: 0, limit: 100 } }, input: { limit: 2, offset: 4, desc: true }, query: { limit: "2", offset: "4", desc: "true" } },
|
|
99
|
+
{ provider: calendlyProvider, check: "/users/me", header: "authorization", prefix: "Bearer ", response: { resource: { uri: "https://api.calendly.com/users/fixture", name: "Fixture" } }, operation: "eventTypes.list", operationPath: "/event_types", operationResponse: { collection: [], pagination: { count: 0, next_page_token: "page & 2" } }, input: { user: "https://api.calendly.com/users/fixture", count: 2, page_token: "page & 2", active: false }, query: { user: "https://api.calendly.com/users/fixture", count: "2", page_token: "page & 2", active: "false" } },
|
|
100
|
+
{ provider: hubspotProvider, scopes: ["crm.objects.contacts.read"], check: "/crm/objects/2026-09/contacts", header: "authorization", prefix: "Bearer ", response: { results: [], paging: { next: { after: "42" } } }, input: { limit: 2, after: "42", archived: true }, query: { limit: "2", after: "42", archived: "true" } },
|
|
101
|
+
{ provider: linearProvider, scopes: ["read"], check: "/graphql", header: "authorization", prefix: "", response: { data: { viewer: { id: "user-1", name: "Fixture" } } }, method: "POST", operation: "issues.list", operationResponse: { data: { issues: { nodes: [], pageInfo: { hasNextPage: true, endCursor: "next" } } } }, input: { first: 2, after: "cursor & 2" }, body: { query: "query ConnectorIssues($first: Int!, $after: String) { issues(first: $first, after: $after) { nodes { id identifier title } pageInfo { hasNextPage endCursor } } }", variables: { first: 2, after: "cursor & 2" } } },
|
|
102
|
+
{ provider: pipedriveProvider, check: "/v1/users/me", header: "x-api-token", prefix: "", response: { success: true, data: { id: 42, name: "Fixture", company_domain: "fixture" } } },
|
|
103
|
+
{ provider: gitlabApiProvider, check: "/api/v4/user", header: "PRIVATE-TOKEN", prefix: "", response: { id: 42, username: "fixture" }, operation: "projects.list", operationPath: "/api/v4/projects", operationResponse: [{ id: 1, path_with_namespace: "fixture/project" }], input: { per_page: 2, page: 3 }, query: { per_page: "2", page: "3", membership: "true", simple: "true" } },
|
|
104
|
+
{ provider: tallyProvider, check: "/forms", header: "authorization", prefix: "Bearer ", response: { items: [], page: 1, limit: 50, total: 0, hasMore: false }, input: { limit: 2, page: 3 }, query: { limit: "2", page: "3" }, extraHeaders: { "tally-version": "2025-02-01" } },
|
|
105
|
+
{ provider: contentfulProvider, settings: { spaceId: "space-one" }, check: "/spaces/space-one/environments/master/entries", header: "authorization", prefix: "Bearer ", response: { sys: { type: "Array" }, items: [], skip: 0, limit: 1, total: 0 }, input: { limit: 2, skip: 4, content_type: "article" }, query: { limit: "2", skip: "4", content_type: "article" } },
|
|
106
|
+
{ provider: asanaProvider, check: "/api/1.0/workspaces", header: "authorization", prefix: "Bearer ", response: { data: [{ gid: "123", name: "Fixture" }], next_page: { offset: "next & 2" } }, input: { limit: 2, offset: "next & 2" }, query: { limit: "2", offset: "next & 2" } }
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
async function fixture(t, entry) {
|
|
110
|
+
const directory = await mkdtemp(path.join(tmpdir(), "connector-token-"));
|
|
111
|
+
t.after(() => rm(directory, { recursive: true, force: true }));
|
|
112
|
+
const state = { key: entry.key || "private-fixture-key", status: 200, response: entry.response };
|
|
113
|
+
const requests = [];
|
|
114
|
+
const protection = createCredentialProtection({ keys: { current: new Uint8Array(32).fill(5) }, activeKeyId: "current" });
|
|
115
|
+
const options = {
|
|
116
|
+
configuration: { schemaVersion: 1, registrations: {}, integrations: {
|
|
117
|
+
source: { provider: entry.provider.id, ...(entry.settings ? { settings: entry.settings } : {}), accountMode: "shared", scopes: entry.scopes || (entry.provider.id === "calendly" ? ["users:read", "event_types:read"] : []), authentication: { method: "api-key", secretRef: "env:SERVICE_TOKEN" } }
|
|
118
|
+
} },
|
|
119
|
+
providers: [entry.provider], authorize: async (context) => context,
|
|
120
|
+
resolveReference: async () => state.key,
|
|
121
|
+
store: createFileConnectionStore({ directory, protection }),
|
|
122
|
+
fetchImpl: async (url, init) => {
|
|
123
|
+
requests.push({ url: new URL(String(url)), init, headers: new Headers(init.headers) });
|
|
124
|
+
return Response.json(state.response, { status: state.status });
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
const input = { context: { applicationId: "app-one", subjectId: "account-one" }, integrationId: "source" };
|
|
128
|
+
return { service: createConnectionService(options), options, directory, protection, input, state, requests };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const entry of cases) {
|
|
132
|
+
test(`${entry.provider.id}: verifies its credential and performs the documented operation after a file-store restart`, async (t) => {
|
|
133
|
+
const { service, options, directory, protection, input, state, requests } = await fixture(t, entry);
|
|
134
|
+
assert.equal((await service.connectApiKey(input)).status, "connected");
|
|
135
|
+
const first = requests[0];
|
|
136
|
+
assert.equal(first.url.origin, (typeof entry.provider.apiOrigins === "function" ? entry.provider.apiOrigins(entry.settings || {}) : entry.provider.apiOrigins)[0]);
|
|
137
|
+
assert.equal(first.url.pathname, (entry.pathPrefix || "") + entry.check);
|
|
138
|
+
assert.equal(first.init.method, entry.method || "GET");
|
|
139
|
+
if (entry.pathPrefix) {
|
|
140
|
+
assert.equal(first.headers.has("authorization"), false);
|
|
141
|
+
} else if (entry.queryCredential) {
|
|
142
|
+
assert.deepEqual(first.url.searchParams.getAll(entry.queryCredential), [state.key]);
|
|
143
|
+
assert.equal(first.headers.has("authorization"), false);
|
|
144
|
+
} else assert.equal(first.headers.get(entry.header), entry.initialHeader || entry.prefix + state.key);
|
|
145
|
+
assert.equal(first.init.redirect, "error");
|
|
146
|
+
for (const [header, value] of Object.entries(entry.extraHeaders || {})) assert.equal(first.headers.get(header), value);
|
|
147
|
+
for (const name of await readdir(directory)) assert.equal((await readFile(path.join(directory, name), "utf8")).includes(state.key), false);
|
|
148
|
+
const restarted = createConnectionService({ ...options, store: createFileConnectionStore({ directory, protection }) });
|
|
149
|
+
assert.equal((await restarted.status(input)).status, "connected");
|
|
150
|
+
state.response = entry.operationResponse || entry.response;
|
|
151
|
+
state.key = entry.rotatedKey || "rotated-fixture-key";
|
|
152
|
+
const result = await restarted.invoke({ ...input, operation: entry.operation || entry.provider.checkOperation, input: entry.input });
|
|
153
|
+
assert.deepEqual(result, state.response);
|
|
154
|
+
const request = requests.at(-1);
|
|
155
|
+
assert.equal(request.url.pathname, (entry.rotatedPathPrefix || "") + (entry.operationPath || entry.check));
|
|
156
|
+
if (entry.pathPrefix) assert.equal(request.headers.has("authorization"), false);
|
|
157
|
+
else if (entry.queryCredential) assert.equal(request.url.searchParams.get(entry.queryCredential), state.key);
|
|
158
|
+
else assert.equal(request.headers.get(entry.header), entry.rotatedHeader || entry.prefix + state.key);
|
|
159
|
+
for (const [key, value] of Object.entries(entry.query || {})) assert.equal(request.url.searchParams.get(key), value);
|
|
160
|
+
if (entry.body) assert.deepEqual(JSON.parse(request.init.body), entry.body);
|
|
161
|
+
await assert.rejects(restarted.invoke({ ...input, context: { ...input.context, applicationId: "app-two" }, operation: typeof entry.provider.checkOperation === "function" ? entry.provider.checkOperation("api-key") : entry.provider.checkOperation }), { code: "connector_reconnect_required" });
|
|
162
|
+
await restarted.disconnect(input);
|
|
163
|
+
assert.equal((await service.status(input)).status, "disconnected");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test(`${entry.provider.id}: rejects invalid inputs, malformed successes, invalid keys and rate limits without leaking credentials`, async (t) => {
|
|
167
|
+
const { service, input, state, requests } = await fixture(t, entry);
|
|
168
|
+
for (const [status, code] of [[200, "connector_response_invalid"], [401, "connector_reconnect_required"], [403, "connector_permission_denied"], [429, "connector_rate_limited"]]) {
|
|
169
|
+
state.status = status;
|
|
170
|
+
state.response = { error: { message: state.key } };
|
|
171
|
+
await assert.rejects(service.connectApiKey(input), (error) => {
|
|
172
|
+
assert.equal(error.code, code);
|
|
173
|
+
assert.equal(JSON.stringify(error).includes(state.key), false);
|
|
174
|
+
return true;
|
|
175
|
+
});
|
|
176
|
+
assert.equal((await service.status(input)).status, "disconnected");
|
|
177
|
+
}
|
|
178
|
+
state.status = 200;
|
|
179
|
+
state.response = entry.response;
|
|
180
|
+
await service.connectApiKey(input);
|
|
181
|
+
const count = requests.length;
|
|
182
|
+
await assert.rejects(service.invoke({ ...input, operation: entry.operation || entry.provider.checkOperation, input: { ...entry.input, unknown: "reject" } }), { code: "connector_input_invalid" });
|
|
183
|
+
assert.equal(requests.length, count);
|
|
184
|
+
state.status = 401;
|
|
185
|
+
await assert.rejects(service.invoke({ ...input, operation: typeof entry.provider.checkOperation === "function" ? entry.provider.checkOperation("api-key") : entry.provider.checkOperation }), { code: "connector_reconnect_required" });
|
|
186
|
+
assert.equal((await service.status(input)).status, "reconnect-required");
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
test("Clay advances only explicitly requested query pages and validates each result envelope", async (t) => {
|
|
191
|
+
const { service, input, state, requests } = await fixture(t, cases.find(({ provider }) => provider === clayProvider));
|
|
192
|
+
await service.connectApiKey(input);
|
|
193
|
+
assert.equal(requests.length, 1);
|
|
194
|
+
assert.equal(requests[0].url.pathname, "/public/v0/me");
|
|
195
|
+
state.response = { data: [{ clay_company_id: 123, name: "Fixture" }], has_more: true, source_type: "companies",
|
|
196
|
+
period_quota: { limit: 1000, used: 20, remaining: 980, resets_at: "2026-10-01T00:00:00Z" } };
|
|
197
|
+
assert.deepEqual(await service.invoke({ ...input, operation: "searches.next", input: { searchId: "search-1" } }), state.response);
|
|
198
|
+
assert.equal(requests.length, 2);
|
|
199
|
+
assert.equal(requests.at(-1).url.href, "https://api.clay.com/public/v0/search/query-mode/search-1/run");
|
|
200
|
+
assert.equal(requests.at(-1).init.method, "POST");
|
|
201
|
+
assert.deepEqual(JSON.parse(requests.at(-1).init.body), { limit: 20 });
|
|
202
|
+
state.response = { data: [], has_more: false, source_type: "people", exhaustion_reason: "no_more_results" };
|
|
203
|
+
assert.deepEqual(await service.invoke({ ...input, operation: "searches.next", input: { searchId: "search_2", limit: 500 } }), state.response);
|
|
204
|
+
assert.equal(requests.length, 3);
|
|
205
|
+
assert.deepEqual(JSON.parse(requests.at(-1).init.body), { limit: 500 });
|
|
206
|
+
const count = requests.length;
|
|
207
|
+
for (const invalid of [{}, { query: "" }, { query: " " }, { query: "x".repeat(16001) }, { query: "x", apiKey: "caller-key" }]) {
|
|
208
|
+
await assert.rejects(service.invoke({ ...input, operation: "searches.create", input: invalid }), { code: "connector_input_invalid" });
|
|
209
|
+
}
|
|
210
|
+
for (const invalid of [{}, { searchId: ".." }, { searchId: "a/b" }, { searchId: "https://other.invalid" },
|
|
211
|
+
{ searchId: "search-1", limit: 0 }, { searchId: "search-1", limit: 501 }, { searchId: "search-1", limit: 1.5 }, { searchId: "search-1", page: 2 }]) {
|
|
212
|
+
await assert.rejects(service.invoke({ ...input, operation: "searches.next", input: invalid }), { code: "connector_input_invalid" });
|
|
213
|
+
}
|
|
214
|
+
assert.equal(requests.length, count);
|
|
215
|
+
for (const malformed of [{ data: [], source_type: "people" }, { data: {}, has_more: false, source_type: "companies" },
|
|
216
|
+
{ data: [], has_more: false, source_type: "jobs" }]) {
|
|
217
|
+
state.response = malformed;
|
|
218
|
+
await assert.rejects(service.invoke({ ...input, operation: "searches.next", input: { searchId: "search-1" } }), { code: "connector_response_invalid" });
|
|
219
|
+
}
|
|
220
|
+
assert.equal(clayProvider.operations["identity.read"].validateResult({ user: { id: "user-1" }, workspace: {} }), false);
|
|
221
|
+
assert.equal(clayProvider.operations["searches.create"].validateResult({ search_id: "search-1", source_type: "jobs" }), false);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("Clay search policy, billing failure and interrupted iterators never replay a request", async (t) => {
|
|
225
|
+
const { service, options, input, state, requests } = await fixture(t, cases.find(({ provider }) => provider === clayProvider));
|
|
226
|
+
await service.connectApiKey(input);
|
|
227
|
+
const restricted = createConnectionService({ ...options, authorize: async (owner, request) =>
|
|
228
|
+
request.operation === "searches.create" || request.operation === "searches.next" && request.input.searchId !== "search-1" ? null : owner });
|
|
229
|
+
await assert.rejects(restricted.invoke({ ...input, operation: "searches.create", input: { query: "anything" } }), { code: "connector_access_denied" });
|
|
230
|
+
await assert.rejects(restricted.invoke({ ...input, operation: "searches.next", input: { searchId: "another-search" } }), { code: "connector_access_denied" });
|
|
231
|
+
assert.equal(requests.length, 1);
|
|
232
|
+
state.status = 402;
|
|
233
|
+
state.response = { message: `Credit limit: ${state.key}` };
|
|
234
|
+
await assert.rejects(service.invoke({ ...input, operation: "searches.next", input: { searchId: "search-1" } }), (error) => {
|
|
235
|
+
assert.equal(error.code, "connector_provider_failed");
|
|
236
|
+
assert(!JSON.stringify(error).includes(state.key));
|
|
237
|
+
return true;
|
|
238
|
+
});
|
|
239
|
+
assert.equal(requests.length, 2);
|
|
240
|
+
let started;
|
|
241
|
+
const pendingRequest = new Promise((resolve) => { started = resolve; });
|
|
242
|
+
let attempts = 0;
|
|
243
|
+
const interrupted = createConnectionService({ ...options, fetchImpl: async (_url, init) => {
|
|
244
|
+
attempts += 1;
|
|
245
|
+
init.signal.throwIfAborted();
|
|
246
|
+
started();
|
|
247
|
+
return new Promise((resolve, reject) => init.signal.addEventListener("abort", () => reject(init.signal.reason), { once: true }));
|
|
248
|
+
} });
|
|
249
|
+
const controller = new AbortController();
|
|
250
|
+
const pending = assert.rejects(interrupted.invoke({ ...input, operation: "searches.next", input: { searchId: "search-1" }, signal: controller.signal }), { code: "connector_cancelled" });
|
|
251
|
+
await pendingRequest;
|
|
252
|
+
controller.abort();
|
|
253
|
+
await pending;
|
|
254
|
+
assert.equal(attempts, 1);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("Clay runs approved routines and queries Enterprise tables without replay or hidden paging", async t => {
|
|
258
|
+
const { service, options, input, state, requests } = await fixture(t, cases.find(({ provider }) => provider === clayProvider));
|
|
259
|
+
await service.connectApiKey(input);
|
|
260
|
+
const invoke = (operation, values = {}) => service.invoke({ ...input, operation, input: values });
|
|
261
|
+
state.response = { reference: "Provider-owned query grammar" };
|
|
262
|
+
assert.deepEqual(await invoke("searches.reference"), state.response);
|
|
263
|
+
assert.equal(requests.at(-1).url.pathname, "/public/v0/search/query-mode/reference");
|
|
264
|
+
state.response = { balance: 10, action_execution_balance: 20 };
|
|
265
|
+
assert.deepEqual(await invoke("credits.balance"), state.response);
|
|
266
|
+
const items = [{ id: "row-1", inputs: { domain: "example.test" } }];
|
|
267
|
+
state.response = { routine_run_id: "run-1", status: "in_progress" }; state.status = 202;
|
|
268
|
+
assert.deepEqual(await invoke("routines.run", { routineId: "function:t_example", items }), state.response);
|
|
269
|
+
assert.equal(requests.at(-1).url.pathname, "/public/v0/routines/function%3At_example/run");
|
|
270
|
+
assert.deepEqual(JSON.parse(requests.at(-1).init.body), { items });
|
|
271
|
+
state.response = { routine_run_id: "run-1", total: 1, finished: 0, status: "in_progress" };
|
|
272
|
+
assert.deepEqual(await invoke("routines.results", { routineId: "run-1" }), state.response);
|
|
273
|
+
state.status = 200; state.response = { routine_run_id: "run-1", total: 1, finished: 1, status: "complete", data: [{ id: "row-1", error: "enrichment unavailable" }], cursor: "next+opaque" };
|
|
274
|
+
assert.deepEqual(await invoke("routines.results", { routineId: "run-1", cursor: "prior+&cursor", limit: 100 }), state.response);
|
|
275
|
+
assert.equal(requests.at(-1).url.searchParams.get("cursor"), "prior+&cursor");
|
|
276
|
+
const query = { tables: [{ id: "t_example" }], select: [{ field: "Domain", as: "domain" }], filter: { field: "Domain", op: "is_not_empty" }, field_mode: "names" };
|
|
277
|
+
state.response = { data: [{ domain: { value: "example.test" } }], fields: { domain: { name: "Domain" } }, truncated: true };
|
|
278
|
+
assert.deepEqual(await invoke("tables.query", { query, limit: 100 }), state.response);
|
|
279
|
+
assert.equal(requests.at(-1).url.pathname, "/public/v0/tables/query");
|
|
280
|
+
assert.deepEqual(JSON.parse(requests.at(-1).init.body), { query, limit: 100 });
|
|
281
|
+
const count = requests.length;
|
|
282
|
+
for (const [operation, values] of [["routines.run", { routineId: "../other", items }], ["routines.run", { routineId: "function:t_x", items: [] }],
|
|
283
|
+
["routines.run", { routineId: "x", items: [{ id: "x" }] }], ["tables.query", { query: { tables: [] } }],
|
|
284
|
+
["tables.query", { query, limit: 101 }], ["tables.query", { query: { ...query, url: "https://other.test" } }]])
|
|
285
|
+
await assert.rejects(invoke(operation, values), { code: "connector_input_invalid" });
|
|
286
|
+
const restricted = createConnectionService({ ...options, authorize: async (owner, request) => request.operation === "routines.run" ? null : owner });
|
|
287
|
+
await assert.rejects(restricted.invoke({ ...input, operation: "routines.run", input: { routineId: "x", items } }), { code: "connector_access_denied" });
|
|
288
|
+
assert.equal(requests.length, count);
|
|
289
|
+
state.response = { status: "complete" };
|
|
290
|
+
await assert.rejects(invoke("routines.results", { routineId: "run-1" }), { code: "connector_response_invalid" });
|
|
291
|
+
for (const status of [402, 403, 422, 429, 500]) { state.status = status; const before = requests.length; await assert.rejects(invoke("routines.run", { routineId: "function:t_x", items })); assert.equal(requests.length, before + 1); }
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("regional connections require re-verification before changing their provider destination", async (t) => {
|
|
295
|
+
for (const [provider, setting, alternative] of [
|
|
296
|
+
[paddleProvider, "environment", "live"], [mailgunProvider, "region", "eu"],
|
|
297
|
+
[gatewayApiProvider, "region", "eu"], [polarProvider, "environment", "production"],
|
|
298
|
+
[storyblokProvider, "region", "us"]
|
|
299
|
+
]) {
|
|
300
|
+
const entry = cases.find((item) => item.provider === provider);
|
|
301
|
+
const { service, options, input, requests } = await fixture(t, entry);
|
|
302
|
+
await service.connectApiKey(input);
|
|
303
|
+
const config = structuredClone(options.configuration);
|
|
304
|
+
config.integrations.source.settings = { [setting]: alternative };
|
|
305
|
+
const changed = createConnectionService({ ...options, configuration: config });
|
|
306
|
+
assert.equal((await changed.status(input)).status, "reconnect-required");
|
|
307
|
+
await assert.rejects(changed.invoke({ ...input, operation: provider.checkOperation }), { code: "connector_reconnect_required" });
|
|
308
|
+
assert.equal(requests.length, 1);
|
|
309
|
+
await changed.connectApiKey(input);
|
|
310
|
+
assert.equal(requests.at(-1).url.origin, provider.apiOrigins[1]);
|
|
311
|
+
await changed.invoke({ ...input, operation: provider.checkOperation });
|
|
312
|
+
assert.equal(requests.at(-1).url.origin, provider.apiOrigins[1]);
|
|
313
|
+
assert.equal((await service.status(input)).status, "reconnect-required");
|
|
314
|
+
config.integrations.source.settings[setting] = "https://attacker.invalid";
|
|
315
|
+
const count = requests.length;
|
|
316
|
+
assert.throws(() => createConnectionService({ ...options, configuration: config }), { code: "integration_configuration_invalid" });
|
|
317
|
+
assert.equal(requests.length, count);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("Storyblok supports all space regions and requires explicit draft reads without accepting caller credentials", async (t) => {
|
|
322
|
+
const entry = cases.find(({ provider }) => provider === storyblokProvider);
|
|
323
|
+
const { service, options, input, state, requests } = await fixture(t, entry);
|
|
324
|
+
for (const [region, host] of [
|
|
325
|
+
["eu", "api.storyblok.com"], ["us", "api-us.storyblok.com"], ["ca", "api-ca.storyblok.com"],
|
|
326
|
+
["ap", "api-ap.storyblok.com"], ["cn", "app.storyblokchina.cn"]
|
|
327
|
+
]) {
|
|
328
|
+
const config = structuredClone(options.configuration);
|
|
329
|
+
config.integrations.source.settings = { region };
|
|
330
|
+
const regional = createConnectionService({ ...options, configuration: config });
|
|
331
|
+
state.response = entry.response;
|
|
332
|
+
await regional.connectApiKey(input);
|
|
333
|
+
assert.equal(requests.at(-1).url.host, host);
|
|
334
|
+
state.response = { stories: [] };
|
|
335
|
+
assert.deepEqual(await regional.invoke({ ...input, operation: "stories.list" }), state.response);
|
|
336
|
+
assert.deepEqual(Object.fromEntries(requests.at(-1).url.searchParams), {
|
|
337
|
+
version: "published", page: "1", per_page: "25", token: state.key
|
|
338
|
+
});
|
|
339
|
+
await regional.invoke({ ...input, operation: "stories.list", input: { version: "draft", per_page: 100 } });
|
|
340
|
+
assert.equal(requests.at(-1).url.searchParams.get("version"), "draft");
|
|
341
|
+
assert.equal(requests.at(-1).url.searchParams.get("per_page"), "100");
|
|
342
|
+
const count = requests.length;
|
|
343
|
+
for (const invalid of [{ token: "caller-key" }, { version: "all" }, { per_page: 0 }, { per_page: 101 }, { page: 0 }, { cv: -1 }]) {
|
|
344
|
+
await assert.rejects(regional.invoke({ ...input, operation: "stories.list", input: invalid }), { code: "connector_input_invalid" });
|
|
345
|
+
}
|
|
346
|
+
assert.equal(requests.length, count);
|
|
347
|
+
}
|
|
348
|
+
state.response = entry.response;
|
|
349
|
+
await service.connectApiKey(input);
|
|
350
|
+
state.status = 403;
|
|
351
|
+
state.response = { error: "Preview access required" };
|
|
352
|
+
await assert.rejects(service.invoke({ ...input, operation: "stories.list", input: { version: "draft" } }), { code: "connector_permission_denied" });
|
|
353
|
+
assert.equal(requests.at(-1).url.searchParams.get("version"), "draft");
|
|
354
|
+
assert.equal(storyblokProvider.operations["space.read"].validateResult({ space: { name: "incomplete" } }), false);
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("Storyblok delivers one page with resolved content and keeps private previews behind application policy", async (t) => {
|
|
358
|
+
const entry = cases.find(({ provider }) => provider === storyblokProvider);
|
|
359
|
+
const f = await fixture(t, entry);
|
|
360
|
+
await f.service.connectApiKey(f.input);
|
|
361
|
+
const content = { id: 123, full_slug: "bookings/groom & wash", content: { component: "page", title: "Grooming" } };
|
|
362
|
+
f.state.response = { story: content, rels: [{ id: 456, content: { component: "author", name: "Pat" } }],
|
|
363
|
+
assets: [{ id: 789, filename: "https://a.storyblok.com/f/123/photo.jpg", alt: "A groomed dog" }], links: [], cv: 42 };
|
|
364
|
+
assert.deepEqual(await f.service.invoke({ ...f.input, operation: "stories.get", input: {
|
|
365
|
+
id: content.full_slug, resolve_relations: "page.author", resolve_links: "url", resolve_assets: 1, cv: 42
|
|
366
|
+
} }), f.state.response);
|
|
367
|
+
const request = f.requests.at(-1);
|
|
368
|
+
assert.equal(request.init.method, "GET");
|
|
369
|
+
assert.equal(request.url.pathname, "/v2/cdn/stories/bookings/groom%20%26%20wash");
|
|
370
|
+
assert.deepEqual(Object.fromEntries(request.url.searchParams), { version: "published", cv: "42",
|
|
371
|
+
resolve_relations: "page.author", resolve_links: "url", resolve_assets: "1", token: f.state.key });
|
|
372
|
+
const restricted = createConnectionService({ ...f.options, authorize: async (owner, request) => request.input?.version === "draft" ? null : owner });
|
|
373
|
+
const before = f.requests.length;
|
|
374
|
+
await assert.rejects(restricted.invoke({ ...f.input, operation: "stories.get", input: { id: "home", version: "draft" } }), { code: "connector_access_denied" });
|
|
375
|
+
for (const id of ["../private", "a/../private", "a//b", "/home"]) {
|
|
376
|
+
await assert.rejects(f.service.invoke({ ...f.input, operation: "stories.get", input: { id } }), { code: "connector_input_invalid" });
|
|
377
|
+
}
|
|
378
|
+
assert.equal(f.requests.length, before);
|
|
379
|
+
f.state.response = { story: { id: 123, content: null } };
|
|
380
|
+
await assert.rejects(f.service.invoke({ ...f.input, operation: "stories.get", input: { id: "home" } }), { code: "connector_response_invalid" });
|
|
381
|
+
f.state.status = 404; f.state.response = { message: "Not found" };
|
|
382
|
+
await assert.rejects(f.service.invoke({ ...f.input, operation: "stories.get", input: { id: "unpublished" } }));
|
|
383
|
+
assert.equal(f.requests.at(-1).url.searchParams.get("version"), "published");
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("regional and account readers retain pagination defaults and reject oversized pages", () => {
|
|
387
|
+
for (const [provider, settings, size, maximum, defaults] of [
|
|
388
|
+
[paddleProvider, { environment: "sandbox" }, "per_page", 200, { per_page: "50", status: "active" }],
|
|
389
|
+
[mailgunProvider, { region: "us" }, "limit", 1000, { limit: "100", skip: "0", include_subaccounts: "false" }],
|
|
390
|
+
[fireworksAiProvider, {}, "pageSize", 200, { pageSize: "50" }],
|
|
391
|
+
[polarProvider, { environment: "sandbox" }, "limit", 100, { page: "1", limit: "10" }]
|
|
392
|
+
]) {
|
|
393
|
+
const operation = provider.operations[provider.checkOperation];
|
|
394
|
+
const url = new URL(operation.request({}, settings).url);
|
|
395
|
+
assert.deepEqual(Object.fromEntries(url.searchParams), defaults);
|
|
396
|
+
for (const value of [0, maximum + 1]) assert.throws(() => operation.request({ [size]: value }, settings));
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("provider page sizes reject zero and values above the provider limit", () => {
|
|
401
|
+
for (const [provider, operation, field, maximum] of [
|
|
402
|
+
[notionProvider, "content.search", "page_size", 100], [brevoProvider, "contacts.list", "limit", 1000],
|
|
403
|
+
[elevenlabsProvider, "voices.list", "page_size", 100], [githubApiProvider, "repositories.list", "per_page", 100],
|
|
404
|
+
[apifyProvider, "actors.list", "limit", 1000], [hubspotProvider, "contacts.list", "limit", 100],
|
|
405
|
+
[linearProvider, "issues.list", "first", 100], [gitlabApiProvider, "projects.list", "per_page", 100],
|
|
406
|
+
[tallyProvider, "forms.list", "limit", 500], [contentfulProvider, "entries.list", "limit", 1000],
|
|
407
|
+
[asanaProvider, "workspaces.list", "limit", 100],
|
|
408
|
+
[incidentIoProvider, "incidents.list", "page_size", 500],
|
|
409
|
+
[firefliesProvider, "transcripts.list", "limit", 50], [heygenProvider, "voices.list", "limit", 100]
|
|
410
|
+
]) {
|
|
411
|
+
for (const value of [0, maximum + 1]) assert.throws(() => provider.operations[operation].request({ [field]: value }), (error) => Boolean(error.fieldErrors[field]));
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("Linear rejects GraphQL errors even when the HTTP request and some data succeed", async (t) => {
|
|
416
|
+
const entry = cases.find(({ provider }) => provider === linearProvider);
|
|
417
|
+
const { service, input, state, requests } = await fixture(t, entry);
|
|
418
|
+
state.response = { ...entry.response, errors: [{ message: state.key, extensions: { code: "FORBIDDEN" } }] };
|
|
419
|
+
await assert.rejects(service.connectApiKey(input), { code: "connector_response_invalid" });
|
|
420
|
+
assert.equal((await service.status(input)).status, "disconnected");
|
|
421
|
+
assert.deepEqual(JSON.parse(requests[0].init.body), { query: "query ConnectorViewer { viewer { id name email } }", variables: {} });
|
|
422
|
+
state.response = entry.response;
|
|
423
|
+
await service.connectApiKey(input);
|
|
424
|
+
state.response = { ...entry.operationResponse, errors: [{ message: state.key }] };
|
|
425
|
+
await assert.rejects(service.invoke({ ...input, operation: "issues.list" }), { code: "connector_response_invalid" });
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("Calendly requires a user URI and GitLab keeps project lists within membership", () => {
|
|
429
|
+
assert.throws(() => calendlyProvider.operations["eventTypes.list"].request({}), (error) => Boolean(error.fieldErrors.user));
|
|
430
|
+
for (const count of [0, 101]) {
|
|
431
|
+
assert.throws(() => calendlyProvider.operations["eventTypes.list"].request({ user: "https://api.calendly.com/users/fixture", count }), (error) => Boolean(error.fieldErrors.count));
|
|
432
|
+
}
|
|
433
|
+
for (const field of ["membership", "simple"]) {
|
|
434
|
+
assert.throws(() => gitlabApiProvider.operations["projects.list"].request({ [field]: false }), (error) => Boolean(error.fieldErrors[field]));
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
test("Fireflies rejects partial GraphQL errors and uses the authenticated user's profile", async (t) => {
|
|
440
|
+
const entry = cases.find(({ provider }) => provider === firefliesProvider);
|
|
441
|
+
const { service, input, state, requests } = await fixture(t, entry);
|
|
442
|
+
state.response = { ...entry.response, errors: [{ message: state.key }] };
|
|
443
|
+
await assert.rejects(service.connectApiKey(input), { code: "connector_response_invalid" });
|
|
444
|
+
assert.equal((await service.status(input)).status, "disconnected");
|
|
445
|
+
assert.deepEqual(JSON.parse(requests[0].init.body), { query: "query ConnectorUser { user { user_id name email } }", variables: {} });
|
|
446
|
+
state.response = entry.response;
|
|
447
|
+
await service.connectApiKey(input);
|
|
448
|
+
state.response = { data: { transcript: { id: "meeting-1", title: "Planning", sentences: [{ speaker_name: "Ana", text: "Book the appointment", start_time: 1, end_time: 3 }], summary: { action_items: "Book appointment" } } } };
|
|
449
|
+
const detail = await service.invoke({ ...input, operation: "transcripts.get", input: { id: "meeting-1" } });
|
|
450
|
+
assert.equal(detail.data.transcript.sentences[0].text, "Book the appointment");
|
|
451
|
+
assert.equal(detail.data.transcript.summary.action_items, "Book appointment");
|
|
452
|
+
assert.deepEqual(JSON.parse(requests.at(-1).init.body).variables, { id: "meeting-1" });
|
|
453
|
+
state.response = { data: { transcript: null } };
|
|
454
|
+
await assert.rejects(service.invoke({ ...input, operation: "transcripts.get", input: { id: "missing" } }), { code: "connector_response_invalid" });
|
|
455
|
+
state.response = entry.operationResponse;
|
|
456
|
+
await service.invoke({ ...input, operation: "transcripts.search", input: { keyword: "appointment", skip: 25 } });
|
|
457
|
+
assert.deepEqual(JSON.parse(requests.at(-1).init.body).variables, { keyword: "appointment", scope: "all", limit: 25, skip: 25, mine: true });
|
|
458
|
+
await assert.rejects(service.invoke({ ...input, operation: "transcripts.search", input: { keyword: "", scope: "arbitrary" } }), { code: "connector_input_invalid" });
|
|
459
|
+
state.response = { ...entry.operationResponse, errors: [{ message: state.key }] };
|
|
460
|
+
await assert.rejects(service.invoke({ ...input, operation: "transcripts.list" }), { code: "connector_response_invalid" });
|
|
461
|
+
assert.deepEqual(JSON.parse(requests.at(-1).init.body).variables, { limit: 25, skip: 0, mine: true });
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test("new read fragments preserve empty collections and reject invalid envelopes and boundaries", () => {
|
|
465
|
+
const incidents = incidentIoProvider.operations["incidents.list"];
|
|
466
|
+
assert.equal(incidents.validateResult({ incidents: [] }), true);
|
|
467
|
+
assert.equal(incidents.validateResult({ incidents: [], pagination_meta: null }), false);
|
|
468
|
+
assert.throws(() => incidents.request({ sort_by: "arbitrary" }));
|
|
469
|
+
const initial = new URL(incidents.request({}).url);
|
|
470
|
+
assert.equal(initial.searchParams.get("page_size"), "25");
|
|
471
|
+
assert.equal(initial.searchParams.get("sort_by"), "created_at_newest_first");
|
|
472
|
+
const balance = stripeProvider.operations["balance.read"];
|
|
473
|
+
assert.equal(balance.validateResult({ object: "balance", available: [], pending: [], livemode: true }), true);
|
|
474
|
+
assert.equal(balance.validateResult({ object: "balance", available: [], pending: [] }), false);
|
|
475
|
+
assert.equal(balance.request({}).url, "https://api.stripe.com/v1/balance");
|
|
476
|
+
const hardware = replicateProvider.operations["hardware.list"];
|
|
477
|
+
assert.equal(hardware.validateResult([]), true);
|
|
478
|
+
assert.equal(hardware.validateResult([null]), false);
|
|
479
|
+
for (const skip of [-1, 2147483648]) assert.throws(() => firefliesProvider.operations["transcripts.list"].request({ skip }));
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
test("AI service readers distinguish authenticated checks and preserve provider paging fields", () => {
|
|
484
|
+
assert.equal(perplexityProvider.checkOperation, "requests.list");
|
|
485
|
+
assert.equal(perplexityProvider.operations["requests.list"].validateResult({ requests: [], next_token: null }), true);
|
|
486
|
+
assert.equal(perplexityProvider.operations["requests.list"].validateResult({ object: "list", data: [] }), false);
|
|
487
|
+
assert.equal(supabaseProvider.operations["projects.list"].validateResult([]), true);
|
|
488
|
+
assert.equal(supabaseProvider.operations["projects.list"].validateResult([{ error: "no" }]), false);
|
|
489
|
+
const voices = heygenProvider.operations["voices.list"];
|
|
490
|
+
assert.equal(voices.validateResult({ data: [], has_more: false }), true);
|
|
491
|
+
assert.equal(voices.validateResult({ data: [], has_more: "false" }), false);
|
|
492
|
+
assert.throws(() => voices.request({ type: "arbitrary" }));
|
|
493
|
+
assert.throws(() => voices.request({ gender: "arbitrary" }));
|
|
494
|
+
const url = new URL(voices.request({}).url);
|
|
495
|
+
assert.equal(url.searchParams.get("limit"), "20");
|
|
496
|
+
assert.equal(url.searchParams.get("type"), "public");
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test("Ashby preserves incremental-sync state and rejects HTTP-success errors or broken cursors", async (t) => {
|
|
500
|
+
const entry = cases.find((item) => item.provider === ashbyProvider);
|
|
501
|
+
const { service, input, state, requests } = await fixture(t, entry);
|
|
502
|
+
state.response = { success: false, errors: [state.key], errorInfo: { code: "next_cursor_expired", message: state.key } };
|
|
503
|
+
await assert.rejects(service.connectApiKey(input), (error) => {
|
|
504
|
+
assert.equal(error.code, "connector_provider_failed");
|
|
505
|
+
assert.equal(error.message.includes(state.key), false);
|
|
506
|
+
assert.equal(JSON.stringify(error).includes(state.key), false);
|
|
507
|
+
return true;
|
|
508
|
+
});
|
|
509
|
+
assert.equal((await service.status(input)).status, "disconnected");
|
|
510
|
+
state.response = entry.response;
|
|
511
|
+
await service.connectApiKey(input);
|
|
512
|
+
assert.deepEqual(JSON.parse(requests.at(-1).init.body), { limit: 25 });
|
|
513
|
+
const count = requests.length;
|
|
514
|
+
for (const invalid of [{ limit: 0 }, { limit: 101 }, { limit: 1.5 }, { cursor: "" }, { cursor: "x".repeat(8193) }, { syncToken: "" }, { syncToken: "x".repeat(8193) }, { apiKey: "other" }, { url: "https://attacker.invalid" }]) {
|
|
515
|
+
await assert.rejects(service.invoke({ ...input, operation: "jobs.list", input: invalid }), { code: "connector_input_invalid" });
|
|
516
|
+
}
|
|
517
|
+
for (const key of ["key:password", "key with space", "key\nheader"]) {
|
|
518
|
+
state.key = key;
|
|
519
|
+
await assert.rejects(service.invoke({ ...input, operation: "jobs.list" }), { code: "connector_binding_missing" });
|
|
520
|
+
}
|
|
521
|
+
assert.equal(requests.length, count);
|
|
522
|
+
state.key = "private-fixture-key";
|
|
523
|
+
for (const response of [{ success: true, results: [] }, { ...entry.response, nextCursor: "" }, { ...entry.response, syncToken: {} }]) {
|
|
524
|
+
state.response = response;
|
|
525
|
+
await assert.rejects(service.invoke({ ...input, operation: "jobs.list" }), { code: "connector_response_invalid" });
|
|
526
|
+
}
|
|
527
|
+
state.response = { success: true, results: [], moreDataAvailable: false, syncToken: "next-sync" };
|
|
528
|
+
assert.deepEqual(await service.invoke({ ...input, operation: "jobs.list", input: { cursor: "next-cursor", syncToken: "current-sync" } }), state.response);
|
|
529
|
+
assert.deepEqual(JSON.parse(requests.at(-1).init.body), { limit: 25, cursor: "next-cursor", syncToken: "current-sync" });
|
|
530
|
+
state.status = 403;
|
|
531
|
+
state.response = { success: false, errors: ["missing_endpoint_permission"] };
|
|
532
|
+
await assert.rejects(service.invoke({ ...input, operation: "jobs.list" }), { code: "connector_permission_denied" });
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
test("Lexware validates contact pages and role filters without accepting unsupported search or pagination values", async (t) => {
|
|
536
|
+
const entry = cases.find((item) => item.provider === lexwareProvider);
|
|
537
|
+
const { service, input, state, requests } = await fixture(t, entry);
|
|
538
|
+
await service.connectApiKey(input);
|
|
539
|
+
assert.deepEqual(Object.fromEntries(requests[0].url.searchParams), { page: "0", size: "25" });
|
|
540
|
+
for (const invalid of [{ page: -1 }, { page: 1.5 }, { size: 0 }, { size: 251 }, { size: 1.5 }, { customer: "maybe" }, { vendor: [] }, { name: "A&B" }]) {
|
|
541
|
+
await assert.rejects(service.invoke({ ...input, operation: "contacts.list", input: invalid }), { code: "connector_input_invalid" });
|
|
542
|
+
}
|
|
543
|
+
assert.equal(requests.length, 1);
|
|
544
|
+
state.response = { content: [], first: true, last: true, totalPages: 0, totalElements: 0, numberOfElements: 0, size: 250, number: 0 };
|
|
545
|
+
assert.deepEqual(await service.invoke({ ...input, operation: "contacts.list", input: { size: 250, customer: true, vendor: false } }), state.response);
|
|
546
|
+
for (const response of [{ ...state.response, number: -1 }, { ...state.response, last: "true" }, { ...state.response, content: {} }, { ...state.response, size: 0 }]) {
|
|
547
|
+
state.response = response;
|
|
548
|
+
await assert.rejects(service.invoke({ ...input, operation: "contacts.list" }), { code: "connector_response_invalid" });
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
test("Lexware reads accounting resources and creates explicit draft invoices without replay", async (t) => {
|
|
553
|
+
const entry = cases.find(item => item.provider === lexwareProvider);
|
|
554
|
+
const { service, options, input, state, requests } = await fixture(t, entry);
|
|
555
|
+
await service.connectApiKey(input);
|
|
556
|
+
const id = "11111111-1111-4111-8111-111111111111";
|
|
557
|
+
for (const resource of ["contacts", "articles", "invoices", "vouchers"]) {
|
|
558
|
+
state.response = { id, version: 1 };
|
|
559
|
+
assert.deepEqual(await service.invoke({ ...input, operation: `${resource}.get`, input: { id } }), state.response);
|
|
560
|
+
assert.equal(requests.at(-1).url.pathname, `/v1/${resource}/${id}`);
|
|
561
|
+
}
|
|
562
|
+
state.response = { ...entry.response, content: [{ id }], number: 1, last: false };
|
|
563
|
+
await service.invoke({ ...input, operation: "articles.list", input: { page: 1, type: "SERVICE" } });
|
|
564
|
+
assert.equal(requests.at(-1).url.pathname, "/v1/articles");
|
|
565
|
+
assert.equal(requests.at(-1).url.searchParams.get("type"), "SERVICE");
|
|
566
|
+
await service.invoke({ ...input, operation: "vouchers.list", input: { voucherType: "invoice", voucherStatus: "open", contactId: id } });
|
|
567
|
+
assert.equal(requests.at(-1).url.pathname, "/v1/voucherlist");
|
|
568
|
+
assert.equal(requests.at(-1).url.searchParams.get("contactId"), id);
|
|
569
|
+
const draft = { contactId: id, voucherDate: "2026-09-13T12:00:00+02:00", shippingDate: "2026-09-12T12:00:00+02:00", shippingType: "service",
|
|
570
|
+
lineItems: [{ name: "Grooming", quantity: 1, unitName: "session", netAmount: 50, taxRatePercentage: 19 }], title: "Booking invoice" };
|
|
571
|
+
state.response = { id, version: 1, resourceUri: "https://untrusted.invalid/not-followed" };
|
|
572
|
+
await service.invoke({ ...input, operation: "invoices.createDraft", input: draft });
|
|
573
|
+
const sent = requests.at(-1);
|
|
574
|
+
assert.equal(sent.init.method, "POST");
|
|
575
|
+
assert.equal(sent.url.href, "https://api.lexware.io/v1/invoices?finalize=false");
|
|
576
|
+
const body = JSON.parse(sent.init.body);
|
|
577
|
+
assert.deepEqual(body.address, { contactId: id });
|
|
578
|
+
assert.deepEqual(body.taxConditions, { taxType: "net" });
|
|
579
|
+
assert.deepEqual(body.lineItems[0].unitPrice, { currency: "EUR", netAmount: 50, taxRatePercentage: 19 });
|
|
580
|
+
assert.equal(body.lineItems[0].type, "custom");
|
|
581
|
+
const before = requests.length;
|
|
582
|
+
for (const invalid of [{ ...draft, lineItems: [] }, { ...draft, finalize: true }, { ...draft, contactId: "../other" },
|
|
583
|
+
{ ...draft, voucherDate: "2026-02-30T00:00:00Z" }, { ...draft, lineItems: [{ ...draft.lineItems[0], netAmount: -1 }] },
|
|
584
|
+
{ ...draft, lineItems: [{ ...draft.lineItems[0], taxRatePercentage: 101 }] }, { ...draft, lineItems: [{ ...draft.lineItems[0], currency: "USD" }] }])
|
|
585
|
+
await assert.rejects(service.invoke({ ...input, operation: "invoices.createDraft", input: invalid }), { code: "connector_input_invalid" });
|
|
586
|
+
const restricted = createConnectionService({ ...options, authorize: async (owner, request) => request.operation === "invoices.createDraft" ? null : owner });
|
|
587
|
+
await assert.rejects(restricted.invoke({ ...input, operation: "invoices.createDraft", input: draft }), { code: "connector_access_denied" });
|
|
588
|
+
assert.equal(requests.length, before);
|
|
589
|
+
state.response = {};
|
|
590
|
+
await assert.rejects(service.invoke({ ...input, operation: "invoices.createDraft", input: draft }), { code: "connector_response_invalid" });
|
|
591
|
+
for (const status of [403, 406, 429, 504]) {
|
|
592
|
+
state.status = status; const count = requests.length;
|
|
593
|
+
await assert.rejects(service.invoke({ ...input, operation: "invoices.createDraft", input: draft }));
|
|
594
|
+
assert.equal(requests.length, count + 1);
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test("Sevdesk includes people by default, preserves numeric-string totals and bounds contact pagination", async (t) => {
|
|
599
|
+
const entry = cases.find((item) => item.provider === sevdeskProvider);
|
|
600
|
+
const { service, input, state, requests } = await fixture(t, entry);
|
|
601
|
+
await service.connectApiKey(input);
|
|
602
|
+
assert.deepEqual(Object.fromEntries(requests[0].url.searchParams), { limit: "100", offset: "0", countAll: "false", depth: "1" });
|
|
603
|
+
for (const invalid of [{ limit: 0 }, { limit: 1001 }, { limit: 1.5 }, { offset: -1 }, { offset: 1.5 }, { depth: 2 }, { countAll: "maybe" }, { Authorization: "other-token" }]) {
|
|
604
|
+
await assert.rejects(service.invoke({ ...input, operation: "contacts.list", input: invalid }), { code: "connector_input_invalid" });
|
|
605
|
+
}
|
|
606
|
+
assert.equal(requests.length, 1);
|
|
607
|
+
for (const total of [undefined, "0", 0]) {
|
|
608
|
+
state.response = { objects: [], ...(total === undefined ? {} : { total }) };
|
|
609
|
+
assert.deepEqual(await service.invoke({ ...input, operation: "contacts.list", input: { limit: 1000, countAll: true, depth: 0 } }), state.response);
|
|
610
|
+
}
|
|
611
|
+
for (const response of [{ objects: {} }, { objects: [], total: -1 }, { objects: [], total: "-1" }, { objects: [], total: "unknown" }, { objects: [], total: 1.5 }]) {
|
|
612
|
+
state.response = response;
|
|
613
|
+
await assert.rejects(service.invoke({ ...input, operation: "contacts.list" }), { code: "connector_response_invalid" });
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test("Apollo searches saved accounts with bounded pages and cannot substitute a public health reply", async (t) => {
|
|
618
|
+
const entry = cases.find((entry) => entry.provider === apolloIoProvider);
|
|
619
|
+
const { service, input, state, requests } = await fixture(t, entry);
|
|
620
|
+
for (const response of [{ healthy: true, is_logged_in: false }, { organizations: [] }, { accounts: {} }]) {
|
|
621
|
+
state.response = response;
|
|
622
|
+
await assert.rejects(service.connectApiKey(input), { code: "connector_response_invalid" });
|
|
623
|
+
assert.equal((await service.status(input)).status, "disconnected");
|
|
624
|
+
}
|
|
625
|
+
state.response = entry.response;
|
|
626
|
+
await service.connectApiKey(input);
|
|
627
|
+
assert.deepEqual(JSON.parse(requests.at(-1).init.body), { page: 1, per_page: 25 });
|
|
628
|
+
assert.equal(requests.at(-1).headers.has("authorization"), false);
|
|
629
|
+
const count = requests.length;
|
|
630
|
+
for (const invalid of [{ page: 0 }, { page: 501 }, { page: 1.5 }, { per_page: 0 }, { per_page: 101 }, { per_page: 1.5 }, { q_organization_name: "" }, { q_organization_name: "x".repeat(257) }, { api_key: "other" }, { url: "https://attacker.invalid" }]) {
|
|
631
|
+
await assert.rejects(service.invoke({ ...input, operation: "accounts.search", input: invalid }), { code: "connector_input_invalid" });
|
|
632
|
+
}
|
|
633
|
+
assert.equal(requests.length, count);
|
|
634
|
+
state.response = { accounts: [], breadcrumbs: [] };
|
|
635
|
+
assert.deepEqual(await service.invoke({ ...input, operation: "accounts.search", input: { page: 500, per_page: 100 } }), state.response);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
test("Attention uses the current conversation listing and preserves partial metadata without following returned URLs", async (t) => {
|
|
639
|
+
const entry = cases.find((entry) => entry.provider === attentionProvider);
|
|
640
|
+
const { service, input, state, requests } = await fixture(t, entry);
|
|
641
|
+
await service.connectApiKey(input);
|
|
642
|
+
assert.deepEqual(Object.fromEntries(requests[0].url.searchParams), { page: "1", size: "20", detailedTranscript: "false" });
|
|
643
|
+
for (const invalid of [{ page: 0 }, { page: 1.5 }, { size: 0 }, { size: 51 }, { size: 1.5 }, { detailedTranscript: "maybe" }, { "filter[title]": "" }, { "filter[title]": "x".repeat(501) }, { "filter[hide_internal]": [] }, { withCrmRecords: true }, { url: "https://attacker.invalid" }]) {
|
|
644
|
+
await assert.rejects(service.invoke({ ...input, operation: "conversations.list", input: invalid }), { code: "connector_input_invalid" });
|
|
645
|
+
}
|
|
646
|
+
assert.equal(requests.length, 1);
|
|
647
|
+
state.response = { data: [], meta: { totalRecords: 0 }, links: { related: "https://attacker.invalid" } };
|
|
648
|
+
assert.deepEqual(await service.invoke({ ...input, operation: "conversations.list", input: { page: 3, size: 50 } }), state.response);
|
|
649
|
+
assert.equal(requests.length, 2);
|
|
650
|
+
assert.equal(requests.at(-1).url.origin, "https://api.attention.tech");
|
|
651
|
+
state.response = { data: [] };
|
|
652
|
+
assert.deepEqual(await service.invoke({ ...input, operation: "conversations.list" }), state.response);
|
|
653
|
+
for (const response of [{ data: {} }, { data: [], meta: [] }, { data: [], meta: null }, { data: [], meta: { totalRecords: -1 } }, { data: [], meta: { pageCount: 1.5 } }]) {
|
|
654
|
+
state.response = response;
|
|
655
|
+
await assert.rejects(service.invoke({ ...input, operation: "conversations.list" }), { code: "connector_response_invalid" });
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
test("Telegram verifies bot identity without consuming updates and accepts polling webhook state", async (t) => {
|
|
661
|
+
const entry = cases.find((entry) => entry.provider === telegramProvider);
|
|
662
|
+
const { service, input, state, requests } = await fixture(t, entry);
|
|
663
|
+
await service.connectApiKey(input);
|
|
664
|
+
state.response = { ok: true, result: { url: "", has_custom_certificate: false, pending_update_count: 0 } };
|
|
665
|
+
assert.deepEqual(await service.invoke({ ...input, operation: "webhook.read" }), state.response);
|
|
666
|
+
for (const response of [
|
|
667
|
+
{ ok: true, result: { url: "", has_custom_certificate: false, pending_update_count: -1 } },
|
|
668
|
+
{ ok: true, result: { url: "", pending_update_count: 0 } }
|
|
669
|
+
]) {
|
|
670
|
+
state.response = response;
|
|
671
|
+
await assert.rejects(service.invoke({ ...input, operation: "webhook.read" }), { code: "connector_response_invalid" });
|
|
672
|
+
}
|
|
673
|
+
const count = requests.length;
|
|
674
|
+
await assert.rejects(service.invoke({ ...input, operation: "getUpdates" }), { code: "connector_operation_unknown" });
|
|
675
|
+
for (const invalid of [{ offset: 2 }, { token: "other" }, { url: "https://attacker.invalid" }]) {
|
|
676
|
+
await assert.rejects(service.invoke({ ...input, operation: "webhook.read", input: invalid }), { code: "connector_input_invalid" });
|
|
677
|
+
}
|
|
678
|
+
for (const key of ["123:has/slash", "123:has?query", "../token", "123:has space", "123:"]) {
|
|
679
|
+
state.key = key;
|
|
680
|
+
await assert.rejects(service.connectApiKey(input), { code: "connector_binding_invalid" });
|
|
681
|
+
}
|
|
682
|
+
assert.equal(requests.length, count);
|
|
683
|
+
assert.ok(requests.every(({ url }) => ["/bot123456:fixture_token/getMe", "/bot123456:fixture_token/getWebhookInfo"].includes(url.pathname)));
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
test("Telegram rejects user identities and HTTP-200 failure envelopes without echoing descriptions", async (t) => {
|
|
687
|
+
const entry = cases.find((entry) => entry.provider === telegramProvider);
|
|
688
|
+
const { service, input, state } = await fixture(t, entry);
|
|
689
|
+
for (const bot of [{ id: 12, is_bot: false, first_name: "User" }, { id: 1.5, is_bot: true, first_name: "Bot" }, { id: 12, is_bot: true }]) {
|
|
690
|
+
state.response = { ok: true, result: bot };
|
|
691
|
+
await assert.rejects(service.connectApiKey(input), { code: "connector_response_invalid" });
|
|
692
|
+
assert.equal((await service.status(input)).status, "disconnected");
|
|
693
|
+
}
|
|
694
|
+
state.response = entry.response;
|
|
695
|
+
await service.connectApiKey(input);
|
|
696
|
+
for (const [status, code] of [[403, "connector_permission_denied"], [429, "connector_rate_limited"], [400, "connector_provider_failed"], [401, "connector_reconnect_required"]]) {
|
|
697
|
+
state.response = { ok: false, error_code: status, description: state.key, parameters: { retry_after: 10 } };
|
|
698
|
+
await assert.rejects(service.invoke({ ...input, operation: "profile.read" }), (error) => {
|
|
699
|
+
assert.equal(error.code, code);
|
|
700
|
+
assert.equal(error.stack.includes(state.key), false);
|
|
701
|
+
assert.equal(error.cause, undefined);
|
|
702
|
+
return true;
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
assert.equal((await service.status(input)).status, "reconnect-required");
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
test("KLIPY validates clip search bounds, encodes its path key and rejects unsuccessful envelopes", async (t) => {
|
|
709
|
+
const entry = cases.find((entry) => entry.provider === klipyProvider);
|
|
710
|
+
const { service, input, state, requests } = await fixture(t, entry);
|
|
711
|
+
state.key = "key /?#%";
|
|
712
|
+
await service.connectApiKey(input);
|
|
713
|
+
assert.equal(requests[0].url.pathname, "/api/v1/key%20%2F%3F%23%25/clips/trending");
|
|
714
|
+
assert.equal(requests[0].url.search, "?page=1&per_page=24&content_filter=high");
|
|
715
|
+
for (const invalid of [{}, { q: "" }, { q: "x".repeat(501) }, { q: "test", page: 0 }, { q: "test", page: 1.5 },
|
|
716
|
+
{ q: "test", per_page: 7 }, { q: "test", per_page: 51 }, { q: "test", per_page: 8.5 },
|
|
717
|
+
{ q: "test", locale: "en_US" }, { q: "test", customer_id: "" }, { q: "test", content_filter: "strict" },
|
|
718
|
+
{ q: "test", app_key: "another" }, { q: "test", url: "https://attacker.invalid" }]) {
|
|
719
|
+
await assert.rejects(service.invoke({ ...input, operation: "clips.search", input: invalid }), { code: "connector_input_invalid" });
|
|
720
|
+
}
|
|
721
|
+
assert.equal(requests.length, 1);
|
|
722
|
+
for (const key of [".", ".."]) {
|
|
723
|
+
state.key = key;
|
|
724
|
+
await assert.rejects(service.connectApiKey(input), { code: "connector_request_invalid" });
|
|
725
|
+
}
|
|
726
|
+
assert.equal(requests.length, 1);
|
|
727
|
+
state.key = "replacement-key";
|
|
728
|
+
state.response = { result: true, data: { data: [], next: false } };
|
|
729
|
+
assert.deepEqual(await service.invoke({ ...input, operation: "clips.search", input: { q: "nothing" } }), state.response);
|
|
730
|
+
for (const [response, code] of [
|
|
731
|
+
[{ result: false, data: { data: [], message: state.key } }, "connector_provider_failed"],
|
|
732
|
+
[{ result: true, data: [] }, "connector_response_invalid"],
|
|
733
|
+
[{ result: true, data: { data: {} } }, "connector_response_invalid"]
|
|
734
|
+
]) {
|
|
735
|
+
state.response = response;
|
|
736
|
+
await assert.rejects(service.invoke({ ...input, operation: "clips.trending" }), (error) => {
|
|
737
|
+
assert.equal(error.code, code);
|
|
738
|
+
assert.equal(error.stack.includes(state.key), false);
|
|
739
|
+
return true;
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
test("KLIPY media families preserve embedding metadata and never fetch media", async (t) => {
|
|
746
|
+
const entry = cases.find((entry) => entry.provider === klipyProvider);
|
|
747
|
+
const { service, input, state, requests } = await fixture(t, entry);
|
|
748
|
+
await service.connectApiKey(input);
|
|
749
|
+
for (const family of ["gifs", "stickers", "emojis"]) {
|
|
750
|
+
state.response = { result: true, data: { data: [
|
|
751
|
+
{ type: family, slug: "happy-dog", file: { hd: { webp: { url: "https://static.klipy.com/dog.webp?delivery=keep" } } } },
|
|
752
|
+
{ type: "ad", id: "ad-1", tracking: "preserve" }
|
|
753
|
+
], has_next: true, current_page: 2 } };
|
|
754
|
+
for (const action of ["trending", "search"]) {
|
|
755
|
+
const result = await service.invoke({ ...input, operation: `${family}.${action}`,
|
|
756
|
+
input: { page: 2, per_page: 8, ...(action === "search" ? { q: "happy dog" } : {}) } });
|
|
757
|
+
assert.deepEqual(result, state.response);
|
|
758
|
+
const request = requests.at(-1);
|
|
759
|
+
assert.equal(request.url.pathname, `/api/v1/${state.key}/${family}/${action}`);
|
|
760
|
+
assert.equal(request.url.searchParams.get("page"), "2");
|
|
761
|
+
assert.equal(request.url.searchParams.get("content_filter"), "high");
|
|
762
|
+
if (action === "search") assert.equal(request.url.searchParams.get("q"), "happy dog");
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
assert.equal(requests.length, 7);
|
|
766
|
+
assert(requests.every(({ url }) => url.origin === "https://api.klipy.com"));
|
|
767
|
+
await assert.rejects(service.invoke({ ...input, operation: "gifs.search", input: { q: "dog", per_page: 7 } }), { code: "connector_input_invalid" });
|
|
768
|
+
assert.equal(requests.length, 7);
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
test("Contentful uses the selected delivery space and rejects invalid settings before transport", async (t) => {
|
|
773
|
+
const entry = cases.find(({ provider }) => provider === contentfulProvider);
|
|
774
|
+
const { options, input, requests } = await fixture(t, entry);
|
|
775
|
+
const configured = (settings) => ({ ...options, configuration: {
|
|
776
|
+
...options.configuration, integrations: { source: {
|
|
777
|
+
...options.configuration.integrations.source, settings
|
|
778
|
+
} }
|
|
779
|
+
} });
|
|
780
|
+
for (const settings of [{}, { spaceId: "../other" }, { spaceId: "space-one", environmentId: "../../other" },
|
|
781
|
+
{ spaceId: "space-one", region: "elsewhere" }]) {
|
|
782
|
+
assert.throws(() => createConnectionService(configured(settings)));
|
|
783
|
+
}
|
|
784
|
+
assert.equal(requests.length, 0);
|
|
785
|
+
const service = createConnectionService(configured({ spaceId: "space-one", environmentId: "preview-2", region: "eu" }));
|
|
786
|
+
await service.connectApiKey(input);
|
|
787
|
+
assert.equal(requests[0].url.href, "https://cdn.eu.contentful.com/spaces/space-one/environments/preview-2/entries?limit=1&skip=0");
|
|
788
|
+
assert.equal(requests[0].headers.get("authorization"), "Bearer private-fixture-key");
|
|
789
|
+
assert.equal(requests[0].url.searchParams.has("access_token"), false);
|
|
790
|
+
const operation = contentfulProvider.operations["entries.list"];
|
|
791
|
+
assert.equal(operation.validateResult({ sys: { type: "Array" }, items: [{ sys: { type: "Entry", id: "entry-1" } }], total: 1, limit: 1, skip: 0 }), true);
|
|
792
|
+
for (const items of [[null], [{ sys: { type: "Space", id: "space-one" } }]]) {
|
|
793
|
+
assert.equal(operation.validateResult({ ...entry.response, items }), false);
|
|
794
|
+
}
|
|
795
|
+
});
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
test("Contentful delivers published localized entries and linked assets without fetching returned URLs", async t => {
|
|
799
|
+
const { service, input, state, requests } = await fixture(t, cases.find(({ provider }) => provider === contentfulProvider));
|
|
800
|
+
await service.connectApiKey(input);
|
|
801
|
+
const asset = { sys: { type: "Asset", id: "hero" }, fields: { file: { url: "//images.ctfassets.net/space/hero/photo.jpg" } } };
|
|
802
|
+
const article = { sys: { type: "Entry", id: "article" }, fields: { title: { "en-US": "Published story" }, image: { sys: { type: "Link", linkType: "Asset", id: "hero" } } } };
|
|
803
|
+
state.response = { sys: { type: "Array" }, items: [article], includes: { Asset: [asset] }, errors: [{ sys: { id: "notResolvable", type: "error" }, details: { id: "unpublished-link" } }], total: 3, skip: 0, limit: 1 };
|
|
804
|
+
const invoke = (operation, values = {}) => service.invoke({ ...input, operation, input: values });
|
|
805
|
+
assert.deepEqual(await invoke("entries.list", { locale: "*", include: 10, content_type: "article", order: "sys.createdAt", "sys.id": "article" }), state.response);
|
|
806
|
+
assert.equal(requests.at(-1).url.searchParams.get("locale"), "*"); assert.equal(requests.at(-1).url.searchParams.get("include"), "10");
|
|
807
|
+
state.response = { ...state.response, items: [], includes: {}, skip: 2 };
|
|
808
|
+
assert.deepEqual(await invoke("entries.list", { skip: 2 }), state.response);
|
|
809
|
+
for (const [operation, path, item] of [["entries.get", "entries/article", article], ["assets.get", "assets/hero", asset], ["contentTypes.get", "content_types/model", { sys: { type: "ContentType", id: "model" }, fields: [{ id: "title", type: "Text" }] }]]) {
|
|
810
|
+
state.response = item; assert.deepEqual(await invoke(operation, { id: item.sys.id }), item);
|
|
811
|
+
assert.equal(requests.at(-1).url.pathname, `/spaces/space-one/environments/master/${path}`);
|
|
812
|
+
}
|
|
813
|
+
for (const [operation, type] of [["assets.list", "Asset"], ["contentTypes.list", "ContentType"], ["locales.list", "Locale"]]) {
|
|
814
|
+
state.response = { sys: { type: "Array" }, items: [{ sys: { type, id: "resource" } }], skip: 1, limit: 2, total: 3 };
|
|
815
|
+
assert.deepEqual(await invoke(operation, { skip: 1, limit: 2 }), state.response);
|
|
816
|
+
}
|
|
817
|
+
assert(requests.every(request => request.init.method === "GET" && request.url.origin === "https://cdn.contentful.com"));
|
|
818
|
+
const before = requests.length;
|
|
819
|
+
for (const [operation, values] of [["entries.get", { id: "../outside" }], ["entries.list", { include: 11 }], ["assets.get", { id: "hero", url: "https://other.test" }]])
|
|
820
|
+
await assert.rejects(invoke(operation, values), { code: "connector_input_invalid" });
|
|
821
|
+
assert.equal(requests.length, before);
|
|
822
|
+
state.response = { sys: { type: "Entry", id: "wrong-type" } };
|
|
823
|
+
await assert.rejects(invoke("assets.get", { id: "hero" }), { code: "connector_response_invalid" });
|
|
824
|
+
state.status = 404; await assert.rejects(invoke("entries.get", { id: "unpublished" }));
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
test("GatewayAPI sends regional SMS/RCS batches and verifies current Messaging callback signatures", async t => {
|
|
829
|
+
const entry = cases.find(({ provider }) => provider === gatewayApiProvider);
|
|
830
|
+
for (const region of ["global", "eu"]) {
|
|
831
|
+
const { service, input, state, requests } = await fixture(t, { ...entry, settings: { region } });
|
|
832
|
+
await service.connectApiKey(input);
|
|
833
|
+
const message = { sender: "Bookings", recipient: 4512345678, message: "Ready", reference: "booking-one" };
|
|
834
|
+
const receipt = { msg_id: "message-one", recipient: message.recipient, reference: message.reference };
|
|
835
|
+
state.response = receipt;
|
|
836
|
+
assert.deepEqual(await service.invoke({ ...input, operation: "messages.send", input: message }), receipt);
|
|
837
|
+
assert.equal(requests.at(-1).url.href, `https://messaging.gatewayapi.${region === "eu" ? "eu" : "com"}/mobile/single`);
|
|
838
|
+
assert.deepEqual(JSON.parse(requests.at(-1).init.body), { ...message, priority: "normal" });
|
|
839
|
+
state.response = { responses: [receipt] };
|
|
840
|
+
await service.invoke({ ...input, operation: "messages.sendBatch", input: { messages: [message] } });
|
|
841
|
+
assert.equal(requests.at(-1).url.pathname, "/mobile/multi");
|
|
842
|
+
const before = requests.length;
|
|
843
|
+
for (const invalid of [{ ...message, recipient: -1 }, { ...message, reference: "" }, { ...message, sender: "TooLongSenderName" }]) await assert.rejects(service.invoke({ ...input, operation: "messages.send", input: invalid }), { code: "connector_input_invalid" });
|
|
844
|
+
await assert.rejects(service.invoke({ ...input, operation: "messages.sendBatch", input: { messages: [] } }), { code: "connector_input_invalid" });
|
|
845
|
+
assert.equal(requests.length, before);
|
|
846
|
+
state.response = { responses: [{}] };
|
|
847
|
+
await assert.rejects(service.invoke({ ...input, operation: "messages.sendBatch", input: { messages: [message] } }), { code: "connector_response_invalid" });
|
|
848
|
+
state.status = 500; const failedBefore = requests.length;
|
|
849
|
+
await assert.rejects(service.invoke({ ...input, operation: "messages.send", input: message }), { code: "connector_provider_failed" });
|
|
850
|
+
assert.equal(requests.length, failedBefore + 1);
|
|
851
|
+
}
|
|
852
|
+
const secret = "fixture-webhook-secret";
|
|
853
|
+
for (const event_type of ["message.status.sms", "message.status.rcs", "user-message.text.sms", "user-message.text.rcs"]) {
|
|
854
|
+
const event = { event_id: "event-one", timestamp: "2026-09-13T00:00:00Z", event_type, event: { msg_id: "message-one", reference: "booking-one", status: "DELIVERED" } };
|
|
855
|
+
const rawBody = Buffer.from(JSON.stringify(event));
|
|
856
|
+
const signature = "v1=" + createHmac("sha256", secret).update(rawBody).digest("hex");
|
|
857
|
+
assert.deepEqual(verifyGatewayApiEvent({ rawBody, signature, secret }), event);
|
|
858
|
+
for (const override of [{ secret: "wrong" }, { rawBody: Buffer.from("{}") }, { signature: "legacy.jwt.value" }]) assert.throws(() => verifyGatewayApiEvent({ rawBody, signature, secret, ...override }), { code: "connector_webhook_invalid" });
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
test("HubSpot leads become linked deals and advance through a pipeline with app-owned policy", async t => {
|
|
863
|
+
const entry = cases.find(item => item.provider.id === "hubspot");
|
|
864
|
+
const f = await fixture(t, { ...entry, scopes: hubspotProvider.scopes.map(scope => scope.value) });
|
|
865
|
+
const calls = [];
|
|
866
|
+
const service = createConnectionService({ ...f.options,
|
|
867
|
+
authorize: async (owner, request) => request.operation === "deals.update" && request.input.id !== "202" ? null : owner,
|
|
868
|
+
fetchImpl: async (address, init) => {
|
|
869
|
+
const url = new URL(address), body = init.body ? JSON.parse(init.body) : undefined;
|
|
870
|
+
calls.push({ url, method: init.method, body });
|
|
871
|
+
assert.equal(new Headers(init.headers).get("authorization"), "Bearer private-fixture-key");
|
|
872
|
+
if (url.pathname.includes("/pipelines/")) return Response.json({ results: [{ id: "sales", stages: [{ id: "qualified" }, { id: "won" }] }] });
|
|
873
|
+
if (url.pathname.includes("/associations/default/")) return Response.json({ status: "COMPLETE", results: [{ from: { id: "202" }, to: { id: "101" } }] });
|
|
874
|
+
if (init.method === "POST" || init.method === "PATCH") return Response.json({ id: url.pathname.includes("deals") ? "202" : "101", properties: body.properties });
|
|
875
|
+
if (/\/[0-9]+$/u.test(url.pathname)) return Response.json({ id: url.pathname.split("/").at(-1), properties: { email: "customer@example.test" }, associations: { contacts: { results: [{ id: "101" }] } } });
|
|
876
|
+
return Response.json({ results: [], paging: { next: { after: "next-page" } } });
|
|
877
|
+
}
|
|
878
|
+
});
|
|
879
|
+
await service.connectApiKey(f.input);
|
|
880
|
+
const invoke = (operation, input = {}) => service.invoke({ ...f.input, operation, input });
|
|
881
|
+
const pipeline = (await invoke("pipelines.list")).results[0];
|
|
882
|
+
const contact = await invoke("contacts.create", { properties: { email: "customer@example.test", firstname: "Customer" } });
|
|
883
|
+
const deal = await invoke("deals.create", { properties: { dealname: "Grooming booking", pipeline: pipeline.id, dealstage: pipeline.stages[0].id, amount: "80.00" } });
|
|
884
|
+
await invoke("deals.associateContact", { contactId: contact.id, dealId: deal.id });
|
|
885
|
+
assert.equal(calls.at(-1).method, "PUT");
|
|
886
|
+
assert.equal(calls.at(-1).url.pathname, "/crm/objects/2026-09/deals/202/associations/default/contacts/101");
|
|
887
|
+
const advanced = await invoke("deals.update", { id: deal.id, properties: { dealstage: "won" } });
|
|
888
|
+
assert.equal(advanced.properties.dealstage, "won");
|
|
889
|
+
assert.equal(calls.at(-1).method, "PATCH");
|
|
890
|
+
assert.deepEqual(calls.at(-1).body, { properties: { dealstage: "won" } });
|
|
891
|
+
await invoke("contacts.update", { id: contact.id, properties: { phone: "" } });
|
|
892
|
+
assert.deepEqual(calls.at(-1).body, { properties: { phone: "" } });
|
|
893
|
+
const read = await invoke("deals.get", { id: deal.id, properties: "dealname,dealstage", associations: "contacts" });
|
|
894
|
+
assert.equal(read.associations.contacts.results[0].id, contact.id);
|
|
895
|
+
assert.equal(calls.at(-1).url.searchParams.get("properties"), "dealname,dealstage");
|
|
896
|
+
await invoke("contacts.get", { id: contact.id });
|
|
897
|
+
assert.equal((await invoke("deals.list", { after: "next & page", limit: 2 })).paging.next.after, "next-page");
|
|
898
|
+
assert.equal(calls.at(-1).url.searchParams.get("after"), "next & page");
|
|
899
|
+
const before = calls.length;
|
|
900
|
+
for (const [operation, input] of [
|
|
901
|
+
["deals.update", { id: "999", properties: { amount: "1" } }],
|
|
902
|
+
["deals.create", { properties: { dealname: "Missing stage" } }],
|
|
903
|
+
["contacts.create", { properties: { phone: "123" } }],
|
|
904
|
+
["contacts.update", { id: "101", properties: { amount: 1 } }],
|
|
905
|
+
["contacts.get", { id: "../contacts" }],
|
|
906
|
+
["deals.associateContact", { contactId: "101/other", dealId: "202" }]
|
|
907
|
+
]) await assert.rejects(invoke(operation, input));
|
|
908
|
+
assert.equal(calls.length, before);
|
|
909
|
+
const limited = createConnectionService({ ...f.options, configuration: { ...f.options.configuration,
|
|
910
|
+
integrations: { source: { ...f.options.configuration.integrations.source, scopes: ["crm.objects.contacts.read"] } } } });
|
|
911
|
+
await assert.rejects(limited.invoke({ ...f.input, operation: "deals.create", input: { properties: { dealname: "Denied", dealstage: "won" } } }), { code: "connector_scope_missing" });
|
|
912
|
+
assert.equal(hubspotProvider.operations["contacts.create"].validateResult({ status: "error" }), false);
|
|
913
|
+
assert.equal(hubspotProvider.operations["deals.associateContact"].validateResult({}), false);
|
|
914
|
+
});
|
|
915
|
+
|
|
916
|
+
test("incident.io incident recovery covers follow-ups, alerts, schedules and catalogue with explicit writes", async t => {
|
|
917
|
+
const f = await fixture(t, cases.find(item => item.provider.id === "incident-io"));
|
|
918
|
+
const calls = [];
|
|
919
|
+
let rejected = false;
|
|
920
|
+
const service = createConnectionService({ ...f.options,
|
|
921
|
+
authorize: async (owner, request) => request.operation === "alerts.resolve" && request.input.id !== "alert-one" ? null : owner,
|
|
922
|
+
fetchImpl: async (address, init) => {
|
|
923
|
+
const url = new URL(address), body = init.body ? JSON.parse(init.body) : undefined;
|
|
924
|
+
calls.push({ url, method: init.method, body });
|
|
925
|
+
assert.equal(new Headers(init.headers).get("authorization"), "Bearer private-fixture-key");
|
|
926
|
+
if (rejected) return Response.json({ message: "Private fixture detail" }, { status: 422 });
|
|
927
|
+
const name = url.pathname.split("/")[2];
|
|
928
|
+
const singular = { incidents: "incident", follow_ups: "follow_up", alerts: "alert", schedules: "schedule", catalog_entries: "catalog_entry" }[name];
|
|
929
|
+
if (init.method !== "GET" || url.pathname.split("/").length > 3) return Response.json({ [singular]: { id: "resource-one", ...body } });
|
|
930
|
+
return Response.json({ [name]: [{ id: "resource-one", ...(name === "schedules" ? { next_shifts: [{ user_id: "responder" }] } : {}) }], pagination_meta: { page_size: 25, after: "next-page" } });
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
await service.connectApiKey(f.input);
|
|
934
|
+
const invoke = (operation, input = {}) => service.invoke({ ...f.input, operation, input });
|
|
935
|
+
const incident = await invoke("incidents.create", { name: "API unavailable", idempotency_key: "outage-event-one", visibility: "private", mode: "test" });
|
|
936
|
+
assert.equal(incident.incident.id, "resource-one");
|
|
937
|
+
assert.equal(calls.at(-1).body.idempotency_key, "outage-event-one");
|
|
938
|
+
await invoke("incidents.update", { id: "resource-one", incident: { summary: "Recovered" }, notify_incident_channel: false });
|
|
939
|
+
assert.deepEqual(calls.at(-1).body, { incident: { summary: "Recovered" }, notify_incident_channel: false });
|
|
940
|
+
await invoke("followUps.create", { incident_id: "resource-one", title: "Add retry coverage", assignee_id: "responder" });
|
|
941
|
+
await invoke("followUps.update", { id: "follow-one", title: "Add retry coverage", status: "completed" });
|
|
942
|
+
assert.equal(calls.at(-1).method, "PUT");
|
|
943
|
+
await invoke("followUps.list", { incident_id: "resource-one", incident_mode: "test" });
|
|
944
|
+
assert.equal(calls.at(-1).url.searchParams.get("incident_mode"), "test");
|
|
945
|
+
await invoke("alerts.resolve", { id: "alert-one" });
|
|
946
|
+
assert.equal(calls.at(-1).url.pathname, "/v2/alerts/alert-one/actions/resolve");
|
|
947
|
+
assert.equal(calls.at(-1).method, "POST");
|
|
948
|
+
await invoke("alerts.list", { page_size: 50, after: "next & page" });
|
|
949
|
+
assert.equal(calls.at(-1).url.searchParams.get("after"), "next & page");
|
|
950
|
+
const schedules = await invoke("schedules.list");
|
|
951
|
+
assert.equal(schedules.schedules[0].next_shifts[0].user_id, "responder");
|
|
952
|
+
assert.equal(calls.at(-1).url.searchParams.get("page_size"), "25");
|
|
953
|
+
await invoke("catalog.types");
|
|
954
|
+
await invoke("catalog.list", { catalog_type_id: "services", identifier: "payments", after: "next" });
|
|
955
|
+
const entry = { name: "Payments", external_id: "payments", attribute_values: { owner: { value: { literal: "team-one" } } } };
|
|
956
|
+
await invoke("catalog.create", { catalog_type_id: "services", ...entry });
|
|
957
|
+
assert.deepEqual(calls.at(-1).body.attribute_values, entry.attribute_values);
|
|
958
|
+
await invoke("catalog.update", { id: "catalog-one", ...entry });
|
|
959
|
+
assert.equal(calls.at(-1).method, "PUT");
|
|
960
|
+
for (const operation of ["incidents.get", "followUps.get", "alerts.get", "schedules.get", "catalog.get"]) await invoke(operation, { id: "resource-one" });
|
|
961
|
+
const before = calls.length;
|
|
962
|
+
for (const [operation, input] of [
|
|
963
|
+
["incidents.create", { name: "No explicit visibility", idempotency_key: "one" }],
|
|
964
|
+
["incidents.update", { id: "one", incident: { summary: "No notification choice" } }],
|
|
965
|
+
["followUps.update", { id: "one", title: "task", status: "arbitrary" }],
|
|
966
|
+
["alerts.resolve", { id: "other-user" }], ["catalog.get", { id: "../keys" }],
|
|
967
|
+
["alerts.list", { page_size: 51 }], ["schedules.list", { page_size: 26 }],
|
|
968
|
+
["catalog.create", { catalog_type_id: "services", name: "Payments", attribute_values: { owner: { value: { literal: 42 } } } }]
|
|
969
|
+
]) await assert.rejects(invoke(operation, input));
|
|
970
|
+
assert.equal(calls.length, before);
|
|
971
|
+
rejected = true;
|
|
972
|
+
await assert.rejects(invoke("alerts.resolve", { id: "alert-one" }), error => !JSON.stringify(error).includes("Private fixture detail"));
|
|
973
|
+
assert.equal(calls.length, before + 1);
|
|
974
|
+
for (const operation of ["incidents.create", "followUps.create", "alerts.resolve", "catalog.update"]) assert.equal(incidentIoProvider.operations[operation].validateResult({}), false);
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
test("Notion reads content and data sources then creates and updates a note with bounded writes", async t => {
|
|
978
|
+
const { service, options, input, state, requests } = await fixture(t, cases.find(entry => entry.provider === notionProvider));
|
|
979
|
+
await service.connectApiKey(input);
|
|
980
|
+
const id = "d9824bdc-8445-4327-be8b-5b47500af6ce";
|
|
981
|
+
const invoke = (operation, values = {}) => service.invoke({ ...input, operation, input: values });
|
|
982
|
+
for (const [operation, object, collection] of [["pages.get", "page", "pages"], ["databases.get", "database", "databases"], ["dataSources.get", "data_source", "data_sources"]]) {
|
|
983
|
+
state.response = { object, id, properties: { Name: { type: "title" } } };
|
|
984
|
+
assert.deepEqual(await invoke(operation, { id }), state.response);
|
|
985
|
+
assert.equal(requests.at(-1).url.pathname, `/v1/${collection}/${id}`);
|
|
986
|
+
}
|
|
987
|
+
state.response = { object: "list", results: [{ object: "block", id, has_children: true }], has_more: true, next_cursor: "next+cursor" };
|
|
988
|
+
assert.deepEqual(await invoke("blocks.list", { id, start_cursor: "prior+&cursor", page_size: 2 }), state.response);
|
|
989
|
+
assert.equal(requests.at(-1).url.searchParams.get("start_cursor"), "prior+&cursor");
|
|
990
|
+
const filter = { property: "Done", checkbox: { equals: false } };
|
|
991
|
+
await invoke("dataSources.query", { id, filter });
|
|
992
|
+
assert.equal(requests.at(-1).url.pathname, `/v1/data_sources/${id}/query`);
|
|
993
|
+
assert.deepEqual(JSON.parse(requests.at(-1).init.body), { page_size: 100, filter });
|
|
994
|
+
const properties = { Name: { title: [{ text: { content: "Meeting" } }] } };
|
|
995
|
+
const children = [{ object: "block", type: "paragraph", paragraph: { rich_text: [{ text: { content: "Action" } }] } }];
|
|
996
|
+
state.response = { object: "page", id, properties };
|
|
997
|
+
await invoke("pages.create", { parentId: id, parentType: "data_source_id", properties, children });
|
|
998
|
+
assert.deepEqual(JSON.parse(requests.at(-1).init.body), { parent: { type: "data_source_id", data_source_id: id }, properties, children });
|
|
999
|
+
await invoke("pages.update", { id, properties });
|
|
1000
|
+
assert.equal(requests.at(-1).init.method, "PATCH");
|
|
1001
|
+
state.response = { object: "list", results: children, has_more: false };
|
|
1002
|
+
await invoke("blocks.append", { id, children });
|
|
1003
|
+
assert.deepEqual(JSON.parse(requests.at(-1).init.body), { children });
|
|
1004
|
+
const before = requests.length;
|
|
1005
|
+
for (const [op, values] of [["pages.get", { id: "../users" }], ["blocks.append", { id, children: [] }],
|
|
1006
|
+
["blocks.append", { id, children: Array(101).fill(children[0]) }], ["pages.update", { id, properties: { x: "x".repeat(65536) } }]])
|
|
1007
|
+
await assert.rejects(invoke(op, values), { code: "connector_input_invalid" });
|
|
1008
|
+
const restricted = createConnectionService({ ...options, authorize: async (owner, request) => request.operation === "pages.create" ? null : owner });
|
|
1009
|
+
await assert.rejects(restricted.invoke({ ...input, operation: "pages.create", input: { parentId: id, parentType: "page_id", properties } }), { code: "connector_access_denied" });
|
|
1010
|
+
assert.equal(requests.length, before);
|
|
1011
|
+
for (const status of [403, 404, 429, 500]) { state.status = status; const count = requests.length; await assert.rejects(invoke("blocks.append", { id, children })); assert.equal(requests.length, count + 1); }
|
|
1012
|
+
});
|
|
1013
|
+
|
|
1014
|
+
test("Pipedrive token CRM reads and writes use its configured company and preserve paging", async t => {
|
|
1015
|
+
const entry = cases.find(entry => entry.provider === pipedriveProvider);
|
|
1016
|
+
const { service, options, input, state, requests } = await fixture(t, { ...entry, settings: { companyDomain: "acme" } });
|
|
1017
|
+
await service.connectApiKey(input);
|
|
1018
|
+
const invoke = (operation, values = {}) => service.invoke({ ...input, operation, input: values });
|
|
1019
|
+
for (const collection of ["deals", "persons", "organizations", "activities", "leads", "pipelines"]) {
|
|
1020
|
+
const lead = collection === "leads";
|
|
1021
|
+
const id = lead ? "d9824bdc-8445-4327-be8b-5b47500af6ce" : 12;
|
|
1022
|
+
state.response = { success: true, data: [{ id }], additional_data: { next_cursor: "next+&2" } };
|
|
1023
|
+
assert.deepEqual(await invoke(`${collection}.list`, lead ? { start: 20, limit: 10 } : { cursor: "prior+&1", limit: 10 }), state.response);
|
|
1024
|
+
assert.equal(requests.at(-1).url.origin, "https://acme.pipedrive.com");
|
|
1025
|
+
assert.equal(requests.at(-1).url.pathname, `/api/${lead ? "v1" : "v2"}/${collection}`);
|
|
1026
|
+
assert.equal(requests.at(-1).headers.get("x-api-token"), state.key);
|
|
1027
|
+
state.response = { success: true, data: { id } };
|
|
1028
|
+
await invoke(`${collection}.get`, { id });
|
|
1029
|
+
if (collection === "pipelines") continue;
|
|
1030
|
+
const values = ["deals", "leads"].includes(collection) ? { title: "Follow-up", person_id: 1 }
|
|
1031
|
+
: collection === "activities" ? { subject: "Call", type: "call", done: false } : { name: "Customer" };
|
|
1032
|
+
await invoke(`${collection}.create`, values);
|
|
1033
|
+
assert.deepEqual(JSON.parse(requests.at(-1).init.body), values);
|
|
1034
|
+
await invoke(`${collection}.update`, { id, ...values }); assert.equal(requests.at(-1).init.method, "PATCH");
|
|
1035
|
+
}
|
|
1036
|
+
const count = requests.length;
|
|
1037
|
+
for (const [op, values] of [["deals.get", { id: "../other" }], ["leads.create", { title: "No contact" }], ["deals.update", { id: 1 }], ["persons.create", { name: "X", emails: Array(21).fill({ value: "x@y.test" }) }]])
|
|
1038
|
+
await assert.rejects(invoke(op, values), { code: "connector_input_invalid" });
|
|
1039
|
+
const denied = createConnectionService({ ...options, authorize: async (owner, request) => request.operation === "deals.create" ? null : owner });
|
|
1040
|
+
await assert.rejects(denied.invoke({ ...input, operation: "deals.create", input: { title: "Denied" } }), { code: "connector_access_denied" });
|
|
1041
|
+
assert.equal(requests.length, count);
|
|
1042
|
+
for (const status of [403, 429, 500]) { state.status = status; const before = requests.length; await assert.rejects(invoke("deals.create", { title: "Explicit" })); assert.equal(requests.length, before + 1); }
|
|
1043
|
+
});
|
|
1044
|
+
|
|
1045
|
+
test("Replicate submits, polls and cancels model predictions without losing media or text outputs", async t => {
|
|
1046
|
+
const entry = cases.find(entry => entry.provider === replicateProvider);
|
|
1047
|
+
const { service, options, input, state, requests } = await fixture(t, entry);
|
|
1048
|
+
await service.connectApiKey(input);
|
|
1049
|
+
const invoke = (operation, values = {}) => service.invoke({ ...input, operation, input: values });
|
|
1050
|
+
state.response = { owner: "fixture", name: "model", latest_version: { id: "a".repeat(64), openapi_schema: { components: { schemas: { Input: { properties: { prompt: { type: "string" } } } } } } } };
|
|
1051
|
+
assert.deepEqual(await invoke("models.get", { owner: "fixture", name: "model" }), state.response);
|
|
1052
|
+
assert.equal(requests.at(-1).url.pathname, "/v1/models/fixture/model");
|
|
1053
|
+
state.response = { id: "job1", status: "starting", output: null };
|
|
1054
|
+
const modelInput = { prompt: "A garden", image: "https://files.example.test/input.png", count: 2 };
|
|
1055
|
+
await invoke("predictions.create", { version: "a".repeat(64), input: modelInput });
|
|
1056
|
+
assert.equal(requests.at(-1).url.pathname, "/v1/predictions");
|
|
1057
|
+
assert.deepEqual(JSON.parse(requests.at(-1).init.body), { version: "a".repeat(64), input: modelInput });
|
|
1058
|
+
await invoke("models.predict", { owner: "fixture", name: "model", input: modelInput });
|
|
1059
|
+
assert.equal(requests.at(-1).url.pathname, "/v1/models/fixture/model/predictions");
|
|
1060
|
+
assert.deepEqual(JSON.parse(requests.at(-1).init.body), { input: modelInput });
|
|
1061
|
+
for (const output of ["Generated text", ["https://files.example.test/image.png"], "https://files.example.test/video.mp4", { audio: "https://files.example.test/audio.wav" }]) {
|
|
1062
|
+
state.response = { id: "job1", status: "succeeded", output, metrics: { predict_time: 2 } };
|
|
1063
|
+
assert.deepEqual(await invoke("predictions.get", { id: "job1" }), state.response);
|
|
1064
|
+
assert.equal(requests.at(-1).url.pathname, "/v1/predictions/job1");
|
|
1065
|
+
}
|
|
1066
|
+
for (const status of ["processing", "failed", "canceled"]) {
|
|
1067
|
+
state.response = { id: "job1", status, output: null, error: status === "failed" ? "Model input failed" : null };
|
|
1068
|
+
assert.deepEqual(await invoke(status === "canceled" ? "predictions.cancel" : "predictions.get", { id: "job1" }), state.response);
|
|
1069
|
+
}
|
|
1070
|
+
assert.equal(requests.at(-1).init.method, "POST");
|
|
1071
|
+
assert.equal(requests.at(-1).url.pathname, "/v1/predictions/job1/cancel");
|
|
1072
|
+
const count = requests.length;
|
|
1073
|
+
for (const [op, values] of [["predictions.get", { id: "../other" }], ["models.predict", { owner: "fixture", name: "model", input: { prompt: "x".repeat(262145) } }], ["predictions.create", { version: "unversioned", input: {} }]])
|
|
1074
|
+
await assert.rejects(invoke(op, values), { code: "connector_input_invalid" });
|
|
1075
|
+
const denied = createConnectionService({ ...options, authorize: async (owner, request) => request.operation === "predictions.cancel" ? null : owner });
|
|
1076
|
+
await assert.rejects(denied.invoke({ ...input, operation: "predictions.cancel", input: { id: "job1" } }), { code: "connector_access_denied" });
|
|
1077
|
+
assert.equal(requests.length, count);
|
|
1078
|
+
for (const status of [403, 429, 500]) { state.status = status; const before = requests.length; await assert.rejects(invoke("models.predict", { owner: "fixture", name: "model", input: {} })); assert.equal(requests.length, before + 1); }
|
|
1079
|
+
});
|