diff --git a/apps/sim/app/api/credential-groups/slack-managed-users/callback/route.test.ts b/apps/sim/app/api/credential-groups/slack-managed-users/callback/route.test.ts new file mode 100644 index 00000000000..0f616e3c4a7 --- /dev/null +++ b/apps/sim/app/api/credential-groups/slack-managed-users/callback/route.test.ts @@ -0,0 +1,20 @@ +import { authMockFns } from '@sim/testing' +import { NextRequest } from 'next/server' +import { describe, expect, it } from 'vitest' +import { GET } from '@/app/api/credential-groups/slack-managed-users/callback/route' + +describe('GET /api/credential-groups/slack-managed-users/callback', () => { + it('sends a signed-out browser to the sign-in recovery screen', async () => { + authMockFns.mockGetSession.mockResolvedValueOnce(null) + const response = await GET( + new NextRequest( + 'http://localhost/api/credential-groups/slack-managed-users/callback?state=s1&code=c1' + ), + {} + ) + expect(response.status).toBe(303) + const location = new URL(response.headers.get('location') ?? '') + expect(location.pathname).toBe('/credential-groups/slack-complete') + expect(location.searchParams.get('reason')).toBe('signin_required') + }) +}) diff --git a/apps/sim/app/api/credential-groups/slack-managed-users/callback/route.ts b/apps/sim/app/api/credential-groups/slack-managed-users/callback/route.ts index 06bc2743e34..3299729d20b 100644 --- a/apps/sim/app/api/credential-groups/slack-managed-users/callback/route.ts +++ b/apps/sim/app/api/credential-groups/slack-managed-users/callback/route.ts @@ -41,7 +41,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => { ok: false, message: 'Sign in to Sim to complete this Slack setup.', state: rawState, - reason: 'unauthenticated', + reason: 'signin_required', }) } const parsed = await parseRequest(slackCredentialGroupConfigurationCallbackContract, request, {}) diff --git a/apps/sim/app/api/desktop/source-connect/route.test.ts b/apps/sim/app/api/desktop/source-connect/route.test.ts new file mode 100644 index 00000000000..27459135c54 --- /dev/null +++ b/apps/sim/app/api/desktop/source-connect/route.test.ts @@ -0,0 +1,27 @@ +import { authMockFns } from '@sim/testing' +import { NextRequest } from 'next/server' +import { beforeEach, describe, expect, it } from 'vitest' +import { POST } from '@/app/api/desktop/source-connect/route' + +describe('POST /api/desktop/source-connect', () => { + beforeEach(() => { + authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'u1' }, session: { id: 's1' } }) + }) + + it('rejects a body far beyond the ticket limit at the parser', async () => { + const body = JSON.stringify({ + requestId: 'a'.repeat(32), + request: { kind: 'reconnect-account', credentialId: 'c'.repeat(128) }, + padding: 'x'.repeat(256 * 1024), + }) + const response = await POST( + new NextRequest('http://localhost/api/desktop/source-connect', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body, + }), + {} + ) + expect(response.status).toBe(413) + }) +}) diff --git a/apps/sim/app/api/desktop/source-connect/route.ts b/apps/sim/app/api/desktop/source-connect/route.ts index 518b0f4e88f..e44102784b3 100644 --- a/apps/sim/app/api/desktop/source-connect/route.ts +++ b/apps/sim/app/api/desktop/source-connect/route.ts @@ -13,6 +13,7 @@ export const POST = defineInternalJsonRoute({ operation: createDesktopSourceRequest.operation, rateLimit: internalRateLimits.user({ bucketName: 'desktop-source-connect' }), errorPolicy: internalOrchestrationErrorPolicy, + parseOptions: { maxBodyBytes: 64 * 1024 }, mapInput: ({ body }) => ({ requestId: body.requestId, payload: JSON.stringify(body.request) }), useCase: createDesktopSourceRequest, staticResponseHeaders: { 'Cache-Control': 'no-store' }, diff --git a/apps/sim/hooks/queries/slack-search.test.tsx b/apps/sim/hooks/queries/slack-search.test.tsx new file mode 100644 index 00000000000..3fe55eeab84 --- /dev/null +++ b/apps/sim/hooks/queries/slack-search.test.tsx @@ -0,0 +1,72 @@ +/** @vitest-environment jsdom */ + +import { act } from 'react' +import { + apiClientRequestMock, + apiClientRequestMockFns, +} from '@sim/testing/mocks/api-client-request.mock' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +vi.mock('@/lib/api/client/request', () => apiClientRequestMock) + +import { useStartSlackSearchOAuth } from '@/hooks/queries/slack-search' + +const mockRequestJson = apiClientRequestMockFns.mockRequestJson + +describe('useStartSlackSearchOAuth', () => { + let root: Root + let client: QueryClient + let result: ReturnType + + function Probe() { + result = useStartSlackSearchOAuth() + return null + } + + beforeEach(async () => { + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true) + mockRequestJson.mockReset() + client = new QueryClient() + root = createRoot(document.createElement('div')) + await act(async () => + root.render( + + + + ) + ) + }) + + afterEach(async () => { + await act(async () => root.unmount()) + client.clear() + }) + + it('cancels an in-flight browser OAuth start when its signal aborts', async () => { + mockRequestJson.mockImplementation( + (_contract: unknown, input: { signal?: AbortSignal }) => + new Promise((_resolve, reject) => + input.signal?.addEventListener('abort', () => reject(input.signal?.reason)) + ) + ) + const controller = new AbortController() + let outcome: 'pending' | 'rejected' = 'pending' + await act(async () => { + void result + .mutateAsync({ + organizationId: 'org-1', + name: 'Sim Search', + description: 'Search Slack', + mode: 'shared', + signal: controller.signal, + }) + .catch(() => { + outcome = 'rejected' + }) + }) + await act(async () => controller.abort()) + expect(outcome).toBe('rejected') + }) +}) diff --git a/apps/sim/hooks/queries/slack-search.ts b/apps/sim/hooks/queries/slack-search.ts index 030c529a924..39a02552e3c 100644 --- a/apps/sim/hooks/queries/slack-search.ts +++ b/apps/sim/hooks/queries/slack-search.ts @@ -48,7 +48,7 @@ export function useStartSlackSearchOAuth() { await connectDesktopSource({ kind: 'slack-search', body }, signal) return null } - return requestJson(startSlackSearchOAuthContract, { body }) + return requestJson(startSlackSearchOAuthContract, { body, signal }) }, onSettled: (_data, _error, input) => Promise.all([