11import { resolve } from "node:path" ;
22import { beforeEach , describe , expect , it , vi } from "vitest" ;
3+ import { InvalidInputError } from "../../src/core/errors.js" ;
34import * as api from "../../src/core/resources/connector/api.js" ;
45import { readAllConnectors } from "../../src/core/resources/connector/config.js" ;
56import {
@@ -40,10 +41,21 @@ describe("IntegrationTypeSchema", () => {
4041 }
4142 } ) ;
4243
43- it ( "rejects invalid integration types" , ( ) => {
44- const invalidTypes = [ "invalid" , "google" , "facebook" , "twitter" , "" ] ;
44+ it ( "accepts arbitrary integration types (including custom providers) " , ( ) => {
45+ const arbitraryTypes = [ "invalid" , "google" , "facebook" , "twitter" , "custom-oauth-provider " ] ;
4546
46- for ( const type of invalidTypes ) {
47+ for ( const type of arbitraryTypes ) {
48+ expect ( IntegrationTypeSchema . safeParse ( type ) . success ) . toBe ( true ) ;
49+ }
50+ } ) ;
51+
52+ it ( "rejects empty strings" , ( ) => {
53+ expect ( IntegrationTypeSchema . safeParse ( "" ) . success ) . toBe ( false ) ;
54+ } ) ;
55+
56+ it ( "rejects path traversal strings" , ( ) => {
57+ const malicious = [ "../admin" , "../../endpoint" , "type/with/slashes" , "type with spaces" ] ;
58+ for ( const type of malicious ) {
4759 expect ( IntegrationTypeSchema . safeParse ( type ) . success ) . toBe ( false ) ;
4860 }
4961 } ) ;
@@ -92,14 +104,18 @@ describe("ConnectorResourceSchema", () => {
92104 }
93105 } ) ;
94106
95- it ( "rejects connector with invalid type" , ( ) => {
107+ it ( "accepts connector with arbitrary provider type" , ( ) => {
96108 const connector = {
97- type : "invalid " ,
98- scopes : [ ] ,
109+ type : "custom-oauth-provider " ,
110+ scopes : [ "scope1" , "scope2" ] ,
99111 } ;
100112
101113 const result = ConnectorResourceSchema . safeParse ( connector ) ;
102- expect ( result . success ) . toBe ( false ) ;
114+ expect ( result . success ) . toBe ( true ) ;
115+ if ( result . success ) {
116+ expect ( result . data . type ) . toBe ( "custom-oauth-provider" ) ;
117+ expect ( result . data . scopes ) . toEqual ( [ "scope1" , "scope2" ] ) ;
118+ }
103119 } ) ;
104120
105121 it ( "rejects connector without type" , ( ) => {
@@ -144,6 +160,17 @@ describe("readAllConnectors", () => {
144160 "Invalid connector file"
145161 ) ;
146162 } ) ;
163+
164+ it ( "throws InvalidInputError for duplicate connector types" , async ( ) => {
165+ const connectorsDir = resolve ( FIXTURES_DIR , "duplicate-connectors/connectors" ) ;
166+
167+ await expect ( readAllConnectors ( connectorsDir ) ) . rejects . toThrow (
168+ InvalidInputError
169+ ) ;
170+ await expect ( readAllConnectors ( connectorsDir ) ) . rejects . toThrow (
171+ 'Duplicate connector type "slack"'
172+ ) ;
173+ } ) ;
147174} ) ;
148175
149176const mockListConnectors = vi . mocked ( api . listConnectors ) ;
@@ -183,7 +210,7 @@ describe("pushConnectors", () => {
183210 it ( "removes upstream-only connectors" , async ( ) => {
184211 mockListConnectors . mockResolvedValue ( {
185212 integrations : [
186- { integration_type : "slack" , status : "ACTIVE " , scopes : [ "chat:write" ] } ,
213+ { integration_type : "slack" , status : "active " , scopes : [ "chat:write" ] } ,
187214 ] ,
188215 } ) ;
189216 mockRemoveConnector . mockResolvedValue ( {
@@ -203,7 +230,7 @@ describe("pushConnectors", () => {
203230 ] ;
204231 mockListConnectors . mockResolvedValue ( {
205232 integrations : [
206- { integration_type : "slack" , status : "ACTIVE " , scopes : [ "chat:write" ] } ,
233+ { integration_type : "slack" , status : "active " , scopes : [ "chat:write" ] } ,
207234 ] ,
208235 } ) ;
209236 mockSetConnector . mockResolvedValue ( {
@@ -236,7 +263,7 @@ describe("pushConnectors", () => {
236263 integrations : [
237264 {
238265 integration_type : "gmail" ,
239- status : "ACTIVE " ,
266+ status : "active " ,
240267 scopes : [ "https://mail.google.com/" ] ,
241268 } ,
242269 ] ,
@@ -299,6 +326,30 @@ describe("pushConnectors", () => {
299326 ] ) ;
300327 } ) ;
301328
329+ it ( "returns fallback message when different_user has no error_message or email" , async ( ) => {
330+ const local : ConnectorResource [ ] = [
331+ { type : "gmail" , scopes : [ "https://mail.google.com/" ] } ,
332+ ] ;
333+ mockSetConnector . mockResolvedValue ( {
334+ redirect_url : null ,
335+ connection_id : null ,
336+ already_authorized : false ,
337+ error : "different_user" ,
338+ error_message : null ,
339+ other_user_email : null ,
340+ } ) ;
341+
342+ const result = await pushConnectors ( local ) ;
343+
344+ expect ( result . results ) . toEqual ( [
345+ {
346+ type : "gmail" ,
347+ action : "error" ,
348+ error : "Already connected by another user" ,
349+ } ,
350+ ] ) ;
351+ } ) ;
352+
302353 it ( "handles sync errors gracefully" , async ( ) => {
303354 const local : ConnectorResource [ ] = [
304355 { type : "gmail" , scopes : [ "https://mail.google.com/" ] } ,
@@ -315,7 +366,7 @@ describe("pushConnectors", () => {
315366 it ( "handles remove errors gracefully" , async ( ) => {
316367 mockListConnectors . mockResolvedValue ( {
317368 integrations : [
318- { integration_type : "slack" , status : "ACTIVE " , scopes : [ "chat:write" ] } ,
369+ { integration_type : "slack" , status : "active " , scopes : [ "chat:write" ] } ,
319370 ] ,
320371 } ) ;
321372 mockRemoveConnector . mockRejectedValue ( new Error ( "Remove failed" ) ) ;
0 commit comments