From 5cf7e8d546545e57d9c51b9e020bf7987f893033 Mon Sep 17 00:00:00 2001 From: Waleed Date: Sun, 19 Apr 2026 23:05:06 -0700 Subject: [PATCH] improvement(codebase): migrate tests to dbChainMock, extract react-query hooks (#4235) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * improvement(codebase): migrate tests to dbChainMock, extract react-query hooks Migrate 97 test files to centralized dbChainMock/dbChainMockFns helpers from @sim/testing — removes hoisted chain-wiring boilerplate. Extend dbChainMock to cover insert/update/delete/transaction/execute patterns. Extract useGitHubStars and useVoiceSettings react-query hooks from inline fetches. Centralize additional mocks (authMockFns, hybridAuthMockFns) and update docs. * fix(github-stars): centralize fallback via initialData, remove stale constants Move the placeholder star count into useGitHubStars as initialData with initialDataUpdatedAt: 0 so `data` is always a narrowed string while still refetching on mount. Fixes two Bugbot issues: stale '25.8k' in chat.tsx (vs '27.8k' in navbar) and empty-string return in fetchGitHubStars that bypassed `??` fallbacks in consumers. Co-Authored-By: Claude Opus 4.7 * fix(testing): wire dbChainMock.db to shared transaction and execute fns dbChainMock.db.transaction was an inline vi.fn() separate from the exported dbChainMockFns.transaction, so dbChainMockFns.transaction.mockResolvedValueOnce and assertions silently targeted the wrong instance. dbChainMock.db also omitted execute, so tests for any module that calls db.execute (logging-session, table service, billing balance) would throw TypeError. Both mocks now reference the module-level constants so overrides and resetDbChainMock affect the same fn. Co-Authored-By: Claude Opus 4.7 * fix(chat,testing): memoize welcome message and add selectDistinct to dbChainMock.db Why: - Welcome ChatMessage was rebuilt inline each render, producing a fresh timestamp and new array identity — cascading to ChatMessageContainer and VoiceInterface props on every tick. - dbChainMockFns exports selectDistinct/selectDistinctOn but the dbChainMock.db object omitted them, so tests that stub those builders hit undefined on the mocked module. Co-Authored-By: Claude Opus 4.7 * fix(chat): re-attach scroll listener once container mounts The scroll effect's empty dep array meant it ran only on the first render, when `chatConfig` is still loading and the component returns `` — so `messagesContainerRef.current` was null and the listener was never attached. Depend on the gating conditions that control which tree renders, so the effect re-runs once the real container is in the DOM (and re-attaches when toggling in/out of voice mode). Co-Authored-By: Claude Opus 4.7 * fix(chat): reset chat state on identifier change via key prop Keying `` on `identifier` guarantees a full remount on route transitions between chats, so `conversationId`, `messages`, and every other piece of local state start fresh — no reset effect required. Co-Authored-By: Claude Opus 4.7 --------- Co-authored-by: Claude Opus 4.7 --- .claude/rules/sim-testing.md | 42 ++- .cursor/rules/sim-testing.mdc | 4 + apps/sim/app/(landing)/actions/github.ts | 26 -- .../navbar/components/github-stars.tsx | 18 +- .../api/auth/oauth/connections/route.test.ts | 86 ++---- .../api/auth/oauth/credentials/route.test.ts | 36 +-- .../api/auth/oauth/disconnect/route.test.ts | 78 +----- .../app/api/auth/oauth/token/route.test.ts | 30 +-- .../api/chat/[identifier]/otp/route.test.ts | 13 +- .../app/api/chat/[identifier]/otp/route.ts | 16 +- .../app/api/chat/[identifier]/route.test.ts | 40 ++- apps/sim/app/api/chat/[identifier]/route.ts | 52 ++-- .../app/api/chat/manage/[id]/route.test.ts | 54 +--- apps/sim/app/api/chat/route.test.ts | 57 ++-- apps/sim/app/api/chat/utils.test.ts | 3 - .../app/api/copilot/api-keys/route.test.ts | 4 +- .../app/api/copilot/chat/delete/route.test.ts | 33 +-- .../app/api/copilot/chat/stop/route.test.ts | 6 +- .../chat/update-messages/route.test.ts | 6 +- apps/sim/app/api/copilot/chats/route.test.ts | 4 +- .../copilot/checkpoints/revert/route.test.ts | 12 +- .../app/api/copilot/checkpoints/route.test.ts | 12 +- .../app/api/copilot/feedback/route.test.ts | 61 ++--- apps/sim/app/api/files/delete/route.test.ts | 8 +- apps/sim/app/api/files/parse/route.test.ts | 25 +- .../sim/app/api/files/presigned/route.test.ts | 4 +- .../api/files/serve/[...path]/route.test.ts | 4 +- apps/sim/app/api/files/upload/route.test.ts | 16 +- apps/sim/app/api/folders/[id]/route.test.ts | 2 - apps/sim/app/api/folders/route.test.ts | 2 - .../app/api/function/execute/route.test.ts | 3 - apps/sim/app/api/jobs/[jobId]/route.test.ts | 12 +- .../[connectorId]/documents/route.test.ts | 9 +- .../connectors/[connectorId]/route.test.ts | 6 - .../[connectorId]/sync/route.test.ts | 9 +- .../[id]/documents/[documentId]/route.test.ts | 13 +- .../knowledge/[id]/documents/route.test.ts | 13 +- apps/sim/app/api/knowledge/[id]/route.test.ts | 13 +- apps/sim/app/api/knowledge/route.test.ts | 18 +- .../app/api/knowledge/search/route.test.ts | 9 - apps/sim/app/api/knowledge/utils.test.ts | 17 +- apps/sim/app/api/mcp/events/route.test.ts | 10 +- .../api/mcp/serve/[serverId]/route.test.ts | 46 +--- apps/sim/app/api/schedules/[id]/route.test.ts | 9 - .../app/api/schedules/execute/route.test.ts | 78 +++--- apps/sim/app/api/schedules/route.test.ts | 16 +- .../table/[tableId]/import-csv/route.test.ts | 6 +- .../api/tools/crowdstrike/query/route.test.ts | 4 +- apps/sim/app/api/tools/custom/route.test.ts | 12 - .../api/webhooks/trigger/[path]/route.test.ts | 3 - .../workflows/[id]/chat/status/route.test.ts | 31 +-- .../[id]/execute/route.async.test.ts | 22 +- .../[executionId]/cancel/route.test.ts | 3 - .../workflows/[id]/form/status/route.test.ts | 31 +-- apps/sim/app/api/workflows/[id]/route.test.ts | 9 - .../workflows/[id]/variables/route.test.ts | 6 - apps/sim/app/api/workflows/route.test.ts | 6 - .../[id]/docx/preview/route.test.ts | 9 +- .../workspaces/[id]/pdf/preview/route.test.ts | 9 +- .../[id]/pptx/preview/route.test.ts | 9 +- .../invitations/[invitationId]/route.test.ts | 6 - .../api/workspaces/invitations/route.test.ts | 13 +- apps/sim/app/chat/[identifier]/chat.tsx | 250 ++++++------------ apps/sim/app/chat/[identifier]/page.tsx | 2 +- .../chat/components/auth/email/email-auth.tsx | 118 ++------- .../auth/password/password-auth.tsx | 48 +--- .../async-preprocessing-correlation.test.ts | 57 ++-- .../utils/permission-check.test.ts | 2 - .../handlers/agent/agent-handler.test.ts | 4 +- .../condition/condition-handler.test.ts | 3 - apps/sim/hooks/queries/chats.ts | 174 +++++++++++- apps/sim/hooks/queries/github-stars.ts | 46 ++++ apps/sim/hooks/queries/voice-settings.ts | 34 +++ apps/sim/lib/audit/log.test.ts | 75 +++--- .../lib/billing/credits/daily-refresh.test.ts | 3 - .../sim/lib/billing/webhooks/invoices.test.ts | 4 +- .../lib/copilot/async-runs/repository.test.ts | 45 +--- apps/sim/lib/copilot/chat/post.test.ts | 6 - .../copilot/request/handlers/handlers.test.ts | 29 +- .../validation/selector-validator.test.ts | 52 +--- apps/sim/lib/execution/preprocessing.test.ts | 3 +- .../preprocessing.webhook-correlation.test.ts | 3 +- .../knowledge/connectors/sync-engine.test.ts | 3 +- apps/sim/lib/logs/execution/logger.test.ts | 4 +- .../logs/execution/logging-session.test.ts | 3 - .../logs/execution/snapshot/service.test.ts | 3 +- .../lib/messaging/email/unsubscribe.test.ts | 4 +- .../lib/table/__tests__/update-row.test.ts | 30 +-- apps/sim/lib/webhooks/processor.test.ts | 3 - .../lib/webhooks/providers/whatsapp.test.ts | 3 - apps/sim/lib/workflows/lifecycle.test.ts | 3 - .../workflows/persistence/duplicate.test.ts | 3 - apps/sim/lib/workflows/utils.test.ts | 3 - apps/sim/lib/workspaces/lifecycle.test.ts | 4 +- .../lib/workspaces/permissions/utils.test.ts | 6 +- .../sim/socket/middleware/permissions.test.ts | 6 +- apps/sim/vitest.setup.ts | 8 + packages/testing/src/mocks/database.mock.ts | 143 ++++++++++ .../testing/src/mocks/hybrid-auth.mock.ts | 27 +- packages/testing/src/mocks/index.ts | 3 + 100 files changed, 976 insertions(+), 1455 deletions(-) delete mode 100644 apps/sim/app/(landing)/actions/github.ts create mode 100644 apps/sim/hooks/queries/github-stars.ts create mode 100644 apps/sim/hooks/queries/voice-settings.ts diff --git a/.claude/rules/sim-testing.md b/.claude/rules/sim-testing.md index 68a7b8203a..36b19dc0d6 100644 --- a/.claude/rules/sim-testing.md +++ b/.claude/rules/sim-testing.md @@ -13,8 +13,12 @@ Use Vitest. Test files: `feature.ts` → `feature.test.ts` These modules are mocked globally — do NOT re-mock them in test files unless you need to override behavior: - `@sim/db` → `databaseMock` +- `@sim/db/schema` → `schemaMock` - `drizzle-orm` → `drizzleOrmMock` - `@sim/logger` → `loggerMock` +- `@/lib/auth` → `authMock` +- `@/lib/auth/hybrid` → `hybridAuthMock` (with default session-delegating behavior) +- `@/lib/core/utils/request` → `requestUtilsMock` - `@/stores/console/store`, `@/stores/terminal`, `@/stores/execution/store` - `@/blocks/registry` - `@trigger.dev/sdk` @@ -192,24 +196,38 @@ hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({ ### Database chain mocking -```typescript -const { mockSelect, mockFrom, mockWhere } = vi.hoisted(() => ({ - mockSelect: vi.fn(), - mockFrom: vi.fn(), - mockWhere: vi.fn(), -})) +Use the centralized `dbChainMock` + `dbChainMockFns` helpers — no `vi.hoisted()` or chain-wiring boilerplate needed. -vi.mock('@sim/db', () => ({ - db: { select: mockSelect }, -})) +```typescript +import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing' + +vi.mock('@sim/db', () => dbChainMock) +// Spread for custom exports: vi.mock('@sim/db', () => ({ ...dbChainMock, myTable: {...} })) beforeEach(() => { - mockSelect.mockReturnValue({ from: mockFrom }) - mockFrom.mockReturnValue({ where: mockWhere }) - mockWhere.mockResolvedValue([{ id: '1', name: 'test' }]) + vi.clearAllMocks() + resetDbChainMock() // only needed if tests use permanent (non-`Once`) overrides +}) + +it('reads a row', async () => { + dbChainMockFns.limit.mockResolvedValueOnce([{ id: '1', name: 'test' }]) + // exercise code that hits db.select().from().where().limit() + expect(dbChainMockFns.where).toHaveBeenCalled() }) ``` +**Default chains supported:** +- `select()/selectDistinct()/selectDistinctOn() → from() → where()/innerJoin()/leftJoin() → where() → limit()/orderBy()/returning()/groupBy()` +- `insert() → values() → returning()/onConflictDoUpdate()/onConflictDoNothing()` +- `update() → set() → where() → limit()/orderBy()/returning()` +- `delete() → where() → limit()/orderBy()/returning()` +- `db.execute()` resolves `[]` +- `db.transaction(cb)` calls cb with `dbChainMock.db` + +All terminals default to `Promise.resolve([])`. Override per-test with `dbChainMockFns..mockResolvedValueOnce(...)`. + +Use `resetDbChainMock()` in `beforeEach` only when tests replace wiring with `.mockReturnValue` / `.mockResolvedValue` (permanent). Tests using only `...Once` variants don't need it. + ## @sim/testing Package Always prefer over local test data. diff --git a/.cursor/rules/sim-testing.mdc b/.cursor/rules/sim-testing.mdc index 41b66b3693..7e3e480664 100644 --- a/.cursor/rules/sim-testing.mdc +++ b/.cursor/rules/sim-testing.mdc @@ -13,8 +13,12 @@ Use Vitest. Test files: `feature.ts` → `feature.test.ts` These modules are mocked globally — do NOT re-mock them in test files unless you need to override behavior: - `@sim/db` → `databaseMock` +- `@sim/db/schema` → `schemaMock` - `drizzle-orm` → `drizzleOrmMock` - `@sim/logger` → `loggerMock` +- `@/lib/auth` → `authMock` +- `@/lib/auth/hybrid` → `hybridAuthMock` (with default session-delegating behavior) +- `@/lib/core/utils/request` → `requestUtilsMock` - `@/stores/console/store`, `@/stores/terminal`, `@/stores/execution/store` - `@/blocks/registry` - `@trigger.dev/sdk` diff --git a/apps/sim/app/(landing)/actions/github.ts b/apps/sim/app/(landing)/actions/github.ts deleted file mode 100644 index c5785f0e9c..0000000000 --- a/apps/sim/app/(landing)/actions/github.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { createLogger } from '@sim/logger' - -const DEFAULT_STARS = '19.4k' - -const logger = createLogger('GitHubStars') - -export async function getFormattedGitHubStars(): Promise { - try { - const response = await fetch('/api/stars', { - headers: { - 'Cache-Control': 'max-age=3600', // Cache for 1 hour - }, - }) - - if (!response.ok) { - logger.warn('Failed to fetch GitHub stars from API') - return DEFAULT_STARS - } - - const data = await response.json() - return data.stars || DEFAULT_STARS - } catch (error) { - logger.warn('Error fetching GitHub stars:', error) - return DEFAULT_STARS - } -} diff --git a/apps/sim/app/(landing)/components/navbar/components/github-stars.tsx b/apps/sim/app/(landing)/components/navbar/components/github-stars.tsx index a8aafdc45a..63bf503f56 100644 --- a/apps/sim/app/(landing)/components/navbar/components/github-stars.tsx +++ b/apps/sim/app/(landing)/components/navbar/components/github-stars.tsx @@ -1,13 +1,7 @@ 'use client' -import { useEffect, useState } from 'react' -import { createLogger } from '@sim/logger' import { GithubOutlineIcon } from '@/components/icons' -import { getFormattedGitHubStars } from '@/app/(landing)/actions/github' - -const logger = createLogger('github-stars') - -const INITIAL_STARS = '27.7k' +import { useGitHubStars } from '@/hooks/queries/github-stars' /** * Client component that displays GitHub stars count. @@ -16,15 +10,7 @@ const INITIAL_STARS = '27.7k' * a Server Component for optimal SEO/GEO crawlability. */ export function GitHubStars() { - const [stars, setStars] = useState(INITIAL_STARS) - - useEffect(() => { - getFormattedGitHubStars() - .then(setStars) - .catch((error) => { - logger.warn('Failed to fetch GitHub stars', error) - }) - }, []) + const { data: stars } = useGitHubStars() return ( { - const db = { - select: vi.fn().mockReturnThis(), - from: vi.fn().mockReturnThis(), - where: vi.fn().mockReturnThis(), - limit: vi.fn(), - } - const logger = { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - debug: vi.fn(), - trace: vi.fn(), - fatal: vi.fn(), - child: vi.fn(), - } - return { - mockDb: db, - mockLogger: logger, - mockParseProvider: vi.fn(), - mockJwtDecode: vi.fn(), - mockEq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })), - } -}) - -vi.mock('@/lib/auth', () => authMock) +const { mockParseProvider, mockJwtDecode, mockEq } = vi.hoisted(() => ({ + mockParseProvider: vi.fn(), + mockJwtDecode: vi.fn(), + mockEq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })), +})) vi.mock('@sim/db', () => ({ - db: mockDb, + ...dbChainMock, account: { userId: 'userId', providerId: 'providerId' }, user: { email: 'email', id: 'id' }, eq: mockEq, @@ -48,10 +33,6 @@ vi.mock('jwt-decode', () => ({ jwtDecode: mockJwtDecode, })) -vi.mock('@sim/logger', () => ({ - createLogger: vi.fn().mockReturnValue(mockLogger), -})) - vi.mock('@/lib/oauth/utils', () => ({ parseProvider: mockParseProvider, })) @@ -61,10 +42,7 @@ import { GET } from '@/app/api/auth/oauth/connections/route' describe('OAuth Connections API Route', () => { beforeEach(() => { vi.clearAllMocks() - - mockDb.select.mockReturnThis() - mockDb.from.mockReturnThis() - mockDb.where.mockReturnThis() + resetDbChainMock() mockParseProvider.mockImplementation((providerId: string) => ({ baseProvider: providerId.split('-')[0] || providerId, @@ -98,14 +76,8 @@ describe('OAuth Connections API Route', () => { const mockUserRecord = [{ email: 'user@example.com' }] - mockDb.select.mockReturnValueOnce(mockDb) - mockDb.from.mockReturnValueOnce(mockDb) - mockDb.where.mockResolvedValueOnce(mockAccounts) - - mockDb.select.mockReturnValueOnce(mockDb) - mockDb.from.mockReturnValueOnce(mockDb) - mockDb.where.mockReturnValueOnce(mockDb) - mockDb.limit.mockResolvedValueOnce(mockUserRecord) + dbChainMockFns.where.mockResolvedValueOnce(mockAccounts) + dbChainMockFns.limit.mockResolvedValueOnce(mockUserRecord) const req = createMockRequest('GET') @@ -138,7 +110,6 @@ describe('OAuth Connections API Route', () => { expect(response.status).toBe(401) expect(data.error).toBe('User not authenticated') - expect(mockLogger.warn).toHaveBeenCalled() }) it('should handle user with no connections', async () => { @@ -146,14 +117,8 @@ describe('OAuth Connections API Route', () => { user: { id: 'user-123' }, }) - mockDb.select.mockReturnValueOnce(mockDb) - mockDb.from.mockReturnValueOnce(mockDb) - mockDb.where.mockResolvedValueOnce([]) - - mockDb.select.mockReturnValueOnce(mockDb) - mockDb.from.mockReturnValueOnce(mockDb) - mockDb.where.mockReturnValueOnce(mockDb) - mockDb.limit.mockResolvedValueOnce([]) + dbChainMockFns.where.mockResolvedValueOnce([]) + dbChainMockFns.limit.mockResolvedValueOnce([]) const req = createMockRequest('GET') @@ -169,9 +134,7 @@ describe('OAuth Connections API Route', () => { user: { id: 'user-123' }, }) - mockDb.select.mockReturnValueOnce(mockDb) - mockDb.from.mockReturnValueOnce(mockDb) - mockDb.where.mockRejectedValueOnce(new Error('Database error')) + dbChainMockFns.where.mockRejectedValueOnce(new Error('Database error')) const req = createMockRequest('GET') @@ -180,7 +143,6 @@ describe('OAuth Connections API Route', () => { expect(response.status).toBe(500) expect(data.error).toBe('Internal server error') - expect(mockLogger.error).toHaveBeenCalled() }) it('should decode ID token for display name', async () => { @@ -204,14 +166,8 @@ describe('OAuth Connections API Route', () => { name: 'Decoded User', }) - mockDb.select.mockReturnValueOnce(mockDb) - mockDb.from.mockReturnValueOnce(mockDb) - mockDb.where.mockResolvedValueOnce(mockAccounts) - - mockDb.select.mockReturnValueOnce(mockDb) - mockDb.from.mockReturnValueOnce(mockDb) - mockDb.where.mockReturnValueOnce(mockDb) - mockDb.limit.mockResolvedValueOnce([]) + dbChainMockFns.where.mockResolvedValueOnce(mockAccounts) + dbChainMockFns.limit.mockResolvedValueOnce([]) const req = createMockRequest('GET') diff --git a/apps/sim/app/api/auth/oauth/credentials/route.test.ts b/apps/sim/app/api/auth/oauth/credentials/route.test.ts index 7c21745e54..fd87f08f5e 100644 --- a/apps/sim/app/api/auth/oauth/credentials/route.test.ts +++ b/apps/sim/app/api/auth/oauth/credentials/route.test.ts @@ -4,36 +4,10 @@ * @vitest-environment node */ -import { - hybridAuthMock, - hybridAuthMockFns, - permissionsMock, - requestUtilsMock, - schemaMock, - workflowsUtilsMock, -} from '@sim/testing' +import { hybridAuthMockFns, permissionsMock, workflowsUtilsMock } from '@sim/testing' import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockLogger } = vi.hoisted(() => { - const logger = { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - debug: vi.fn(), - trace: vi.fn(), - fatal: vi.fn(), - child: vi.fn(), - } - return { - mockLogger: logger, - } -}) - -vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) - -vi.mock('@/lib/core/utils/request', () => requestUtilsMock) - vi.mock('@/lib/credentials/oauth', () => ({ syncWorkspaceOAuthCredentialsForUser: vi.fn(), })) @@ -42,12 +16,6 @@ vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock) -vi.mock('@sim/db/schema', () => schemaMock) - -vi.mock('@sim/logger', () => ({ - createLogger: vi.fn().mockReturnValue(mockLogger), -})) - import { GET } from '@/app/api/auth/oauth/credentials/route' describe('OAuth Credentials API Route', () => { @@ -73,7 +41,6 @@ describe('OAuth Credentials API Route', () => { expect(response.status).toBe(401) expect(data.error).toBe('User not authenticated') - expect(mockLogger.warn).toHaveBeenCalled() }) it('should handle missing provider parameter', async () => { @@ -90,7 +57,6 @@ describe('OAuth Credentials API Route', () => { expect(response.status).toBe(400) expect(data.error).toBe('Provider or credentialId is required') - expect(mockLogger.warn).toHaveBeenCalled() }) it('should handle no credentials found', async () => { diff --git a/apps/sim/app/api/auth/oauth/disconnect/route.test.ts b/apps/sim/app/api/auth/oauth/disconnect/route.test.ts index 0f0ad680e4..13a3b4bc7e 100644 --- a/apps/sim/app/api/auth/oauth/disconnect/route.test.ts +++ b/apps/sim/app/api/auth/oauth/disconnect/route.test.ts @@ -5,64 +5,19 @@ */ import { auditMock, - authMock, authMockFns, createMockRequest, - requestUtilsMock, - schemaMock, + dbChainMock, + dbChainMockFns, + resetDbChainMock, } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockDb, mockSelectChain, mockLogger, mockSyncAllWebhooksForCredentialSet } = vi.hoisted( - () => { - const selectChain = { - from: vi.fn().mockReturnThis(), - innerJoin: vi.fn().mockReturnThis(), - where: vi.fn().mockResolvedValue([]), - } - const db = { - delete: vi.fn().mockReturnThis(), - where: vi.fn(), - select: vi.fn().mockReturnValue(selectChain), - } - const logger = { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - debug: vi.fn(), - trace: vi.fn(), - fatal: vi.fn(), - child: vi.fn(), - } - return { - mockDb: db, - mockSelectChain: selectChain, - mockLogger: logger, - mockSyncAllWebhooksForCredentialSet: vi.fn().mockResolvedValue({}), - } - } -) - -vi.mock('@/lib/auth', () => authMock) - -vi.mock('@sim/db', () => ({ - db: mockDb, +const { mockSyncAllWebhooksForCredentialSet } = vi.hoisted(() => ({ + mockSyncAllWebhooksForCredentialSet: vi.fn().mockResolvedValue({}), })) -vi.mock('@sim/db/schema', () => schemaMock) - -vi.mock('drizzle-orm', () => ({ - and: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'and' })), - eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })), - like: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'like' })), - or: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'or' })), -})) - -vi.mock('@sim/logger', () => ({ - createLogger: vi.fn().mockReturnValue(mockLogger), -})) - -vi.mock('@/lib/core/utils/request', () => requestUtilsMock) +vi.mock('@sim/db', () => dbChainMock) vi.mock('@/lib/webhooks/utils.server', () => ({ syncAllWebhooksForCredentialSet: mockSyncAllWebhooksForCredentialSet, @@ -75,11 +30,8 @@ import { POST } from '@/app/api/auth/oauth/disconnect/route' describe('OAuth Disconnect API Route', () => { beforeEach(() => { vi.clearAllMocks() - - mockDb.delete.mockReturnThis() - mockSelectChain.from.mockReturnThis() - mockSelectChain.innerJoin.mockReturnThis() - mockSelectChain.where.mockResolvedValue([]) + resetDbChainMock() + dbChainMockFns.where.mockResolvedValue([]) }) it('should disconnect provider successfully', async () => { @@ -87,9 +39,6 @@ describe('OAuth Disconnect API Route', () => { user: { id: 'user-123' }, }) - mockDb.delete.mockReturnValueOnce(mockDb) - mockDb.where.mockResolvedValueOnce(undefined) - const req = createMockRequest('POST', { provider: 'google', }) @@ -99,7 +48,6 @@ describe('OAuth Disconnect API Route', () => { expect(response.status).toBe(200) expect(data.success).toBe(true) - expect(mockLogger.info).toHaveBeenCalled() }) it('should disconnect specific provider ID successfully', async () => { @@ -107,9 +55,6 @@ describe('OAuth Disconnect API Route', () => { user: { id: 'user-123' }, }) - mockDb.delete.mockReturnValueOnce(mockDb) - mockDb.where.mockResolvedValueOnce(undefined) - const req = createMockRequest('POST', { provider: 'google', providerId: 'google-email', @@ -120,7 +65,6 @@ describe('OAuth Disconnect API Route', () => { expect(response.status).toBe(200) expect(data.success).toBe(true) - expect(mockLogger.info).toHaveBeenCalled() }) it('should handle unauthenticated user', async () => { @@ -135,7 +79,6 @@ describe('OAuth Disconnect API Route', () => { expect(response.status).toBe(401) expect(data.error).toBe('User not authenticated') - expect(mockLogger.warn).toHaveBeenCalled() }) it('should handle missing provider', async () => { @@ -150,7 +93,6 @@ describe('OAuth Disconnect API Route', () => { expect(response.status).toBe(400) expect(data.error).toBe('Provider is required') - expect(mockLogger.warn).toHaveBeenCalled() }) it('should handle database error', async () => { @@ -158,8 +100,7 @@ describe('OAuth Disconnect API Route', () => { user: { id: 'user-123' }, }) - mockDb.delete.mockReturnValueOnce(mockDb) - mockDb.where.mockRejectedValueOnce(new Error('Database error')) + dbChainMockFns.where.mockRejectedValueOnce(new Error('Database error')) const req = createMockRequest('POST', { provider: 'google', @@ -170,6 +111,5 @@ describe('OAuth Disconnect API Route', () => { expect(response.status).toBe(500) expect(data.error).toBe('Internal server error') - expect(mockLogger.error).toHaveBeenCalled() }) }) diff --git a/apps/sim/app/api/auth/oauth/token/route.test.ts b/apps/sim/app/api/auth/oauth/token/route.test.ts index e589a1fbe4..73091ae8b0 100644 --- a/apps/sim/app/api/auth/oauth/token/route.test.ts +++ b/apps/sim/app/api/auth/oauth/token/route.test.ts @@ -7,42 +7,20 @@ import { authOAuthUtilsMock, authOAuthUtilsMockFns, createMockRequest, - hybridAuthMock, hybridAuthMockFns, - requestUtilsMock, } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockAuthorizeCredentialUse, mockLogger } = vi.hoisted(() => { - const logger = { - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - debug: vi.fn(), - trace: vi.fn(), - fatal: vi.fn(), - child: vi.fn(), - } - return { - mockAuthorizeCredentialUse: vi.fn(), - mockLogger: logger, - } -}) +const { mockAuthorizeCredentialUse } = vi.hoisted(() => ({ + mockAuthorizeCredentialUse: vi.fn(), +})) vi.mock('@/app/api/auth/oauth/utils', () => authOAuthUtilsMock) -vi.mock('@sim/logger', () => ({ - createLogger: vi.fn().mockReturnValue(mockLogger), -})) - vi.mock('@/lib/auth/credential-access', () => ({ authorizeCredentialUse: mockAuthorizeCredentialUse, })) -vi.mock('@/lib/core/utils/request', () => requestUtilsMock) - -vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) - import { GET, POST } from '@/app/api/auth/oauth/token/route' describe('OAuth Token API Routes', () => { @@ -134,7 +112,6 @@ describe('OAuth Token API Routes', () => { 'error', 'Either credentialId or (credentialAccountUserId + providerId) is required' ) - expect(mockLogger.warn).toHaveBeenCalled() }) it('should handle authentication failure', async () => { @@ -369,7 +346,6 @@ describe('OAuth Token API Routes', () => { expect(response.status).toBe(400) expect(data).toHaveProperty('error', 'Credential ID is required') - expect(mockLogger.warn).toHaveBeenCalled() }) it('should handle authentication failure', async () => { diff --git a/apps/sim/app/api/chat/[identifier]/otp/route.test.ts b/apps/sim/app/api/chat/[identifier]/otp/route.test.ts index b15147c539..8069757ea7 100644 --- a/apps/sim/app/api/chat/[identifier]/otp/route.test.ts +++ b/apps/sim/app/api/chat/[identifier]/otp/route.test.ts @@ -6,7 +6,7 @@ import { redisConfigMock, redisConfigMockFns, - schemaMock, + requestUtilsMockFns, workflowsApiUtilsMock, workflowsApiUtilsMockFns, } from '@sim/testing' @@ -28,7 +28,6 @@ const { mockRenderOTPEmail, mockAddCorsHeaders, mockSetChatAuthCookie, - mockGenerateRequestId, mockGetStorageMethod, mockZodParse, mockGetEnv, @@ -53,7 +52,6 @@ const { const mockRenderOTPEmail = vi.fn() const mockAddCorsHeaders = vi.fn() const mockSetChatAuthCookie = vi.fn() - const mockGenerateRequestId = vi.fn() const mockGetStorageMethod = vi.fn() const mockZodParse = vi.fn() const mockGetEnv = vi.fn() @@ -73,7 +71,6 @@ const { mockRenderOTPEmail, mockAddCorsHeaders, mockSetChatAuthCookie, - mockGenerateRequestId, mockGetStorageMethod, mockZodParse, mockGetEnv, @@ -103,8 +100,6 @@ vi.mock('@sim/db', () => ({ }, })) -vi.mock('@sim/db/schema', () => schemaMock) - vi.mock('drizzle-orm', () => ({ eq: vi.fn((field: string, value: string) => ({ field, value, type: 'eq' })), and: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'and' })), @@ -177,10 +172,6 @@ vi.mock('zod', () => { } }) -vi.mock('@/lib/core/utils/request', () => ({ - generateRequestId: mockGenerateRequestId, -})) - import { POST, PUT } from './route' describe('Chat OTP API Route', () => { @@ -242,7 +233,7 @@ describe('Chat OTP API Route', () => { status, })) - mockGenerateRequestId.mockReturnValue('req-123') + requestUtilsMockFns.mockGenerateRequestId.mockReturnValue('req-123') mockZodParse.mockImplementation((data: unknown) => data) diff --git a/apps/sim/app/api/chat/[identifier]/otp/route.ts b/apps/sim/app/api/chat/[identifier]/otp/route.ts index 594949ba8d..6039f40715 100644 --- a/apps/sim/app/api/chat/[identifier]/otp/route.ts +++ b/apps/sim/app/api/chat/[identifier]/otp/route.ts @@ -303,8 +303,12 @@ export async function PUT( const deploymentResult = await db .select({ id: chat.id, + title: chat.title, + description: chat.description, + customizations: chat.customizations, authType: chat.authType, password: chat.password, + outputConfigs: chat.outputConfigs, }) .from(chat) .where(and(eq(chat.identifier, identifier), eq(chat.isActive, true), isNull(chat.archivedAt))) @@ -350,7 +354,17 @@ export async function PUT( await deleteOTP(email, deployment.id) - const response = addCorsHeaders(createSuccessResponse({ authenticated: true }), request) + const response = addCorsHeaders( + createSuccessResponse({ + id: deployment.id, + title: deployment.title, + description: deployment.description, + customizations: deployment.customizations, + authType: deployment.authType, + outputConfigs: deployment.outputConfigs, + }), + request + ) setChatAuthCookie(response, deployment.id, deployment.authType, deployment.password) return response diff --git a/apps/sim/app/api/chat/[identifier]/route.test.ts b/apps/sim/app/api/chat/[identifier]/route.test.ts index 3e364253ae..bddc7d99dd 100644 --- a/apps/sim/app/api/chat/[identifier]/route.test.ts +++ b/apps/sim/app/api/chat/[identifier]/route.test.ts @@ -4,11 +4,12 @@ * @vitest-environment node */ import { + dbChainMock, + dbChainMockFns, encryptionMock, executionPreprocessingMock, executionPreprocessingMockFns, loggingSessionMock, - requestUtilsMock, workflowsApiUtilsMock, workflowsApiUtilsMockFns, } from '@sim/testing' @@ -59,25 +60,19 @@ const createMockStream = () => { }) } -const { - mockDbSelect, - mockAddCorsHeaders, - mockValidateChatAuth, - mockSetChatAuthCookie, - mockValidateAuthToken, -} = vi.hoisted(() => ({ - mockDbSelect: vi.fn(), - mockAddCorsHeaders: vi.fn().mockImplementation((response: Response) => response), - mockValidateChatAuth: vi.fn().mockResolvedValue({ authorized: true }), - mockSetChatAuthCookie: vi.fn(), - mockValidateAuthToken: vi.fn().mockReturnValue(false), -})) +const { mockAddCorsHeaders, mockValidateChatAuth, mockSetChatAuthCookie, mockValidateAuthToken } = + vi.hoisted(() => ({ + mockAddCorsHeaders: vi.fn().mockImplementation((response: Response) => response), + mockValidateChatAuth: vi.fn().mockResolvedValue({ authorized: true }), + mockSetChatAuthCookie: vi.fn(), + mockValidateAuthToken: vi.fn().mockReturnValue(false), + })) const mockCreateErrorResponse = workflowsApiUtilsMockFns.mockCreateErrorResponse const mockCreateSuccessResponse = workflowsApiUtilsMockFns.mockCreateSuccessResponse vi.mock('@sim/db', () => ({ - db: { select: mockDbSelect }, + ...dbChainMock, chat: {}, workflow: {}, })) @@ -117,8 +112,6 @@ vi.mock('@/lib/core/utils/sse', () => ({ }, })) -vi.mock('@/lib/core/utils/request', () => requestUtilsMock) - vi.mock('@/lib/core/security/encryption', () => encryptionMock) import { preprocessExecution } from '@/lib/execution/preprocessing' @@ -201,7 +194,7 @@ describe('Chat Identifier API Route', () => { return new Response(JSON.stringify(data), { status: 200 }) }) - mockDbSelect.mockImplementation((fields: Record) => { + dbChainMockFns.select.mockImplementation((fields: Record) => { if (fields && fields.isDeployed !== undefined) { return { from: vi.fn().mockReturnValue({ @@ -239,7 +232,7 @@ describe('Chat Identifier API Route', () => { }) it('should return 404 for non-existent identifier', async () => { - mockDbSelect.mockImplementation(() => { + dbChainMockFns.select.mockImplementation(() => { return { from: vi.fn().mockReturnValue({ where: vi.fn().mockReturnValue({ @@ -262,7 +255,7 @@ describe('Chat Identifier API Route', () => { }) it('should return 403 for inactive chat', async () => { - mockDbSelect.mockImplementation(() => { + dbChainMockFns.select.mockImplementation(() => { return { from: vi.fn().mockReturnValue({ where: vi.fn().mockReturnValue({ @@ -310,7 +303,7 @@ describe('Chat Identifier API Route', () => { }) describe('POST endpoint', () => { - it('should handle authentication requests without input', async () => { + it('should return chat config on successful authentication', async () => { const req = createMockNextRequest('POST', { password: 'test-password' }) const params = Promise.resolve({ identifier: 'password-protected-chat' }) @@ -319,7 +312,10 @@ describe('Chat Identifier API Route', () => { expect(response.status).toBe(200) const data = await response.json() - expect(data).toHaveProperty('authenticated', true) + expect(data).toHaveProperty('id', 'chat-id') + expect(data).toHaveProperty('title', 'Test Chat') + expect(data).toHaveProperty('customizations') + expect(data.customizations).toHaveProperty('welcomeMessage', 'Welcome to the test chat') expect(mockSetChatAuthCookie).toHaveBeenCalled() }) diff --git a/apps/sim/app/api/chat/[identifier]/route.ts b/apps/sim/app/api/chat/[identifier]/route.ts index 1a8d3b548d..8abc55835e 100644 --- a/apps/sim/app/api/chat/[identifier]/route.ts +++ b/apps/sim/app/api/chat/[identifier]/route.ts @@ -15,6 +15,26 @@ import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/ const logger = createLogger('ChatIdentifierAPI') +interface ChatConfigSource { + id: string + title: string + description: string | null + customizations: unknown + authType: string | null + outputConfigs: unknown +} + +function toChatConfigResponse(deployment: ChatConfigSource) { + return { + id: deployment.id, + title: deployment.title, + description: deployment.description, + customizations: deployment.customizations, + authType: deployment.authType, + outputConfigs: deployment.outputConfigs, + } +} + const chatFileSchema = z.object({ name: z.string().min(1, 'File name is required'), type: z.string().min(1, 'File type is required'), @@ -66,6 +86,9 @@ export async function POST( const deploymentResult = await db .select({ id: chat.id, + title: chat.title, + description: chat.description, + customizations: chat.customizations, workflowId: chat.workflowId, userId: chat.userId, isActive: chat.isActive, @@ -139,7 +162,10 @@ export async function POST( const { input, password, email, conversationId, files } = parsedBody if ((password || email) && !input) { - const response = addCorsHeaders(createSuccessResponse({ authenticated: true }), request) + const response = addCorsHeaders( + createSuccessResponse(toChatConfigResponse(deployment)), + request + ) setChatAuthCookie(response, deployment.id, deployment.authType, deployment.password) @@ -346,17 +372,7 @@ export async function GET( authCookie && validateAuthToken(authCookie.value, deployment.id, deployment.password) ) { - return addCorsHeaders( - createSuccessResponse({ - id: deployment.id, - title: deployment.title, - description: deployment.description, - customizations: deployment.customizations, - authType: deployment.authType, - outputConfigs: deployment.outputConfigs, - }), - request - ) + return addCorsHeaders(createSuccessResponse(toChatConfigResponse(deployment)), request) } const authResult = await validateChatAuth(requestId, deployment, request) @@ -370,17 +386,7 @@ export async function GET( ) } - return addCorsHeaders( - createSuccessResponse({ - id: deployment.id, - title: deployment.title, - description: deployment.description, - customizations: deployment.customizations, - authType: deployment.authType, - outputConfigs: deployment.outputConfigs, - }), - request - ) + return addCorsHeaders(createSuccessResponse(toChatConfigResponse(deployment)), request) } catch (error: any) { logger.error(`[${requestId}] Error fetching chat info:`, error) return addCorsHeaders( diff --git a/apps/sim/app/api/chat/manage/[id]/route.test.ts b/apps/sim/app/api/chat/manage/[id]/route.test.ts index cb80ea64a8..81c04439e3 100644 --- a/apps/sim/app/api/chat/manage/[id]/route.test.ts +++ b/apps/sim/app/api/chat/manage/[id]/route.test.ts @@ -5,11 +5,12 @@ */ import { auditMock, - authMock, authMockFns, + dbChainMock, + dbChainMockFns, encryptionMock, encryptionMockFns, - schemaMock, + resetDbChainMock, workflowsApiUtilsMock, workflowsApiUtilsMockFns, workflowsOrchestrationMock, @@ -20,18 +21,9 @@ import { import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockSelect, mockFrom, mockWhere, mockLimit, mockUpdate, mockSet, mockCheckChatAccess } = - vi.hoisted(() => { - return { - mockSelect: vi.fn(), - mockFrom: vi.fn(), - mockWhere: vi.fn(), - mockLimit: vi.fn(), - mockUpdate: vi.fn(), - mockSet: vi.fn(), - mockCheckChatAccess: vi.fn(), - } - }) +const { mockCheckChatAccess } = vi.hoisted(() => ({ + mockCheckChatAccess: vi.fn(), +})) const mockCreateSuccessResponse = workflowsApiUtilsMockFns.mockCreateSuccessResponse const mockCreateErrorResponse = workflowsApiUtilsMockFns.mockCreateErrorResponse @@ -47,14 +39,7 @@ vi.mock('@/lib/core/config/feature-flags', () => ({ isHosted: false, isProd: false, })) -vi.mock('@/lib/auth', () => authMock) -vi.mock('@sim/db', () => ({ - db: { - select: mockSelect, - update: mockUpdate, - }, -})) -vi.mock('@sim/db/schema', () => schemaMock) +vi.mock('@sim/db', () => dbChainMock) vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock) vi.mock('@/lib/core/security/encryption', () => encryptionMock) vi.mock('@/lib/core/utils/urls', () => ({ @@ -65,24 +50,13 @@ vi.mock('@/app/api/chat/utils', () => ({ })) vi.mock('@/lib/workflows/persistence/utils', () => workflowsPersistenceUtilsMock) vi.mock('@/lib/workflows/orchestration', () => workflowsOrchestrationMock) -vi.mock('drizzle-orm', () => ({ - and: vi.fn((...conditions: unknown[]) => ({ type: 'and', conditions })), - eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })), - isNull: vi.fn((field: unknown) => ({ type: 'isNull', field })), -})) import { DELETE, GET, PATCH } from '@/app/api/chat/manage/[id]/route' describe('Chat Edit API Route', () => { beforeEach(() => { vi.clearAllMocks() - - mockLimit.mockResolvedValue([]) - mockSelect.mockReturnValue({ from: mockFrom }) - mockFrom.mockReturnValue({ where: mockWhere }) - mockWhere.mockReturnValue({ limit: mockLimit }) - mockUpdate.mockReturnValue({ set: mockSet }) - mockSet.mockReturnValue({ where: mockWhere }) + resetDbChainMock() mockPerformChatUndeploy.mockResolvedValue({ success: true }) mockCreateSuccessResponse.mockImplementation((data) => { @@ -220,7 +194,7 @@ describe('Chat Edit API Route', () => { const response = await PATCH(req, { params: Promise.resolve({ id: 'chat-123' }) }) expect(response.status).toBe(200) - expect(mockUpdate).toHaveBeenCalled() + expect(dbChainMockFns.update).toHaveBeenCalled() const data = await response.json() expect(data.id).toBe('chat-123') expect(data.chatUrl).toBe('http://localhost:3000/chat/test-chat') @@ -241,9 +215,9 @@ describe('Chat Edit API Route', () => { mockCheckChatAccess.mockResolvedValue({ hasAccess: true, chat: mockChat }) - mockLimit.mockReset() - mockLimit.mockResolvedValue([{ id: 'other-chat-id', identifier: 'new-identifier' }]) - mockWhere.mockReturnValue({ limit: mockLimit }) + dbChainMockFns.limit.mockResolvedValueOnce([ + { id: 'other-chat-id', identifier: 'new-identifier' }, + ]) const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', { method: 'PATCH', @@ -311,7 +285,7 @@ describe('Chat Edit API Route', () => { expect(response.status).toBe(200) expect(mockEncryptSecret).not.toHaveBeenCalled() - expect(mockSet).toHaveBeenCalledWith( + expect(dbChainMockFns.set).toHaveBeenCalledWith( expect.objectContaining({ authType: 'password', allowedEmails: [], @@ -319,7 +293,7 @@ describe('Chat Edit API Route', () => { }) ) - const updatePayload = mockSet.mock.calls[0]?.[0] + const updatePayload = dbChainMockFns.set.mock.calls[0]?.[0] expect(updatePayload.password).toBeUndefined() }) diff --git a/apps/sim/app/api/chat/route.test.ts b/apps/sim/app/api/chat/route.test.ts index ecb1cb80b0..59d6a72b0e 100644 --- a/apps/sim/app/api/chat/route.test.ts +++ b/apps/sim/app/api/chat/route.test.ts @@ -4,10 +4,10 @@ * @vitest-environment node */ import { - authMock, authMockFns, createEnvMock, - schemaMock, + dbChainMock, + dbChainMockFns, workflowsApiUtilsMock, workflowsApiUtilsMockFns, workflowsOrchestrationMock, @@ -16,32 +16,15 @@ import { import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockSelect, mockFrom, mockWhere, mockLimit, mockCheckWorkflowAccessForChatCreation } = - vi.hoisted(() => ({ - mockSelect: vi.fn(), - mockFrom: vi.fn(), - mockWhere: vi.fn(), - mockLimit: vi.fn(), - mockCheckWorkflowAccessForChatCreation: vi.fn(), - })) +const { mockCheckWorkflowAccessForChatCreation } = vi.hoisted(() => ({ + mockCheckWorkflowAccessForChatCreation: vi.fn(), +})) const mockCreateSuccessResponse = workflowsApiUtilsMockFns.mockCreateSuccessResponse const mockCreateErrorResponse = workflowsApiUtilsMockFns.mockCreateErrorResponse const mockPerformChatDeploy = workflowsOrchestrationMockFns.mockPerformChatDeploy -vi.mock('@sim/db', () => ({ - db: { - select: mockSelect, - }, -})) - -vi.mock('@sim/db/schema', () => schemaMock) - -vi.mock('drizzle-orm', () => ({ - and: vi.fn((...conditions: unknown[]) => ({ type: 'and', conditions })), - eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })), - isNull: vi.fn((field: unknown) => ({ type: 'isNull', field })), -})) +vi.mock('@sim/db', () => dbChainMock) vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock) @@ -51,8 +34,6 @@ vi.mock('@/app/api/chat/utils', () => ({ vi.mock('@/lib/workflows/orchestration', () => workflowsOrchestrationMock) -vi.mock('@/lib/auth', () => authMock) - vi.mock('@/lib/core/config/env', () => createEnvMock({ NODE_ENV: 'development', @@ -66,10 +47,6 @@ describe('Chat API Route', () => { beforeEach(() => { vi.clearAllMocks() - mockSelect.mockReturnValue({ from: mockFrom }) - mockFrom.mockReturnValue({ where: mockWhere }) - mockWhere.mockReturnValue({ limit: mockLimit }) - mockCreateSuccessResponse.mockImplementation((data) => { return new Response(JSON.stringify(data), { status: 200, @@ -108,14 +85,14 @@ describe('Chat API Route', () => { }) const mockDeployments = [{ id: 'deployment-1' }, { id: 'deployment-2' }] - mockWhere.mockResolvedValue(mockDeployments) + dbChainMockFns.where.mockResolvedValueOnce(mockDeployments) const req = new NextRequest('http://localhost:3000/api/chat') const response = await GET(req) expect(response.status).toBe(200) expect(mockCreateSuccessResponse).toHaveBeenCalledWith({ deployments: mockDeployments }) - expect(mockWhere).toHaveBeenCalled() + expect(dbChainMockFns.where).toHaveBeenCalled() }) it('should handle errors when fetching deployments', async () => { @@ -123,7 +100,7 @@ describe('Chat API Route', () => { user: { id: 'user-id' }, }) - mockWhere.mockRejectedValue(new Error('Database error')) + dbChainMockFns.where.mockRejectedValueOnce(new Error('Database error')) const req = new NextRequest('http://localhost:3000/api/chat') const response = await GET(req) @@ -178,7 +155,7 @@ describe('Chat API Route', () => { }, } - mockLimit.mockResolvedValueOnce([{ id: 'existing-chat' }]) // Identifier exists + dbChainMockFns.limit.mockResolvedValueOnce([{ id: 'existing-chat' }]) // Identifier exists mockCheckWorkflowAccessForChatCreation.mockResolvedValue({ hasAccess: false }) const req = new NextRequest('http://localhost:3000/api/chat', { @@ -206,7 +183,7 @@ describe('Chat API Route', () => { }, } - mockLimit.mockResolvedValueOnce([]) // Identifier is available + dbChainMockFns.limit.mockResolvedValueOnce([]) // Identifier is available mockCheckWorkflowAccessForChatCreation.mockResolvedValue({ hasAccess: false }) const req = new NextRequest('http://localhost:3000/api/chat', { @@ -237,7 +214,7 @@ describe('Chat API Route', () => { }, } - mockLimit.mockResolvedValueOnce([]) // Identifier is available + dbChainMockFns.limit.mockResolvedValueOnce([]) // Identifier is available mockCheckWorkflowAccessForChatCreation.mockResolvedValue({ hasAccess: true, workflow: { userId: 'user-id', workspaceId: null, isDeployed: true }, @@ -277,7 +254,7 @@ describe('Chat API Route', () => { outputConfigs: [{ blockId: 'agent-1', path: 'content' }], } - mockLimit.mockResolvedValueOnce([]) + dbChainMockFns.limit.mockResolvedValueOnce([]) mockCheckWorkflowAccessForChatCreation.mockResolvedValue({ hasAccess: true, workflow: { userId: 'user-id', workspaceId: null, isDeployed: true }, @@ -319,7 +296,7 @@ describe('Chat API Route', () => { }, } - mockLimit.mockResolvedValueOnce([]) // Identifier is available + dbChainMockFns.limit.mockResolvedValueOnce([]) // Identifier is available mockCheckWorkflowAccessForChatCreation.mockResolvedValue({ hasAccess: true, workflow: { userId: 'other-user-id', workspaceId: 'workspace-123', isDeployed: true }, @@ -356,7 +333,7 @@ describe('Chat API Route', () => { }, } - mockLimit.mockResolvedValueOnce([]) // Identifier is available + dbChainMockFns.limit.mockResolvedValueOnce([]) // Identifier is available mockCheckWorkflowAccessForChatCreation.mockResolvedValue({ hasAccess: false, }) @@ -390,7 +367,7 @@ describe('Chat API Route', () => { }, } - mockLimit.mockResolvedValueOnce([]) // Identifier is available + dbChainMockFns.limit.mockResolvedValueOnce([]) // Identifier is available mockCheckWorkflowAccessForChatCreation.mockRejectedValue(new Error('Permission check failed')) const req = new NextRequest('http://localhost:3000/api/chat', { @@ -418,7 +395,7 @@ describe('Chat API Route', () => { }, } - mockLimit.mockResolvedValueOnce([]) // Identifier is available + dbChainMockFns.limit.mockResolvedValueOnce([]) // Identifier is available mockCheckWorkflowAccessForChatCreation.mockResolvedValue({ hasAccess: true, workflow: { userId: 'user-id', workspaceId: null, isDeployed: false }, diff --git a/apps/sim/app/api/chat/utils.test.ts b/apps/sim/app/api/chat/utils.test.ts index 4f8dd64c0e..f4cdb54fb8 100644 --- a/apps/sim/app/api/chat/utils.test.ts +++ b/apps/sim/app/api/chat/utils.test.ts @@ -7,7 +7,6 @@ import { encryptionMock, encryptionMockFns, loggingSessionMock, - requestUtilsMock, workflowsUtilsMock, } from '@sim/testing' import type { NextResponse } from 'next/server' @@ -48,8 +47,6 @@ vi.mock('@/lib/workflows/subblocks', () => ({ vi.mock('@/lib/core/security/encryption', () => encryptionMock) -vi.mock('@/lib/core/utils/request', () => requestUtilsMock) - vi.mock('@/lib/core/security/deployment', () => ({ validateAuthToken: mockValidateAuthToken, setDeploymentAuthCookie: mockSetDeploymentAuthCookie, diff --git a/apps/sim/app/api/copilot/api-keys/route.test.ts b/apps/sim/app/api/copilot/api-keys/route.test.ts index 86d4a0cb77..28f635828f 100644 --- a/apps/sim/app/api/copilot/api-keys/route.test.ts +++ b/apps/sim/app/api/copilot/api-keys/route.test.ts @@ -3,7 +3,7 @@ * * @vitest-environment node */ -import { authMock, authMockFns, createEnvMock } from '@sim/testing' +import { authMockFns, createEnvMock } from '@sim/testing' import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -11,8 +11,6 @@ const { mockFetch } = vi.hoisted(() => ({ mockFetch: vi.fn(), })) -vi.mock('@/lib/auth', () => authMock) - vi.mock('@/lib/copilot/constants', () => ({ SIM_AGENT_API_URL_DEFAULT: 'https://agent.sim.example.com', SIM_AGENT_API_URL: 'https://agent.sim.example.com', diff --git a/apps/sim/app/api/copilot/chat/delete/route.test.ts b/apps/sim/app/api/copilot/chat/delete/route.test.ts index 4f88769021..4d1ef809e7 100644 --- a/apps/sim/app/api/copilot/chat/delete/route.test.ts +++ b/apps/sim/app/api/copilot/chat/delete/route.test.ts @@ -3,30 +3,15 @@ * * @vitest-environment node */ -import { authMock, authMockFns, schemaMock } from '@sim/testing' +import { authMockFns, dbChainMock, dbChainMockFns } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -const { mockDelete, mockWhere, mockGetAccessibleCopilotChat } = vi.hoisted(() => ({ - mockDelete: vi.fn(), - mockWhere: vi.fn(), +const { mockGetAccessibleCopilotChat } = vi.hoisted(() => ({ mockGetAccessibleCopilotChat: vi.fn(), })) -vi.mock('@/lib/auth', () => authMock) - -vi.mock('@sim/db', () => ({ - db: { - delete: mockDelete, - }, -})) - -vi.mock('@sim/db/schema', () => schemaMock) - -vi.mock('drizzle-orm', () => ({ - and: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'and' })), - eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })), -})) +vi.mock('@sim/db', () => dbChainMock) vi.mock('@/lib/copilot/chat/lifecycle', () => ({ getAccessibleCopilotChat: mockGetAccessibleCopilotChat, @@ -52,9 +37,7 @@ describe('Copilot Chat Delete API Route', () => { authMockFns.mockGetSession.mockResolvedValue(null) - const mockReturning = vi.fn().mockResolvedValue([{ workspaceId: 'ws-1' }]) - mockWhere.mockReturnValue({ returning: mockReturning }) - mockDelete.mockReturnValue({ where: mockWhere }) + dbChainMockFns.returning.mockResolvedValue([{ workspaceId: 'ws-1' }]) mockGetAccessibleCopilotChat.mockResolvedValue({ id: 'chat-123', userId: 'user-123' }) }) @@ -90,8 +73,8 @@ describe('Copilot Chat Delete API Route', () => { const responseData = await response.json() expect(responseData).toEqual({ success: true }) - expect(mockDelete).toHaveBeenCalled() - expect(mockWhere).toHaveBeenCalled() + expect(dbChainMockFns.delete).toHaveBeenCalled() + expect(dbChainMockFns.where).toHaveBeenCalled() }) it('should return 500 for invalid request body - missing chatId', async () => { @@ -123,7 +106,7 @@ describe('Copilot Chat Delete API Route', () => { it('should handle database errors gracefully', async () => { authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } }) - mockWhere.mockRejectedValueOnce(new Error('Database connection failed')) + dbChainMockFns.returning.mockRejectedValueOnce(new Error('Database connection failed')) const req = createMockRequest('DELETE', { chatId: 'chat-123', @@ -180,7 +163,7 @@ describe('Copilot Chat Delete API Route', () => { const response = await DELETE(req) expect(response.status).toBe(200) - expect(mockDelete).toHaveBeenCalled() + expect(dbChainMockFns.delete).toHaveBeenCalled() }) }) }) diff --git a/apps/sim/app/api/copilot/chat/stop/route.test.ts b/apps/sim/app/api/copilot/chat/stop/route.test.ts index ed92e2481c..0ac05257bf 100644 --- a/apps/sim/app/api/copilot/chat/stop/route.test.ts +++ b/apps/sim/app/api/copilot/chat/stop/route.test.ts @@ -1,7 +1,7 @@ /** * @vitest-environment node */ -import { authMock, authMockFns, schemaMock } from '@sim/testing' +import { authMockFns } from '@sim/testing' import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -29,8 +29,6 @@ const { mockSql: vi.fn((strings: TemplateStringsArray, ...values: unknown[]) => ({ strings, values })), })) -vi.mock('@/lib/auth', () => authMock) - vi.mock('@sim/db', () => ({ db: { select: mockSelect, @@ -38,8 +36,6 @@ vi.mock('@sim/db', () => ({ }, })) -vi.mock('@sim/db/schema', () => schemaMock) - vi.mock('drizzle-orm', () => ({ and: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'and' })), eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })), diff --git a/apps/sim/app/api/copilot/chat/update-messages/route.test.ts b/apps/sim/app/api/copilot/chat/update-messages/route.test.ts index 14d2fa199d..a2f45487a6 100644 --- a/apps/sim/app/api/copilot/chat/update-messages/route.test.ts +++ b/apps/sim/app/api/copilot/chat/update-messages/route.test.ts @@ -3,7 +3,7 @@ * * @vitest-environment node */ -import { authMock, authMockFns, schemaMock } from '@sim/testing' +import { authMockFns } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -18,8 +18,6 @@ const { mockSelect, mockFrom, mockWhere, mockLimit, mockUpdate, mockSet, mockUpd mockUpdateWhere: vi.fn(), })) -vi.mock('@/lib/auth', () => authMock) - vi.mock('@sim/db', () => ({ db: { select: mockSelect, @@ -27,8 +25,6 @@ vi.mock('@sim/db', () => ({ }, })) -vi.mock('@sim/db/schema', () => schemaMock) - vi.mock('drizzle-orm', () => ({ and: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'and' })), eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })), diff --git a/apps/sim/app/api/copilot/chats/route.test.ts b/apps/sim/app/api/copilot/chats/route.test.ts index 4262b41e1d..11046b7a34 100644 --- a/apps/sim/app/api/copilot/chats/route.test.ts +++ b/apps/sim/app/api/copilot/chats/route.test.ts @@ -3,7 +3,7 @@ * * @vitest-environment node */ -import { copilotHttpMock, copilotHttpMockFns, schemaMock } from '@sim/testing' +import { copilotHttpMock, copilotHttpMockFns } from '@sim/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const { mockSelectDistinctOn, mockFrom, mockLeftJoin, mockWhere, mockOrderBy } = vi.hoisted(() => ({ @@ -20,8 +20,6 @@ vi.mock('@sim/db', () => ({ }, })) -vi.mock('@sim/db/schema', () => schemaMock) - vi.mock('drizzle-orm', () => ({ and: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'and' })), eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })), diff --git a/apps/sim/app/api/copilot/checkpoints/revert/route.test.ts b/apps/sim/app/api/copilot/checkpoints/revert/route.test.ts index 4526de0940..05520d5984 100644 --- a/apps/sim/app/api/copilot/checkpoints/revert/route.test.ts +++ b/apps/sim/app/api/copilot/checkpoints/revert/route.test.ts @@ -3,13 +3,7 @@ * * @vitest-environment node */ -import { - authMock, - authMockFns, - schemaMock, - workflowsUtilsMock, - workflowsUtilsMockFns, -} from '@sim/testing' +import { authMockFns, workflowsUtilsMock, workflowsUtilsMockFns } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -31,8 +25,6 @@ const { mockGetAccessibleCopilotChat: vi.fn(), })) -vi.mock('@/lib/auth', () => authMock) - vi.mock('@/lib/core/utils/urls', () => ({ getBaseUrl: vi.fn(() => 'http://localhost:3000'), getInternalApiBaseUrl: vi.fn(() => 'http://localhost:3000'), @@ -53,8 +45,6 @@ vi.mock('@sim/db', () => ({ }, })) -vi.mock('@sim/db/schema', () => schemaMock) - vi.mock('drizzle-orm', () => ({ and: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'and' })), eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })), diff --git a/apps/sim/app/api/copilot/checkpoints/route.test.ts b/apps/sim/app/api/copilot/checkpoints/route.test.ts index 7521958259..e73b6ed0ca 100644 --- a/apps/sim/app/api/copilot/checkpoints/route.test.ts +++ b/apps/sim/app/api/copilot/checkpoints/route.test.ts @@ -3,13 +3,7 @@ * * @vitest-environment node */ -import { - authMock, - authMockFns, - schemaMock, - workflowsUtilsMock, - workflowsUtilsMockFns, -} from '@sim/testing' +import { authMockFns, workflowsUtilsMock, workflowsUtilsMockFns } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -35,8 +29,6 @@ const { mockGetAccessibleCopilotChat: vi.fn(), })) -vi.mock('@/lib/auth', () => authMock) - vi.mock('@sim/db', () => ({ db: { select: mockSelect, @@ -44,8 +36,6 @@ vi.mock('@sim/db', () => ({ }, })) -vi.mock('@sim/db/schema', () => schemaMock) - vi.mock('drizzle-orm', () => ({ and: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'and' })), eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })), diff --git a/apps/sim/app/api/copilot/feedback/route.test.ts b/apps/sim/app/api/copilot/feedback/route.test.ts index 9fef1ea204..b1121ee4b8 100644 --- a/apps/sim/app/api/copilot/feedback/route.test.ts +++ b/apps/sim/app/api/copilot/feedback/route.test.ts @@ -3,33 +3,17 @@ * * @vitest-environment node */ -import { copilotHttpMock, copilotHttpMockFns, schemaMock } from '@sim/testing' +import { + copilotHttpMock, + copilotHttpMockFns, + dbChainMock, + dbChainMockFns, + resetDbChainMock, +} from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -const { mockInsert, mockValues, mockReturning, mockSelect, mockFrom, mockWhere } = vi.hoisted( - () => ({ - mockInsert: vi.fn(), - mockValues: vi.fn(), - mockReturning: vi.fn(), - mockSelect: vi.fn(), - mockFrom: vi.fn(), - mockWhere: vi.fn(), - }) -) - -vi.mock('@sim/db', () => ({ - db: { - insert: mockInsert, - select: mockSelect, - }, -})) - -vi.mock('@sim/db/schema', () => schemaMock) - -vi.mock('drizzle-orm', () => ({ - eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })), -})) +vi.mock('@sim/db', () => dbChainMock) vi.mock('@/lib/copilot/request/http', () => copilotHttpMock) @@ -46,13 +30,7 @@ function createMockRequest(method: string, body: Record): NextR describe('Copilot Feedback API Route', () => { beforeEach(() => { vi.clearAllMocks() - - mockInsert.mockReturnValue({ values: mockValues }) - mockValues.mockReturnValue({ returning: mockReturning }) - mockReturning.mockResolvedValue([]) - mockSelect.mockReturnValue({ from: mockFrom }) - mockFrom.mockReturnValue({ where: mockWhere }) - mockWhere.mockResolvedValue([]) + resetDbChainMock() }) afterEach(() => { @@ -97,7 +75,7 @@ describe('Copilot Feedback API Route', () => { workflowYaml: null, createdAt: new Date('2024-01-01'), } - mockReturning.mockResolvedValueOnce([feedbackRecord]) + dbChainMockFns.returning.mockResolvedValueOnce([feedbackRecord]) const req = createMockRequest('POST', { chatId: '550e8400-e29b-41d4-a716-446655440000', @@ -132,7 +110,7 @@ describe('Copilot Feedback API Route', () => { workflowYaml: null, createdAt: new Date('2024-01-01'), } - mockReturning.mockResolvedValueOnce([feedbackRecord]) + dbChainMockFns.returning.mockResolvedValueOnce([feedbackRecord]) const req = createMockRequest('POST', { chatId: '550e8400-e29b-41d4-a716-446655440000', @@ -178,7 +156,7 @@ edges: workflowYaml: workflowYaml, createdAt: new Date('2024-01-01'), } - mockReturning.mockResolvedValueOnce([feedbackRecord]) + dbChainMockFns.returning.mockResolvedValueOnce([feedbackRecord]) const req = createMockRequest('POST', { chatId: '550e8400-e29b-41d4-a716-446655440000', @@ -194,7 +172,7 @@ edges: const responseData = await response.json() expect(responseData.success).toBe(true) - expect(mockValues).toHaveBeenCalledWith( + expect(dbChainMockFns.values).toHaveBeenCalledWith( expect.objectContaining({ workflowYaml: workflowYaml, }) @@ -286,7 +264,7 @@ edges: isAuthenticated: true, }) - mockReturning.mockRejectedValueOnce(new Error('Database connection failed')) + dbChainMockFns.returning.mockRejectedValueOnce(new Error('Database connection failed')) const req = createMockRequest('POST', { chatId: '550e8400-e29b-41d4-a716-446655440000', @@ -343,7 +321,7 @@ edges: isAuthenticated: true, }) - mockWhere.mockResolvedValueOnce([]) + dbChainMockFns.where.mockResolvedValueOnce([]) const request = new Request('http://localhost:3000/api/copilot/feedback') const response = await GET(request as any) @@ -373,7 +351,7 @@ edges: createdAt: new Date('2024-01-01'), }, ] - mockWhere.mockResolvedValueOnce(mockFeedback) + dbChainMockFns.where.mockResolvedValueOnce(mockFeedback) const request = new Request('http://localhost:3000/api/copilot/feedback') const response = await GET(request as any) @@ -385,9 +363,8 @@ edges: expect(responseData.feedback[0].feedbackId).toBe('feedback-1') expect(responseData.feedback[0].userId).toBe('user-123') - // Verify the where clause was called with the authenticated user's ID const { eq } = await import('drizzle-orm') - expect(mockWhere).toHaveBeenCalled() + expect(dbChainMockFns.where).toHaveBeenCalled() expect(eq).toHaveBeenCalledWith('userId', 'user-123') }) @@ -397,7 +374,7 @@ edges: isAuthenticated: true, }) - mockWhere.mockRejectedValueOnce(new Error('Database connection failed')) + dbChainMockFns.where.mockRejectedValueOnce(new Error('Database connection failed')) const request = new Request('http://localhost:3000/api/copilot/feedback') const response = await GET(request as any) @@ -413,7 +390,7 @@ edges: isAuthenticated: true, }) - mockWhere.mockResolvedValueOnce([]) + dbChainMockFns.where.mockResolvedValueOnce([]) const request = new Request('http://localhost:3000/api/copilot/feedback') const response = await GET(request as any) diff --git a/apps/sim/app/api/files/delete/route.test.ts b/apps/sim/app/api/files/delete/route.test.ts index 063e489675..977902d0be 100644 --- a/apps/sim/app/api/files/delete/route.test.ts +++ b/apps/sim/app/api/files/delete/route.test.ts @@ -1,7 +1,7 @@ /** * @vitest-environment node */ -import { authMock, authMockFns, hybridAuthMock, hybridAuthMockFns, schemaMock } from '@sim/testing' +import { authMockFns, hybridAuthMockFns } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' const mocks = vi.hoisted(() => { @@ -26,8 +26,6 @@ const mocks = vi.hoisted(() => { } }) -vi.mock('@sim/db/schema', () => schemaMock) - vi.mock('drizzle-orm', () => ({ and: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'and' })), eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })), @@ -61,10 +59,6 @@ vi.mock('@sim/utils/id', () => ({ ), })) -vi.mock('@/lib/auth', () => authMock) - -vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) - vi.mock('@/app/api/files/authorization', () => ({ verifyFileAccess: mocks.mockVerifyFileAccess, verifyWorkspaceFileAccess: mocks.mockVerifyWorkspaceFileAccess, diff --git a/apps/sim/app/api/files/parse/route.test.ts b/apps/sim/app/api/files/parse/route.test.ts index 0414692a5f..8a2c06f19f 100644 --- a/apps/sim/app/api/files/parse/route.test.ts +++ b/apps/sim/app/api/files/parse/route.test.ts @@ -4,10 +4,12 @@ * @vitest-environment node */ import { + authMockFns, createMockRequest, - hybridAuthMock, hybridAuthMockFns, inputValidationMock, + permissionsMock, + permissionsMockFns, } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -27,7 +29,6 @@ const { mockFsReadFile, mockFsWriteFile, mockJoin, - mockGetSession, actualPath, } = vi.hoisted(() => { // eslint-disable-next-line @typescript-eslint/no-require-imports @@ -58,7 +59,6 @@ const { } return actualPath.join(...args) }), - mockGetSession: vi.fn(), actualPath, } }) @@ -97,15 +97,6 @@ vi.mock('@/lib/uploads/core/setup.server', () => ({ UPLOAD_DIR_SERVER: '/test/uploads', })) -vi.mock('@/lib/auth', () => ({ - getSession: mockGetSession, - auth: vi.fn(), - signIn: vi.fn(), - signUp: vi.fn(), -})) - -vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) - vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock) vi.mock('@/lib/core/utils/logging', () => ({ @@ -120,9 +111,7 @@ vi.mock('@/lib/uploads/server/metadata', () => ({ getFileMetadataByKey: vi.fn(), })) -vi.mock('@/lib/workspaces/permissions/utils', () => ({ - getUserEntityPermissions: vi.fn().mockResolvedValue({ canView: true }), -})) +vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock) vi.mock('fs/promises', () => ({ default: { @@ -149,11 +138,11 @@ function setupFileApiMocks( const { authenticated = true, storageProvider = 's3', cloudEnabled = true } = options if (authenticated) { - mockGetSession.mockResolvedValue({ + authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'test-user-id', email: 'test@example.com' }, }) } else { - mockGetSession.mockResolvedValue(null) + authMockFns.mockGetSession.mockResolvedValue(null) } hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValue({ @@ -186,6 +175,7 @@ describe('File Parse API Route', () => { authenticated: true, }) + permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue({ canView: true }) mockIsSupportedFileType.mockReturnValue(true) mockParseFile.mockResolvedValue({ content: 'parsed content', @@ -380,6 +370,7 @@ describe('Files Parse API - Path Traversal Security', () => { setupFileApiMocks({ authenticated: true, }) + permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue({ canView: true }) }) describe('Path Traversal Prevention', () => { diff --git a/apps/sim/app/api/files/presigned/route.test.ts b/apps/sim/app/api/files/presigned/route.test.ts index 9f6a6ca4c7..f6641c07d9 100644 --- a/apps/sim/app/api/files/presigned/route.test.ts +++ b/apps/sim/app/api/files/presigned/route.test.ts @@ -4,7 +4,7 @@ * @vitest-environment node */ -import { authMock, authMockFns } from '@sim/testing' +import { authMockFns } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -45,8 +45,6 @@ const { mockIsUsingCloudStorageUploads: vi.fn(), })) -vi.mock('@/lib/auth', () => authMock) - vi.mock('@/app/api/files/authorization', () => ({ verifyFileAccess: mockVerifyFileAccess, verifyWorkspaceFileAccess: mockVerifyWorkspaceFileAccess, diff --git a/apps/sim/app/api/files/serve/[...path]/route.test.ts b/apps/sim/app/api/files/serve/[...path]/route.test.ts index 408470b70f..17b7a8d2fd 100644 --- a/apps/sim/app/api/files/serve/[...path]/route.test.ts +++ b/apps/sim/app/api/files/serve/[...path]/route.test.ts @@ -3,7 +3,7 @@ * * @vitest-environment node */ -import { hybridAuthMock, hybridAuthMockFns } from '@sim/testing' +import { hybridAuthMockFns } from '@sim/testing' import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -47,8 +47,6 @@ vi.mock('fs/promises', () => ({ stat: vi.fn().mockResolvedValue({ isFile: () => true, size: 100 }), })) -vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) - vi.mock('@/app/api/files/authorization', () => ({ verifyFileAccess: mockVerifyFileAccess, })) diff --git a/apps/sim/app/api/files/upload/route.test.ts b/apps/sim/app/api/files/upload/route.test.ts index f38c673332..8e9ff1dbe8 100644 --- a/apps/sim/app/api/files/upload/route.test.ts +++ b/apps/sim/app/api/files/upload/route.test.ts @@ -3,7 +3,7 @@ * * @vitest-environment node */ -import { authMock, authMockFns, hybridAuthMock, hybridAuthMockFns, schemaMock } from '@sim/testing' +import { authMockFns, hybridAuthMockFns, permissionsMock, permissionsMockFns } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -12,7 +12,6 @@ const mocks = vi.hoisted(() => { const mockVerifyWorkspaceFileAccess = vi.fn() const mockVerifyKBFileAccess = vi.fn() const mockVerifyCopilotFileAccess = vi.fn() - const mockGetUserEntityPermissions = vi.fn() const mockUploadWorkspaceFile = vi.fn() const mockGetStorageProvider = vi.fn() const mockIsUsingCloudStorage = vi.fn() @@ -25,7 +24,6 @@ const mocks = vi.hoisted(() => { mockVerifyWorkspaceFileAccess, mockVerifyKBFileAccess, mockVerifyCopilotFileAccess, - mockGetUserEntityPermissions, mockUploadWorkspaceFile, mockGetStorageProvider, mockIsUsingCloudStorage, @@ -35,8 +33,6 @@ const mocks = vi.hoisted(() => { } }) -vi.mock('@sim/db/schema', () => schemaMock) - vi.mock('drizzle-orm', () => ({ and: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'and' })), eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })), @@ -70,10 +66,6 @@ vi.mock('@sim/utils/id', () => ({ ), })) -vi.mock('@/lib/auth', () => authMock) - -vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) - vi.mock('@/app/api/files/authorization', () => ({ verifyFileAccess: mocks.mockVerifyFileAccess, verifyWorkspaceFileAccess: mocks.mockVerifyWorkspaceFileAccess, @@ -81,9 +73,7 @@ vi.mock('@/app/api/files/authorization', () => ({ verifyCopilotFileAccess: mocks.mockVerifyCopilotFileAccess, })) -vi.mock('@/lib/workspaces/permissions/utils', () => ({ - getUserEntityPermissions: mocks.mockGetUserEntityPermissions, -})) +vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock) vi.mock('@/lib/uploads/contexts/workspace', () => ({ uploadWorkspaceFile: mocks.mockUploadWorkspaceFile, @@ -140,7 +130,7 @@ function setupFileApiMocks( mocks.mockVerifyKBFileAccess.mockResolvedValue(true) mocks.mockVerifyCopilotFileAccess.mockResolvedValue(true) - mocks.mockGetUserEntityPermissions.mockResolvedValue('admin') + permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue('admin') mocks.mockUploadWorkspaceFile.mockResolvedValue({ id: 'test-file-id', diff --git a/apps/sim/app/api/folders/[id]/route.test.ts b/apps/sim/app/api/folders/[id]/route.test.ts index f920c7cb59..54411b99d9 100644 --- a/apps/sim/app/api/folders/[id]/route.test.ts +++ b/apps/sim/app/api/folders/[id]/route.test.ts @@ -5,7 +5,6 @@ */ import { auditMock, - authMock, authMockFns, createMockRequest, type MockUser, @@ -39,7 +38,6 @@ const mockPerformDeleteFolder = workflowsOrchestrationMockFns.mockPerformDeleteF const mockGetUserEntityPermissions = permissionsMockFns.mockGetUserEntityPermissions vi.mock('@/lib/audit/log', () => auditMock) -vi.mock('@/lib/auth', () => authMock) vi.mock('@sim/logger', () => ({ createLogger: vi.fn().mockReturnValue(mockLogger), })) diff --git a/apps/sim/app/api/folders/route.test.ts b/apps/sim/app/api/folders/route.test.ts index 1f5ba3272a..7f893b06b3 100644 --- a/apps/sim/app/api/folders/route.test.ts +++ b/apps/sim/app/api/folders/route.test.ts @@ -5,7 +5,6 @@ */ import { auditMock, - authMock, authMockFns, createMockRequest, permissionsMock, @@ -36,7 +35,6 @@ vi.mock('drizzle-orm', () => ({ ...drizzleOrmMock, min: vi.fn((field) => ({ type: 'min', field })), })) -vi.mock('@/lib/auth', () => authMock) vi.mock('@sim/logger', () => ({ createLogger: vi.fn().mockReturnValue(mockLogger), })) diff --git a/apps/sim/app/api/function/execute/route.test.ts b/apps/sim/app/api/function/execute/route.test.ts index a11387064d..1176523c1c 100644 --- a/apps/sim/app/api/function/execute/route.test.ts +++ b/apps/sim/app/api/function/execute/route.test.ts @@ -6,7 +6,6 @@ import { createMockRequest, featureFlagsMock, - hybridAuthMock, hybridAuthMockFns, workflowsUtilsMock, } from '@sim/testing' @@ -22,8 +21,6 @@ vi.mock('@/lib/execution/isolated-vm', () => ({ executeInIsolatedVM: mockExecuteInIsolatedVM, })) -vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) - vi.mock('@/lib/execution/e2b', () => ({ executeInE2B: mockExecuteInE2B, executeShellInE2B: vi.fn(), diff --git a/apps/sim/app/api/jobs/[jobId]/route.test.ts b/apps/sim/app/api/jobs/[jobId]/route.test.ts index 0643a494a1..3caf199209 100644 --- a/apps/sim/app/api/jobs/[jobId]/route.test.ts +++ b/apps/sim/app/api/jobs/[jobId]/route.test.ts @@ -1,13 +1,7 @@ /** * @vitest-environment node */ -import { - hybridAuthMock, - hybridAuthMockFns, - requestUtilsMock, - workflowsUtilsMock, - workflowsUtilsMockFns, -} from '@sim/testing' +import { hybridAuthMockFns, workflowsUtilsMock, workflowsUtilsMockFns } from '@sim/testing' import type { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -17,14 +11,10 @@ const { mockGetJobQueue, mockVerifyWorkflowAccess, mockGetJob } = vi.hoisted(() mockGetJob: vi.fn(), })) -vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) - vi.mock('@/lib/core/async-jobs', () => ({ getJobQueue: mockGetJobQueue, })) -vi.mock('@/lib/core/utils/request', () => requestUtilsMock) - vi.mock('@/socket/middleware/permissions', () => ({ verifyWorkflowAccess: mockVerifyWorkflowAccess, })) diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.test.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.test.ts index 68ddcf270f..5034584ee5 100644 --- a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.test.ts +++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/documents/route.test.ts @@ -4,11 +4,10 @@ import { auditMock, createMockRequest, - hybridAuthMock, hybridAuthMockFns, knowledgeApiUtilsMock, knowledgeApiUtilsMockFns, - schemaMock, + requestUtilsMockFns, } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -30,12 +29,7 @@ const mockCheckAccess = knowledgeApiUtilsMockFns.mockCheckKnowledgeBaseAccess const mockCheckWriteAccess = knowledgeApiUtilsMockFns.mockCheckKnowledgeBaseWriteAccess vi.mock('@sim/db', () => ({ db: mockDbChain })) -vi.mock('@sim/db/schema', () => schemaMock) vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock) -vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) -vi.mock('@/lib/core/utils/request', () => ({ - generateRequestId: vi.fn().mockReturnValue('test-req-id'), -})) vi.mock('@/lib/audit/log', () => auditMock) import { GET, PATCH } from '@/app/api/knowledge/[id]/connectors/[connectorId]/documents/route' @@ -45,6 +39,7 @@ describe('Connector Documents API Route', () => { beforeEach(() => { vi.clearAllMocks() + requestUtilsMockFns.mockGenerateRequestId.mockReturnValue('test-req-id') mockDbChain.select.mockReturnThis() mockDbChain.from.mockReturnThis() mockDbChain.where.mockReturnThis() diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.test.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.test.ts index 83d6de59e2..bfc04f5080 100644 --- a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.test.ts +++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/route.test.ts @@ -5,12 +5,9 @@ import { auditMock, authOAuthUtilsMock, createMockRequest, - hybridAuthMock, hybridAuthMockFns, knowledgeApiUtilsMock, knowledgeApiUtilsMockFns, - requestUtilsMock, - schemaMock, } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -40,10 +37,7 @@ const mockCheckAccess = knowledgeApiUtilsMockFns.mockCheckKnowledgeBaseAccess const mockCheckWriteAccess = knowledgeApiUtilsMockFns.mockCheckKnowledgeBaseWriteAccess vi.mock('@sim/db', () => ({ db: mockDbChain })) -vi.mock('@sim/db/schema', () => schemaMock) vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock) -vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) -vi.mock('@/lib/core/utils/request', () => requestUtilsMock) vi.mock('@/app/api/auth/oauth/utils', () => authOAuthUtilsMock) vi.mock('@/connectors/registry', () => ({ CONNECTOR_REGISTRY: { diff --git a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.test.ts b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.test.ts index cceda25a88..cb5145a1be 100644 --- a/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.test.ts +++ b/apps/sim/app/api/knowledge/[id]/connectors/[connectorId]/sync/route.test.ts @@ -4,11 +4,10 @@ import { auditMock, createMockRequest, - hybridAuthMock, hybridAuthMockFns, knowledgeApiUtilsMock, knowledgeApiUtilsMockFns, - schemaMock, + requestUtilsMockFns, } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -31,12 +30,7 @@ const { mockDispatchSync, mockDbChain } = vi.hoisted(() => { const mockCheckWriteAccess = knowledgeApiUtilsMockFns.mockCheckKnowledgeBaseWriteAccess vi.mock('@sim/db', () => ({ db: mockDbChain })) -vi.mock('@sim/db/schema', () => schemaMock) vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock) -vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) -vi.mock('@/lib/core/utils/request', () => ({ - generateRequestId: vi.fn().mockReturnValue('test-req-id'), -})) vi.mock('@/lib/knowledge/connectors/sync-engine', () => ({ dispatchSync: mockDispatchSync, })) @@ -49,6 +43,7 @@ describe('Connector Manual Sync API Route', () => { beforeEach(() => { vi.clearAllMocks() + requestUtilsMockFns.mockGenerateRequestId.mockReturnValue('test-req-id') mockDbChain.select.mockReturnThis() mockDbChain.from.mockReturnThis() mockDbChain.where.mockReturnThis() diff --git a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.test.ts b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.test.ts index 92fa814ece..2562d3ff8e 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.test.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/[documentId]/route.test.ts @@ -3,14 +3,7 @@ * * @vitest-environment node */ -import { - auditMock, - authMock, - authMockFns, - createMockRequest, - knowledgeApiUtilsMock, - schemaMock, -} from '@sim/testing' +import { auditMock, authMockFns, createMockRequest, knowledgeApiUtilsMock } from '@sim/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const { mockDbChain } = vi.hoisted(() => { @@ -27,14 +20,10 @@ const { mockDbChain } = vi.hoisted(() => { return { mockDbChain } }) -vi.mock('@/lib/auth', () => authMock) - vi.mock('@sim/db', () => ({ db: mockDbChain, })) -vi.mock('@sim/db/schema', () => schemaMock) - vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock) vi.mock('@/lib/knowledge/documents/service', () => ({ diff --git a/apps/sim/app/api/knowledge/[id]/documents/route.test.ts b/apps/sim/app/api/knowledge/[id]/documents/route.test.ts index 52b8519d6a..355f570d80 100644 --- a/apps/sim/app/api/knowledge/[id]/documents/route.test.ts +++ b/apps/sim/app/api/knowledge/[id]/documents/route.test.ts @@ -3,14 +3,7 @@ * * @vitest-environment node */ -import { - auditMock, - authMock, - authMockFns, - createMockRequest, - knowledgeApiUtilsMock, - schemaMock, -} from '@sim/testing' +import { auditMock, authMockFns, createMockRequest, knowledgeApiUtilsMock } from '@sim/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const { mockDbChain } = vi.hoisted(() => { @@ -30,14 +23,10 @@ const { mockDbChain } = vi.hoisted(() => { return { mockDbChain } }) -vi.mock('@/lib/auth', () => authMock) - vi.mock('@sim/db', () => ({ db: mockDbChain, })) -vi.mock('@sim/db/schema', () => schemaMock) - vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock) vi.mock('@/lib/knowledge/documents/service', () => ({ diff --git a/apps/sim/app/api/knowledge/[id]/route.test.ts b/apps/sim/app/api/knowledge/[id]/route.test.ts index 879b649f68..3dd0603c9f 100644 --- a/apps/sim/app/api/knowledge/[id]/route.test.ts +++ b/apps/sim/app/api/knowledge/[id]/route.test.ts @@ -3,14 +3,7 @@ * * @vitest-environment node */ -import { - auditMock, - authMock, - authMockFns, - createMockRequest, - knowledgeApiUtilsMock, - schemaMock, -} from '@sim/testing' +import { auditMock, authMockFns, createMockRequest, knowledgeApiUtilsMock } from '@sim/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const { mockDbChain } = vi.hoisted(() => { @@ -25,14 +18,10 @@ const { mockDbChain } = vi.hoisted(() => { return { mockDbChain } }) -vi.mock('@/lib/auth', () => authMock) - vi.mock('@sim/db', () => ({ db: mockDbChain, })) -vi.mock('@sim/db/schema', () => schemaMock) - vi.mock('@/lib/audit/log', () => auditMock) vi.mock('@/lib/knowledge/service', async (importOriginal) => { diff --git a/apps/sim/app/api/knowledge/route.test.ts b/apps/sim/app/api/knowledge/route.test.ts index 47969406ac..64c3638e4f 100644 --- a/apps/sim/app/api/knowledge/route.test.ts +++ b/apps/sim/app/api/knowledge/route.test.ts @@ -3,7 +3,13 @@ * * @vitest-environment node */ -import { auditMock, authMock, authMockFns, createMockRequest, schemaMock } from '@sim/testing' +import { + auditMock, + authMockFns, + createMockRequest, + permissionsMock, + permissionsMockFns, +} from '@sim/testing' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' const { mockDbChain } = vi.hoisted(() => { @@ -21,19 +27,13 @@ const { mockDbChain } = vi.hoisted(() => { return { mockDbChain } }) -vi.mock('@/lib/auth', () => authMock) - vi.mock('@sim/db', () => ({ db: mockDbChain, })) -vi.mock('@sim/db/schema', () => schemaMock) - vi.mock('@/lib/audit/log', () => auditMock) -vi.mock('@/lib/workspaces/permissions/utils', () => ({ - getUserEntityPermissions: vi.fn().mockResolvedValue('admin'), -})) +vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock) import { GET, POST } from '@/app/api/knowledge/route' @@ -50,6 +50,8 @@ describe('Knowledge Base API Route', () => { } }) + permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue('admin') + vi.stubGlobal('crypto', { randomUUID: vi.fn().mockReturnValue('mock-uuid-1234-5678'), }) diff --git a/apps/sim/app/api/knowledge/search/route.test.ts b/apps/sim/app/api/knowledge/search/route.test.ts index 313d57e153..e9efa57270 100644 --- a/apps/sim/app/api/knowledge/search/route.test.ts +++ b/apps/sim/app/api/knowledge/search/route.test.ts @@ -8,12 +8,9 @@ import { createEnvMock, createMockRequest, - hybridAuthMock, hybridAuthMockFns, knowledgeApiUtilsMock, knowledgeApiUtilsMockFns, - requestUtilsMock, - schemaMock, workflowsUtilsMock, workflowsUtilsMockFns, } from '@sim/testing' @@ -63,20 +60,14 @@ vi.mock('drizzle-orm', () => ({ })), })) -vi.mock('@sim/db/schema', () => schemaMock) - vi.mock('@sim/db', () => ({ db: mockDbChain, })) -vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) - vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) vi.mock('@/lib/core/config/env', () => createEnvMock({ OPENAI_API_KEY: 'test-api-key' })) -vi.mock('@/lib/core/utils/request', () => requestUtilsMock) - vi.mock('@/lib/documents/utils', () => ({ retryWithExponentialBackoff: vi.fn().mockImplementation((fn) => fn()), })) diff --git a/apps/sim/app/api/knowledge/utils.test.ts b/apps/sim/app/api/knowledge/utils.test.ts index ba0d438be8..650c7b1dc6 100644 --- a/apps/sim/app/api/knowledge/utils.test.ts +++ b/apps/sim/app/api/knowledge/utils.test.ts @@ -82,16 +82,21 @@ vi.stubGlobal( }) ) -vi.mock('@sim/db', () => { +vi.mock('@sim/db', async () => { + const { schemaMock } = (await import('@sim/testing')) as typeof import('@sim/testing') + const tableNameFor = (table: any) => { + if (table === schemaMock.knowledgeBase) return 'knowledge_base' + if (table === schemaMock.document) return 'document' + if (table === schemaMock.embedding) return 'embedding' + return '' + } const selectBuilder = { from(table: any) { return { where() { return { limit(n: number) { - const tableSymbols = Object.getOwnPropertySymbols(table || {}) - const baseNameSymbol = tableSymbols.find((s) => s.toString().includes('BaseName')) - const tableName = baseNameSymbol ? table[baseNameSymbol] : '' + const tableName = tableNameFor(table) if (tableName === 'knowledge_base') { return Promise.resolve(kbRows.slice(0, n)) @@ -117,9 +122,7 @@ vi.mock('@sim/db', () => { update: (table: any) => ({ set: (payload: any) => ({ where: () => { - const tableSymbols = Object.getOwnPropertySymbols(table || {}) - const baseNameSymbol = tableSymbols.find((s) => s.toString().includes('BaseName')) - const tableName = baseNameSymbol ? table[baseNameSymbol] : '' + const tableName = tableNameFor(table) if (tableName === 'knowledge_base') { dbOps.order.push('updateKb') dbOps.updatePayloads.push(payload) diff --git a/apps/sim/app/api/mcp/events/route.test.ts b/apps/sim/app/api/mcp/events/route.test.ts index 4d1c0b2638..586d87d701 100644 --- a/apps/sim/app/api/mcp/events/route.test.ts +++ b/apps/sim/app/api/mcp/events/route.test.ts @@ -3,19 +3,11 @@ * * @vitest-environment node */ -import { - authMock, - authMockFns, - createMockRequest, - permissionsMock, - permissionsMockFns, -} from '@sim/testing' +import { authMockFns, createMockRequest, permissionsMock, permissionsMockFns } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' const mockGetUserEntityPermissions = permissionsMockFns.mockGetUserEntityPermissions -vi.mock('@/lib/auth', () => authMock) - vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock) vi.mock('@/lib/events/sse-endpoint', () => ({ diff --git a/apps/sim/app/api/mcp/serve/[serverId]/route.test.ts b/apps/sim/app/api/mcp/serve/[serverId]/route.test.ts index bc45762809..bd9b10d4d3 100644 --- a/apps/sim/app/api/mcp/serve/[serverId]/route.test.ts +++ b/apps/sim/app/api/mcp/serve/[serverId]/route.test.ts @@ -4,51 +4,30 @@ * @vitest-environment node */ import { - hybridAuthMock, + dbChainMock, + dbChainMockFns, hybridAuthMockFns, permissionsMock, permissionsMockFns, - schemaMock, + resetDbChainMock, } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -const { - mockGenerateInternalToken, - mockDbSelect, - mockDbFrom, - mockDbInnerJoin, - mockDbWhere, - mockDbLimit, - fetchMock, -} = vi.hoisted(() => ({ +const { mockGenerateInternalToken, fetchMock } = vi.hoisted(() => ({ mockGenerateInternalToken: vi.fn(), - mockDbSelect: vi.fn(), - mockDbFrom: vi.fn(), - mockDbInnerJoin: vi.fn(), - mockDbWhere: vi.fn(), - mockDbLimit: vi.fn(), fetchMock: vi.fn(), })) const mockGetUserEntityPermissions = permissionsMockFns.mockGetUserEntityPermissions +vi.mock('@sim/db', () => dbChainMock) vi.mock('drizzle-orm', () => ({ and: vi.fn(), eq: vi.fn(), isNull: vi.fn(), })) -vi.mock('@sim/db', () => ({ - db: { - select: mockDbSelect, - }, -})) - -vi.mock('@sim/db/schema', () => schemaMock) - -vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) - vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock) vi.mock('@/lib/auth/internal', () => ({ @@ -69,12 +48,7 @@ import { GET, POST } from '@/app/api/mcp/serve/[serverId]/route' describe('MCP Serve Route', () => { beforeEach(() => { vi.clearAllMocks() - - mockDbSelect.mockReturnValue({ from: mockDbFrom }) - mockDbFrom.mockReturnValue({ innerJoin: mockDbInnerJoin, where: mockDbWhere }) - mockDbInnerJoin.mockReturnValue({ where: mockDbWhere }) - mockDbWhere.mockReturnValue({ limit: mockDbLimit }) - + resetDbChainMock() vi.stubGlobal('fetch', fetchMock) }) @@ -83,7 +57,7 @@ describe('MCP Serve Route', () => { }) it('returns 401 for private server when auth fails', async () => { - mockDbLimit.mockResolvedValueOnce([ + dbChainMockFns.limit.mockResolvedValueOnce([ { id: 'server-1', name: 'Private Server', @@ -107,7 +81,7 @@ describe('MCP Serve Route', () => { }) it('returns 401 on GET for private server when auth fails', async () => { - mockDbLimit.mockResolvedValueOnce([ + dbChainMockFns.limit.mockResolvedValueOnce([ { id: 'server-1', name: 'Private Server', @@ -128,7 +102,7 @@ describe('MCP Serve Route', () => { }) it('forwards X-API-Key for private server api_key auth', async () => { - mockDbLimit + dbChainMockFns.limit .mockResolvedValueOnce([ { id: 'server-1', @@ -177,7 +151,7 @@ describe('MCP Serve Route', () => { }) it('forwards internal token for private server session auth', async () => { - mockDbLimit + dbChainMockFns.limit .mockResolvedValueOnce([ { id: 'server-1', diff --git a/apps/sim/app/api/schedules/[id]/route.test.ts b/apps/sim/app/api/schedules/[id]/route.test.ts index 68809d2e79..01932c9fe3 100644 --- a/apps/sim/app/api/schedules/[id]/route.test.ts +++ b/apps/sim/app/api/schedules/[id]/route.test.ts @@ -5,31 +5,22 @@ */ import { auditMock, - authMock, authMockFns, databaseMock, - requestUtilsMock, - schemaMock, workflowsUtilsMock, workflowsUtilsMockFns, } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -vi.mock('@/lib/auth', () => authMock) - vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) -vi.mock('@sim/db/schema', () => schemaMock) - vi.mock('drizzle-orm', () => ({ and: vi.fn(), eq: vi.fn(), isNull: vi.fn(), })) -vi.mock('@/lib/core/utils/request', () => requestUtilsMock) - vi.mock('@/lib/audit/log', () => auditMock) import { PUT } from './route' diff --git a/apps/sim/app/api/schedules/execute/route.test.ts b/apps/sim/app/api/schedules/execute/route.test.ts index d2531cf76b..d5c50c6c64 100644 --- a/apps/sim/app/api/schedules/execute/route.test.ts +++ b/apps/sim/app/api/schedules/execute/route.test.ts @@ -3,7 +3,14 @@ * * @vitest-environment node */ -import { workflowsUtilsMock, workflowsUtilsMockFns } from '@sim/testing' +import { + dbChainMock, + dbChainMockFns, + requestUtilsMockFns, + resetDbChainMock, + workflowsUtilsMock, + workflowsUtilsMockFns, +} from '@sim/testing' import type { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -12,40 +19,25 @@ const { mockExecuteScheduleJob, mockExecuteJobInline, mockFeatureFlags, - mockDbReturning, - mockDbUpdate, mockEnqueue, mockStartJob, mockCompleteJob, mockMarkJobFailed, -} = vi.hoisted(() => { - const mockDbReturning = vi.fn().mockReturnValue([]) - const mockDbWhere = vi.fn().mockReturnValue({ returning: mockDbReturning }) - const mockDbSet = vi.fn().mockReturnValue({ where: mockDbWhere }) - const mockDbUpdate = vi.fn().mockReturnValue({ set: mockDbSet }) - const mockEnqueue = vi.fn().mockResolvedValue('job-id-1') - const mockStartJob = vi.fn().mockResolvedValue(undefined) - const mockCompleteJob = vi.fn().mockResolvedValue(undefined) - const mockMarkJobFailed = vi.fn().mockResolvedValue(undefined) - - return { - mockVerifyCronAuth: vi.fn().mockReturnValue(null), - mockExecuteScheduleJob: vi.fn().mockResolvedValue(undefined), - mockExecuteJobInline: vi.fn().mockResolvedValue(undefined), - mockFeatureFlags: { - isTriggerDevEnabled: false, - isHosted: false, - isProd: false, - isDev: true, - }, - mockDbReturning, - mockDbUpdate, - mockEnqueue, - mockStartJob, - mockCompleteJob, - mockMarkJobFailed, - } -}) +} = vi.hoisted(() => ({ + mockVerifyCronAuth: vi.fn().mockReturnValue(null), + mockExecuteScheduleJob: vi.fn().mockResolvedValue(undefined), + mockExecuteJobInline: vi.fn().mockResolvedValue(undefined), + mockFeatureFlags: { + isTriggerDevEnabled: false, + isHosted: false, + isProd: false, + isDev: true, + }, + mockEnqueue: vi.fn().mockResolvedValue('job-id-1'), + mockStartJob: vi.fn().mockResolvedValue(undefined), + mockCompleteJob: vi.fn().mockResolvedValue(undefined), + mockMarkJobFailed: vi.fn().mockResolvedValue(undefined), +})) vi.mock('@/lib/auth/internal', () => ({ verifyCronAuth: mockVerifyCronAuth, @@ -59,10 +51,6 @@ vi.mock('@/background/schedule-execution', () => ({ vi.mock('@/lib/core/config/feature-flags', () => mockFeatureFlags) -vi.mock('@/lib/core/utils/request', () => ({ - generateRequestId: vi.fn().mockReturnValue('test-request-id'), -})) - vi.mock('@/lib/core/async-jobs', () => ({ getJobQueue: vi.fn().mockResolvedValue({ enqueue: mockEnqueue, @@ -88,9 +76,7 @@ vi.mock('drizzle-orm', () => ({ })) vi.mock('@sim/db', () => ({ - db: { - update: mockDbUpdate, - }, + ...dbChainMock, workflowSchedule: { id: 'id', workflowId: 'workflowId', @@ -180,6 +166,8 @@ function createMockRequest(): NextRequest { describe('Scheduled Workflow Execution API Route', () => { beforeEach(() => { vi.clearAllMocks() + resetDbChainMock() + requestUtilsMockFns.mockGenerateRequestId.mockReturnValue('test-request-id') workflowsUtilsMockFns.mockGetWorkflowById.mockResolvedValue({ id: 'workflow-1', workspaceId: 'workspace-1', @@ -188,11 +176,11 @@ describe('Scheduled Workflow Execution API Route', () => { mockFeatureFlags.isHosted = false mockFeatureFlags.isProd = false mockFeatureFlags.isDev = true - mockDbReturning.mockReturnValue([]) + dbChainMockFns.returning.mockReturnValue([]) }) it('should execute scheduled workflows with Trigger.dev disabled', async () => { - mockDbReturning.mockReturnValueOnce(SINGLE_SCHEDULE).mockReturnValueOnce([]) + dbChainMockFns.returning.mockReturnValueOnce(SINGLE_SCHEDULE).mockReturnValueOnce([]) const response = await GET(createMockRequest()) @@ -205,7 +193,7 @@ describe('Scheduled Workflow Execution API Route', () => { it('should queue schedules to Trigger.dev when enabled', async () => { mockFeatureFlags.isTriggerDevEnabled = true - mockDbReturning.mockReturnValueOnce(SINGLE_SCHEDULE).mockReturnValueOnce([]) + dbChainMockFns.returning.mockReturnValueOnce(SINGLE_SCHEDULE).mockReturnValueOnce([]) const response = await GET(createMockRequest()) @@ -216,7 +204,7 @@ describe('Scheduled Workflow Execution API Route', () => { }) it('should handle case with no due schedules', async () => { - mockDbReturning.mockReturnValueOnce([]).mockReturnValueOnce([]) + dbChainMockFns.returning.mockReturnValueOnce([]).mockReturnValueOnce([]) const response = await GET(createMockRequest()) @@ -227,7 +215,7 @@ describe('Scheduled Workflow Execution API Route', () => { }) it('should execute multiple schedules in parallel', async () => { - mockDbReturning.mockReturnValueOnce(MULTIPLE_SCHEDULES).mockReturnValueOnce([]) + dbChainMockFns.returning.mockReturnValueOnce(MULTIPLE_SCHEDULES).mockReturnValueOnce([]) const response = await GET(createMockRequest()) @@ -237,7 +225,7 @@ describe('Scheduled Workflow Execution API Route', () => { }) it('should execute mothership jobs inline', async () => { - mockDbReturning.mockReturnValueOnce([]).mockReturnValueOnce(SINGLE_JOB) + dbChainMockFns.returning.mockReturnValueOnce([]).mockReturnValueOnce(SINGLE_JOB) const response = await GET(createMockRequest()) @@ -253,7 +241,7 @@ describe('Scheduled Workflow Execution API Route', () => { }) it('should enqueue schedule with correlation metadata via job queue', async () => { - mockDbReturning.mockReturnValueOnce(SINGLE_SCHEDULE).mockReturnValueOnce([]) + dbChainMockFns.returning.mockReturnValueOnce(SINGLE_SCHEDULE).mockReturnValueOnce([]) const response = await GET(createMockRequest()) diff --git a/apps/sim/app/api/schedules/route.test.ts b/apps/sim/app/api/schedules/route.test.ts index 046beb3205..7a0b1828db 100644 --- a/apps/sim/app/api/schedules/route.test.ts +++ b/apps/sim/app/api/schedules/route.test.ts @@ -3,24 +3,12 @@ * * @vitest-environment node */ -import { - authMock, - authMockFns, - databaseMock, - requestUtilsMock, - schemaMock, - workflowsUtilsMock, - workflowsUtilsMockFns, -} from '@sim/testing' +import { authMockFns, databaseMock, workflowsUtilsMock, workflowsUtilsMockFns } from '@sim/testing' import { NextRequest } from 'next/server' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -vi.mock('@/lib/auth', () => authMock) - vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) -vi.mock('@sim/db/schema', () => schemaMock) - vi.mock('drizzle-orm', () => ({ eq: vi.fn(), and: vi.fn(), @@ -28,8 +16,6 @@ vi.mock('drizzle-orm', () => ({ isNull: vi.fn(), })) -vi.mock('@/lib/core/utils/request', () => requestUtilsMock) - import { GET } from '@/app/api/schedules/route' function createRequest(url: string): NextRequest { diff --git a/apps/sim/app/api/table/[tableId]/import-csv/route.test.ts b/apps/sim/app/api/table/[tableId]/import-csv/route.test.ts index 522c508e4e..fc9a24c307 100644 --- a/apps/sim/app/api/table/[tableId]/import-csv/route.test.ts +++ b/apps/sim/app/api/table/[tableId]/import-csv/route.test.ts @@ -1,7 +1,7 @@ /** * @vitest-environment node */ -import { hybridAuthMock, hybridAuthMockFns, requestUtilsMock } from '@sim/testing' +import { hybridAuthMockFns } from '@sim/testing' import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' import type { TableDefinition } from '@/lib/table' @@ -12,10 +12,6 @@ const { mockCheckAccess, mockBatchInsertRows, mockReplaceTableRows } = vi.hoiste mockReplaceTableRows: vi.fn(), })) -vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) - -vi.mock('@/lib/core/utils/request', () => requestUtilsMock) - vi.mock('@sim/utils/id', () => ({ generateId: vi.fn().mockReturnValue('deadbeefcafef00d'), generateShortId: vi.fn().mockReturnValue('short-id'), diff --git a/apps/sim/app/api/tools/crowdstrike/query/route.test.ts b/apps/sim/app/api/tools/crowdstrike/query/route.test.ts index a0d6269641..39aa92dd78 100644 --- a/apps/sim/app/api/tools/crowdstrike/query/route.test.ts +++ b/apps/sim/app/api/tools/crowdstrike/query/route.test.ts @@ -1,15 +1,13 @@ /** * @vitest-environment node */ -import { createMockRequest, hybridAuthMock, hybridAuthMockFns } from '@sim/testing' +import { createMockRequest, hybridAuthMockFns } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' const { fetchMock } = vi.hoisted(() => ({ fetchMock: vi.fn(), })) -vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) - import { POST } from '@/app/api/tools/crowdstrike/query/route' function jsonResponse(body: unknown, status = 200): Response { diff --git a/apps/sim/app/api/tools/custom/route.test.ts b/apps/sim/app/api/tools/custom/route.test.ts index 63769178a9..4414e92277 100644 --- a/apps/sim/app/api/tools/custom/route.test.ts +++ b/apps/sim/app/api/tools/custom/route.test.ts @@ -4,15 +4,11 @@ * @vitest-environment node */ import { - authMock, authMockFns, createMockRequest, - hybridAuthMock, hybridAuthMockFns, permissionsMock, permissionsMockFns, - requestUtilsMock, - schemaMock, workflowsUtilsMock, workflowsUtilsMockFns, } from '@sim/testing' @@ -158,12 +154,6 @@ vi.mock('@sim/db', () => ({ }, })) -vi.mock('@sim/db/schema', () => schemaMock) - -vi.mock('@/lib/auth', () => authMock) - -vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) - vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock) vi.mock('drizzle-orm', () => ({ @@ -189,8 +179,6 @@ vi.mock('drizzle-orm', () => ({ desc: vi.fn().mockImplementation((field: unknown) => ({ field, operator: 'desc' })), })) -vi.mock('@/lib/core/utils/request', () => requestUtilsMock) - vi.mock('@/lib/workflows/custom-tools/operations', () => ({ upsertCustomTools: (...args: unknown[]) => mockUpsertCustomTools(...args), })) diff --git a/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts b/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts index 3a32df330c..83d3b81b14 100644 --- a/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts +++ b/apps/sim/app/api/webhooks/trigger/[path]/route.test.ts @@ -9,7 +9,6 @@ import { executionPreprocessingMock, executionPreprocessingMockFns, loggingSessionMock, - requestUtilsMock, workflowsPersistenceUtilsMock, workflowsPersistenceUtilsMockFns, workflowsUtilsMock, @@ -353,8 +352,6 @@ vi.mock('drizzle-orm/postgres-js', () => ({ vi.mock('postgres', () => vi.fn().mockReturnValue({})) -vi.mock('@/lib/core/utils/request', () => requestUtilsMock) - process.env.DATABASE_URL = 'postgresql://test:test@localhost:5432/test' import { GET, POST } from '@/app/api/webhooks/trigger/[path]/route' diff --git a/apps/sim/app/api/workflows/[id]/chat/status/route.test.ts b/apps/sim/app/api/workflows/[id]/chat/status/route.test.ts index 58c2f0d76e..fef312d7ac 100644 --- a/apps/sim/app/api/workflows/[id]/chat/status/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/chat/status/route.test.ts @@ -4,38 +4,23 @@ * @vitest-environment node */ import { - hybridAuthMock, + dbChainMock, + dbChainMockFns, hybridAuthMockFns, - schemaMock, + resetDbChainMock, workflowsUtilsMock, workflowsUtilsMockFns, } from '@sim/testing' import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockDbSelect, mockDbFrom, mockDbWhere, mockDbLimit } = vi.hoisted(() => ({ - mockDbSelect: vi.fn(), - mockDbFrom: vi.fn(), - mockDbWhere: vi.fn(), - mockDbLimit: vi.fn(), -})) - +vi.mock('@sim/db', () => dbChainMock) vi.mock('drizzle-orm', () => ({ and: vi.fn((...args: unknown[]) => ({ type: 'and', args })), eq: vi.fn(), isNull: vi.fn((field: unknown) => ({ type: 'isNull', field })), })) -vi.mock('@sim/db', () => ({ - db: { - select: mockDbSelect, - }, -})) - -vi.mock('@sim/db/schema', () => schemaMock) - -vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) - vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) import { GET } from '@/app/api/workflows/[id]/chat/status/route' @@ -43,11 +28,7 @@ import { GET } from '@/app/api/workflows/[id]/chat/status/route' describe('Workflow Chat Status Route', () => { beforeEach(() => { vi.clearAllMocks() - - mockDbSelect.mockReturnValue({ from: mockDbFrom }) - mockDbFrom.mockReturnValue({ where: mockDbWhere }) - mockDbWhere.mockReturnValue({ limit: mockDbLimit }) - mockDbLimit.mockResolvedValue([]) + resetDbChainMock() }) it('returns 401 when unauthenticated', async () => { @@ -91,7 +72,7 @@ describe('Workflow Chat Status Route', () => { workflow: { id: 'wf-1', workspaceId: 'ws-1' }, workspacePermission: 'read', }) - mockDbLimit.mockResolvedValueOnce([ + dbChainMockFns.limit.mockResolvedValueOnce([ { id: 'chat-1', identifier: 'assistant', diff --git a/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts b/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts index 29ebb37e41..6fd35a7543 100644 --- a/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts +++ b/apps/sim/app/api/workflows/[id]/execute/route.async.test.ts @@ -6,32 +6,24 @@ import { createMockRequest, executionPreprocessingMock, executionPreprocessingMockFns, + hybridAuthMockFns, loggingSessionMock, + requestUtilsMockFns, workflowsUtilsMock, workflowsUtilsMockFns, } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockCheckHybridAuth, mockEnqueue } = vi.hoisted(() => ({ - mockCheckHybridAuth: vi.fn(), +const { mockEnqueue } = vi.hoisted(() => ({ mockEnqueue: vi.fn().mockResolvedValue('job-123'), })) +const mockCheckHybridAuth = hybridAuthMockFns.mockCheckHybridAuth const mockPreprocessExecution = executionPreprocessingMockFns.mockPreprocessExecution const mockAuthorizeWorkflowByWorkspacePermission = workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission -vi.mock('@/lib/auth/hybrid', () => ({ - checkHybridAuth: mockCheckHybridAuth, - hasExternalApiCredentials: vi.fn().mockReturnValue(true), - AuthType: { - SESSION: 'session', - API_KEY: 'api_key', - INTERNAL_JWT: 'internal_jwt', - }, -})) - vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) vi.mock('@/lib/execution/preprocessing', () => executionPreprocessingMock) @@ -46,10 +38,6 @@ vi.mock('@/lib/core/async-jobs', () => ({ shouldExecuteInline: vi.fn().mockReturnValue(false), })) -vi.mock('@/lib/core/utils/request', () => ({ - generateRequestId: vi.fn().mockReturnValue('req-12345678'), -})) - vi.mock('@/lib/core/utils/urls', () => ({ getBaseUrl: vi.fn().mockReturnValue('http://localhost:3000'), getOllamaUrl: vi.fn().mockReturnValue('http://localhost:11434'), @@ -82,7 +70,9 @@ describe('workflow execute async route', () => { beforeEach(() => { vi.clearAllMocks() + requestUtilsMockFns.mockGenerateRequestId.mockReturnValue('req-12345678') workflowsUtilsMockFns.mockWorkflowHasResponseBlock.mockReturnValue(false) + hybridAuthMockFns.mockHasExternalApiCredentials.mockReturnValue(true) mockCheckHybridAuth.mockResolvedValue({ success: true, diff --git a/apps/sim/app/api/workflows/[id]/executions/[executionId]/cancel/route.test.ts b/apps/sim/app/api/workflows/[id]/executions/[executionId]/cancel/route.test.ts index 07b7dea386..c7e514a082 100644 --- a/apps/sim/app/api/workflows/[id]/executions/[executionId]/cancel/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/executions/[executionId]/cancel/route.test.ts @@ -4,7 +4,6 @@ import { databaseMock, - hybridAuthMock, hybridAuthMockFns, workflowsUtilsMock, workflowsUtilsMockFns, @@ -28,8 +27,6 @@ const { mockCloseWriter: vi.fn(), })) -vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) - vi.mock('@/lib/execution/cancellation', () => ({ markExecutionCancelled: (...args: unknown[]) => mockMarkExecutionCancelled(...args), })) diff --git a/apps/sim/app/api/workflows/[id]/form/status/route.test.ts b/apps/sim/app/api/workflows/[id]/form/status/route.test.ts index 6aba9e9414..5787f6cd26 100644 --- a/apps/sim/app/api/workflows/[id]/form/status/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/form/status/route.test.ts @@ -4,37 +4,22 @@ * @vitest-environment node */ import { - hybridAuthMock, + dbChainMock, + dbChainMockFns, hybridAuthMockFns, - schemaMock, + resetDbChainMock, workflowsUtilsMock, workflowsUtilsMockFns, } from '@sim/testing' import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' -const { mockDbSelect, mockDbFrom, mockDbWhere, mockDbLimit } = vi.hoisted(() => ({ - mockDbSelect: vi.fn(), - mockDbFrom: vi.fn(), - mockDbWhere: vi.fn(), - mockDbLimit: vi.fn(), -})) - +vi.mock('@sim/db', () => dbChainMock) vi.mock('drizzle-orm', () => ({ and: vi.fn(), eq: vi.fn(), })) -vi.mock('@sim/db', () => ({ - db: { - select: mockDbSelect, - }, -})) - -vi.mock('@sim/db/schema', () => schemaMock) - -vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) - vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) import { GET } from '@/app/api/workflows/[id]/form/status/route' @@ -42,11 +27,7 @@ import { GET } from '@/app/api/workflows/[id]/form/status/route' describe('Workflow Form Status Route', () => { beforeEach(() => { vi.clearAllMocks() - - mockDbSelect.mockReturnValue({ from: mockDbFrom }) - mockDbFrom.mockReturnValue({ where: mockDbWhere }) - mockDbWhere.mockReturnValue({ limit: mockDbLimit }) - mockDbLimit.mockResolvedValue([]) + resetDbChainMock() }) it('returns 401 when unauthenticated', async () => { @@ -90,7 +71,7 @@ describe('Workflow Form Status Route', () => { workflow: { id: 'wf-1', workspaceId: 'ws-1' }, workspacePermission: 'read', }) - mockDbLimit.mockResolvedValueOnce([ + dbChainMockFns.limit.mockResolvedValueOnce([ { id: 'form-1', identifier: 'feedback-form', diff --git a/apps/sim/app/api/workflows/[id]/route.test.ts b/apps/sim/app/api/workflows/[id]/route.test.ts index 0cfe4076b5..8201561adc 100644 --- a/apps/sim/app/api/workflows/[id]/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/route.test.ts @@ -7,11 +7,8 @@ import { auditMock, - authMock, envMock, - hybridAuthMock, hybridAuthMockFns, - requestUtilsMock, telemetryMock, workflowsOrchestrationMock, workflowsOrchestrationMockFns, @@ -51,16 +48,10 @@ function mockGetSession(session: { user: { id: string } } | null) { } } -vi.mock('@/lib/auth', () => authMock) - -vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) - vi.mock('@/lib/core/config/env', () => envMock) vi.mock('@/lib/core/telemetry', () => telemetryMock) -vi.mock('@/lib/core/utils/request', () => requestUtilsMock) - vi.mock('@/lib/audit/log', () => auditMock) vi.mock('@/lib/workflows/persistence/utils', () => workflowsPersistenceUtilsMock) diff --git a/apps/sim/app/api/workflows/[id]/variables/route.test.ts b/apps/sim/app/api/workflows/[id]/variables/route.test.ts index e21f717381..511d46c09c 100644 --- a/apps/sim/app/api/workflows/[id]/variables/route.test.ts +++ b/apps/sim/app/api/workflows/[id]/variables/route.test.ts @@ -6,9 +6,7 @@ */ import { auditMock, - hybridAuthMock, hybridAuthMockFns, - requestUtilsMock, workflowsUtilsMock, workflowsUtilsMockFns, } from '@sim/testing' @@ -17,12 +15,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' vi.mock('@/lib/audit/log', () => auditMock) -vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) - vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock) -vi.mock('@/lib/core/utils/request', () => requestUtilsMock) - import { GET, POST } from '@/app/api/workflows/[id]/variables/route' describe('Workflow Variables API Route', () => { diff --git a/apps/sim/app/api/workflows/route.test.ts b/apps/sim/app/api/workflows/route.test.ts index ab64bd4742..f01c4af4e0 100644 --- a/apps/sim/app/api/workflows/route.test.ts +++ b/apps/sim/app/api/workflows/route.test.ts @@ -4,11 +4,9 @@ import { auditMock, createMockRequest, - hybridAuthMock, hybridAuthMockFns, permissionsMock, permissionsMockFns, - schemaMock, workflowsApiUtilsMock, workflowsPersistenceUtilsMock, workflowsPersistenceUtilsMockFns, @@ -43,12 +41,8 @@ vi.mock('@sim/db', () => ({ }, })) -vi.mock('@sim/db/schema', () => schemaMock) - vi.mock('@/lib/audit/log', () => auditMock) -vi.mock('@/lib/auth/hybrid', () => hybridAuthMock) - vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock) vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock) diff --git a/apps/sim/app/api/workspaces/[id]/docx/preview/route.test.ts b/apps/sim/app/api/workspaces/[id]/docx/preview/route.test.ts index 3b536804dd..cffe9cf9ae 100644 --- a/apps/sim/app/api/workspaces/[id]/docx/preview/route.test.ts +++ b/apps/sim/app/api/workspaces/[id]/docx/preview/route.test.ts @@ -1,12 +1,7 @@ /** * @vitest-environment node */ -import { - authMock, - authMockFns, - workflowsApiUtilsMock, - workflowsApiUtilsMockFns, -} from '@sim/testing' +import { authMockFns, workflowsApiUtilsMock, workflowsApiUtilsMockFns } from '@sim/testing' import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' import { MAX_DOCUMENT_PREVIEW_CODE_BYTES } from '@/lib/execution/constants' @@ -17,8 +12,6 @@ const { mockRunSandboxTask } = vi.hoisted(() => ({ const mockVerifyWorkspaceMembership = workflowsApiUtilsMockFns.mockVerifyWorkspaceMembership -vi.mock('@/lib/auth', () => authMock) - vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock) vi.mock('@/lib/execution/sandbox/run-task', () => ({ diff --git a/apps/sim/app/api/workspaces/[id]/pdf/preview/route.test.ts b/apps/sim/app/api/workspaces/[id]/pdf/preview/route.test.ts index 095eda1662..cf5bd49e45 100644 --- a/apps/sim/app/api/workspaces/[id]/pdf/preview/route.test.ts +++ b/apps/sim/app/api/workspaces/[id]/pdf/preview/route.test.ts @@ -1,12 +1,7 @@ /** * @vitest-environment node */ -import { - authMock, - authMockFns, - workflowsApiUtilsMock, - workflowsApiUtilsMockFns, -} from '@sim/testing' +import { authMockFns, workflowsApiUtilsMock, workflowsApiUtilsMockFns } from '@sim/testing' import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' import { MAX_DOCUMENT_PREVIEW_CODE_BYTES } from '@/lib/execution/constants' @@ -17,8 +12,6 @@ const { mockRunSandboxTask } = vi.hoisted(() => ({ const mockVerifyWorkspaceMembership = workflowsApiUtilsMockFns.mockVerifyWorkspaceMembership -vi.mock('@/lib/auth', () => authMock) - vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock) vi.mock('@/lib/execution/sandbox/run-task', () => ({ diff --git a/apps/sim/app/api/workspaces/[id]/pptx/preview/route.test.ts b/apps/sim/app/api/workspaces/[id]/pptx/preview/route.test.ts index d46479841a..08a8e11f88 100644 --- a/apps/sim/app/api/workspaces/[id]/pptx/preview/route.test.ts +++ b/apps/sim/app/api/workspaces/[id]/pptx/preview/route.test.ts @@ -1,12 +1,7 @@ /** * @vitest-environment node */ -import { - authMock, - authMockFns, - workflowsApiUtilsMock, - workflowsApiUtilsMockFns, -} from '@sim/testing' +import { authMockFns, workflowsApiUtilsMock, workflowsApiUtilsMockFns } from '@sim/testing' import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' import { MAX_DOCUMENT_PREVIEW_CODE_BYTES } from '@/lib/execution/constants' @@ -17,8 +12,6 @@ const { mockRunSandboxTask } = vi.hoisted(() => ({ const mockVerifyWorkspaceMembership = workflowsApiUtilsMockFns.mockVerifyWorkspaceMembership -vi.mock('@/lib/auth', () => authMock) - vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock) vi.mock('@/lib/execution/sandbox/run-task', () => ({ diff --git a/apps/sim/app/api/workspaces/invitations/[invitationId]/route.test.ts b/apps/sim/app/api/workspaces/invitations/[invitationId]/route.test.ts index 8b213f3abf..6c6262769c 100644 --- a/apps/sim/app/api/workspaces/invitations/[invitationId]/route.test.ts +++ b/apps/sim/app/api/workspaces/invitations/[invitationId]/route.test.ts @@ -1,12 +1,10 @@ import { auditMock, - authMock, authMockFns, createSession, createWorkspaceRecord, permissionsMock, permissionsMockFns, - schemaMock, } from '@sim/testing' import { NextRequest } from 'next/server' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -65,8 +63,6 @@ const mockDbTransaction = vi.fn().mockImplementation(async (callback: any) => { }) }) -vi.mock('@/lib/auth', () => authMock) - vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock) vi.mock('@/lib/credentials/environment', () => ({ @@ -105,8 +101,6 @@ vi.mock('@sim/db', () => ({ }, })) -vi.mock('@sim/db/schema', () => schemaMock) - vi.mock('drizzle-orm', () => ({ eq: vi.fn((a: unknown, b: unknown) => ({ type: 'eq', a, b })), and: vi.fn((...args: unknown[]) => ({ type: 'and', args })), diff --git a/apps/sim/app/api/workspaces/invitations/route.test.ts b/apps/sim/app/api/workspaces/invitations/route.test.ts index 089fd5bd19..63e02a2fc7 100644 --- a/apps/sim/app/api/workspaces/invitations/route.test.ts +++ b/apps/sim/app/api/workspaces/invitations/route.test.ts @@ -1,14 +1,7 @@ /** * @vitest-environment node */ -import { - authMock, - authMockFns, - createMockRequest, - permissionsMock, - permissionsMockFns, - schemaMock, -} from '@sim/testing' +import { authMockFns, createMockRequest, permissionsMock, permissionsMockFns } from '@sim/testing' import { beforeEach, describe, expect, it, vi } from 'vitest' const { @@ -69,14 +62,10 @@ vi.mock('@sim/utils/id', () => ({ ), })) -vi.mock('@/lib/auth', () => authMock) - vi.mock('@sim/db', () => ({ db: mockDbChain, })) -vi.mock('@sim/db/schema', () => schemaMock) - vi.mock('resend', () => ({ Resend: vi.fn().mockImplementation(() => ({ emails: { send: mockResendSend }, diff --git a/apps/sim/app/chat/[identifier]/chat.tsx b/apps/sim/app/chat/[identifier]/chat.tsx index 6a37aeb58f..f43faf5eb5 100644 --- a/apps/sim/app/chat/[identifier]/chat.tsx +++ b/apps/sim/app/chat/[identifier]/chat.tsx @@ -1,10 +1,9 @@ 'use client' -import { type RefObject, useCallback, useEffect, useRef, useState } from 'react' +import { type RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' import { noop } from '@/lib/core/utils/request' -import { getFormattedGitHubStars } from '@/app/(landing)/actions/github' import { ChatErrorState, ChatHeader, @@ -19,30 +18,31 @@ import { import { CHAT_ERROR_MESSAGES, CHAT_REQUEST_TIMEOUT_MS } from '@/app/chat/constants' import { useAudioStreaming, useChatStreaming } from '@/app/chat/hooks' import SSOAuth from '@/ee/sso/components/sso-auth' +import { useDeployedChatConfig } from '@/hooks/queries/chats' +import { useGitHubStars } from '@/hooks/queries/github-stars' +import { useVoiceSettings } from '@/hooks/queries/voice-settings' const logger = createLogger('ChatClient') -interface ChatConfig { - id: string - title: string - description: string - customizations: { - primaryColor?: string - logoUrl?: string - imageUrl?: string - welcomeMessage?: string - headerText?: string - } - authType?: 'public' | 'password' | 'email' | 'sso' - outputConfigs?: Array<{ blockId: string; path?: string }> -} - interface AudioStreamingOptions { voiceId: string chatId?: string onError: (error: Error) => void } +interface ChatRequestFile { + name: string + size: number + type: string + data: string +} + +interface ChatRequestPayload { + input: string + conversationId: string + files?: ChatRequestFile[] +} + const DEFAULT_VOICE_SETTINGS = { voiceId: 'EXAVITQu4vr4xnSDxMaL', // Default ElevenLabs voice (Bella) } @@ -86,55 +86,46 @@ function createAudioStreamHandler( } } -function throttle any>(func: T, delay: number): T { - let timeoutId: NodeJS.Timeout | null = null - let lastExecTime = 0 - - return ((...args: Parameters) => { - const currentTime = Date.now() - - if (currentTime - lastExecTime > delay) { - func(...args) - lastExecTime = currentTime - } else { - if (timeoutId) clearTimeout(timeoutId) - timeoutId = setTimeout( - () => { - func(...args) - lastExecTime = Date.now() - }, - delay - (currentTime - lastExecTime) - ) - } - }) as T -} - export default function ChatClient({ identifier }: { identifier: string }) { const [messages, setMessages] = useState([]) const [inputValue, setInputValue] = useState('') const [isLoading, setIsLoading] = useState(false) - const [chatConfig, setChatConfig] = useState(null) - const [error, setError] = useState(null) const messagesEndRef = useRef(null) const messagesContainerRef = useRef(null) - const [starCount, setStarCount] = useState('25.8k') - const [conversationId, setConversationId] = useState('') + const [conversationId] = useState(() => generateId()) const [showScrollButton, setShowScrollButton] = useState(false) const [userHasScrolled, setUserHasScrolled] = useState(false) const isUserScrollingRef = useRef(false) - const [authRequired, setAuthRequired] = useState<'password' | 'email' | 'sso' | null>(null) - const [isVoiceFirstMode, setIsVoiceFirstMode] = useState(false) - const [sttAvailable, setSttAvailable] = useState(false) - useEffect(() => { - fetch('/api/settings/voice') - .then((r) => (r.ok ? r.json() : { sttAvailable: false })) - .then((data) => setSttAvailable(data.sttAvailable === true)) - .catch(() => setSttAvailable(false)) - }, []) + const { data: chatConfigResult, error: chatConfigError } = useDeployedChatConfig(identifier) + const { data: voiceSettings } = useVoiceSettings() + const { data: starCount } = useGitHubStars() + + const sttAvailable = voiceSettings?.sttAvailable === true + const authRequired = chatConfigResult?.kind === 'auth' ? chatConfigResult.authType : null + const chatConfig = chatConfigResult?.kind === 'config' ? chatConfigResult.config : null + + const welcomeMessage = chatConfig?.customizations?.welcomeMessage + const welcomeChatMessage = useMemo( + () => + welcomeMessage + ? { + id: 'welcome', + content: welcomeMessage, + type: 'assistant', + timestamp: new Date(), + isInitialMessage: true, + } + : null, + [welcomeMessage] + ) + const displayMessages: ChatMessage[] = welcomeChatMessage + ? [welcomeChatMessage, ...messages] + : messages + const { isStreamingResponse, abortControllerRef, stopStreaming, handleStreamedResponse } = useChatStreaming() const audioContextRef = useRef(null) @@ -174,29 +165,26 @@ export default function ChatClient({ identifier }: { identifier: string }) { [messagesContainerRef] ) - const handleScroll = useCallback( - throttle(() => { - const container = messagesContainerRef.current - if (!container) return - - const { scrollTop, scrollHeight, clientHeight } = container - const distanceFromBottom = scrollHeight - scrollTop - clientHeight - setShowScrollButton(distanceFromBottom > 100) - - if (isStreamingResponse && !isUserScrollingRef.current) { - setUserHasScrolled(true) - } - }, 100), - [isStreamingResponse] - ) + const isStreamingResponseRef = useRef(isStreamingResponse) + isStreamingResponseRef.current = isStreamingResponse useEffect(() => { const container = messagesContainerRef.current if (!container) return + const handleScroll = () => { + const { scrollTop, scrollHeight, clientHeight } = container + const distanceFromBottom = scrollHeight - scrollTop - clientHeight + setShowScrollButton(distanceFromBottom > 100) + + if (isStreamingResponseRef.current && !isUserScrollingRef.current) { + setUserHasScrolled(true) + } + } + container.addEventListener('scroll', handleScroll, { passive: true }) return () => container.removeEventListener('scroll', handleScroll) - }, [handleScroll]) + }, [chatConfig, isVoiceFirstMode, authRequired]) useEffect(() => { if (isStreamingResponse) { @@ -209,83 +197,6 @@ export default function ChatClient({ identifier }: { identifier: string }) { } }, [isStreamingResponse]) - const fetchChatConfig = async () => { - try { - const response = await fetch(`/api/chat/${identifier}`, { - credentials: 'same-origin', - headers: { - 'X-Requested-With': 'XMLHttpRequest', - }, - }) - - if (!response.ok) { - if (response.status === 401) { - const errorData = await response.json() - - if (errorData.error === 'auth_required_password') { - setAuthRequired('password') - return - } - if (errorData.error === 'auth_required_email') { - setAuthRequired('email') - return - } - if (errorData.error === 'auth_required_sso') { - setAuthRequired('sso') - return - } - } - - throw new Error(`Failed to load chat configuration: ${response.status}`) - } - - setAuthRequired(null) - - const data = await response.json() - - setChatConfig(data) - - if (data?.customizations?.welcomeMessage) { - setMessages([ - { - id: 'welcome', - content: data.customizations.welcomeMessage, - type: 'assistant', - timestamp: new Date(), - isInitialMessage: true, - }, - ]) - } - } catch (error) { - logger.error('Error fetching chat config:', error) - setError(CHAT_ERROR_MESSAGES.CHAT_UNAVAILABLE) - } - } - - useEffect(() => { - fetchChatConfig() - setConversationId(generateId()) - - getFormattedGitHubStars() - .then((formattedStars) => { - setStarCount(formattedStars) - }) - .catch((err) => { - logger.error('Failed to fetch GitHub stars:', err) - }) - }, [identifier]) - - const refreshChat = () => { - fetchChatConfig() - } - - const handleAuthSuccess = () => { - setAuthRequired(null) - setTimeout(() => { - refreshChat() - }, 800) - } - const handleSendMessage = async ( messageParam?: string, isVoiceInput = false, @@ -338,23 +249,25 @@ export default function ChatClient({ identifier }: { identifier: string }) { }, CHAT_REQUEST_TIMEOUT_MS) try { - const payload: any = { + const payloadFiles = + files && files.length > 0 + ? await Promise.all( + files.map(async (file) => ({ + name: file.name, + size: file.size, + type: file.type, + data: file.dataUrl || (await fileToBase64(file.file)), + })) + ) + : undefined + + const payload: ChatRequestPayload = { input: typeof userMessage.content === 'string' ? userMessage.content : JSON.stringify(userMessage.content), conversationId, - } - - if (files && files.length > 0) { - payload.files = await Promise.all( - files.map(async (file) => ({ - name: file.name, - size: file.size, - type: file.type, - data: file.dataUrl || (await fileToBase64(file.file)), - })) - ) + ...(payloadFiles ? { files: payloadFiles } : {}), } logger.info('API payload:', { @@ -412,10 +325,10 @@ export default function ChatClient({ identifier }: { identifier: string }) { outputConfigs: chatConfig?.outputConfigs, } ) - } catch (error: any) { + } catch (error) { clearTimeout(timeoutId) - if (error.name === 'AbortError') { + if (error instanceof Error && error.name === 'AbortError') { logger.info('Request aborted by user or timeout') setIsLoading(false) return @@ -468,20 +381,17 @@ export default function ChatClient({ identifier }: { identifier: string }) { [handleSendMessage] ) - if (error) { - return + if (chatConfigError) { + logger.error('Error fetching chat config:', chatConfigError) + return } if (authRequired) { - // const title = new URLSearchParams(window.location.search).get('title') || 'chat' - // const primaryColor = - // new URLSearchParams(window.location.search).get('color') || 'var(--brand-hover)' - if (authRequired === 'password') { - return + return } if (authRequired === 'email') { - return + return } if (authRequired === 'sso') { return @@ -504,7 +414,7 @@ export default function ChatClient({ identifier }: { identifier: string }) { isPlayingAudio={isPlayingAudio} audioContextRef={audioContextRef} chatId={chatConfig?.id} - messages={messages.map((msg) => ({ + messages={displayMessages.map((msg) => ({ content: typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content), type: msg.type, }))} @@ -519,7 +429,7 @@ export default function ChatClient({ identifier }: { identifier: string }) { {/* Message Container component */} } diff --git a/apps/sim/app/chat/[identifier]/page.tsx b/apps/sim/app/chat/[identifier]/page.tsx index a90238b644..37d6800012 100644 --- a/apps/sim/app/chat/[identifier]/page.tsx +++ b/apps/sim/app/chat/[identifier]/page.tsx @@ -7,5 +7,5 @@ export const metadata: Metadata = { export default async function ChatPage({ params }: { params: Promise<{ identifier: string }> }) { const { identifier } = await params - return + return } diff --git a/apps/sim/app/chat/components/auth/email/email-auth.tsx b/apps/sim/app/chat/components/auth/email/email-auth.tsx index 6100fde631..fe8bf0606c 100644 --- a/apps/sim/app/chat/components/auth/email/email-auth.tsx +++ b/apps/sim/app/chat/components/auth/email/email-auth.tsx @@ -1,7 +1,8 @@ 'use client' -import { type KeyboardEvent, useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' import { Loader2 } from 'lucide-react' import { Input, InputOTP, InputOTPGroup, InputOTPSlot, Label } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' @@ -10,12 +11,12 @@ import AuthBackground from '@/app/(auth)/components/auth-background' import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes' import { SupportFooter } from '@/app/(auth)/components/support-footer' import Navbar from '@/app/(landing)/components/navbar/navbar' +import { useChatEmailOtpRequest, useChatEmailOtpVerify } from '@/hooks/queries/chats' const logger = createLogger('EmailAuth') interface EmailAuthProps { identifier: string - onAuthSuccess: () => void } const validateEmailField = (emailValue: string): string[] => { @@ -34,35 +35,24 @@ const validateEmailField = (emailValue: string): string[] => { return errors } -export default function EmailAuth({ identifier, onAuthSuccess }: EmailAuthProps) { +export default function EmailAuth({ identifier }: EmailAuthProps) { const [email, setEmail] = useState('') const [authError, setAuthError] = useState(null) - const [isSendingOtp, setIsSendingOtp] = useState(false) - const [isVerifyingOtp, setIsVerifyingOtp] = useState(false) const [emailErrors, setEmailErrors] = useState([]) const [showEmailValidationError, setShowEmailValidationError] = useState(false) const [showOtpVerification, setShowOtpVerification] = useState(false) const [otpValue, setOtpValue] = useState('') const [countdown, setCountdown] = useState(0) - const [isResendDisabled, setIsResendDisabled] = useState(false) + + const requestOtp = useChatEmailOtpRequest(identifier) + const verifyOtp = useChatEmailOtpVerify(identifier) useEffect(() => { - if (countdown > 0) { - const timer = setTimeout(() => setCountdown((c) => c - 1), 1000) - return () => clearTimeout(timer) - } - if (countdown === 0 && isResendDisabled) { - setIsResendDisabled(false) - } - }, [countdown, isResendDisabled]) - - const handleEmailKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault() - handleSendOtp() - } - } + if (countdown <= 0) return + const timer = setTimeout(() => setCountdown((c) => c - 1), 1000) + return () => clearTimeout(timer) + }, [countdown]) const handleEmailChange = (e: React.ChangeEvent) => { const newEmail = e.target.value @@ -82,32 +72,14 @@ export default function EmailAuth({ identifier, onAuthSuccess }: EmailAuthProps) } setAuthError(null) - setIsSendingOtp(true) try { - const response = await fetch(`/api/chat/${identifier}/otp`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Requested-With': 'XMLHttpRequest', - }, - body: JSON.stringify({ email }), - }) - - if (!response.ok) { - const errorData = await response.json() - setEmailErrors([errorData.error || 'Failed to send verification code']) - setShowEmailValidationError(true) - return - } - + await requestOtp.mutateAsync({ email }) setShowOtpVerification(true) } catch (error) { logger.error('Error sending OTP:', error) - setEmailErrors(['An error occurred while sending the verification code']) + setEmailErrors([toError(error).message || 'Failed to send verification code']) setShowEmailValidationError(true) - } finally { - setIsSendingOtp(false) } } @@ -119,65 +91,26 @@ export default function EmailAuth({ identifier, onAuthSuccess }: EmailAuthProps) } setAuthError(null) - setIsVerifyingOtp(true) try { - const response = await fetch(`/api/chat/${identifier}/otp`, { - method: 'PUT', - headers: { - 'Content-Type': 'application/json', - 'X-Requested-With': 'XMLHttpRequest', - }, - body: JSON.stringify({ email, otp: codeToVerify }), - }) - - if (!response.ok) { - const errorData = await response.json() - setAuthError(errorData.error || 'Invalid verification code') - return - } - - onAuthSuccess() + await verifyOtp.mutateAsync({ email, otp: codeToVerify }) } catch (error) { logger.error('Error verifying OTP:', error) - setAuthError('An error occurred during verification') - } finally { - setIsVerifyingOtp(false) + setAuthError(toError(error).message || 'Invalid verification code') } } const handleResendOtp = async () => { setAuthError(null) - setIsSendingOtp(true) - setIsResendDisabled(true) setCountdown(30) try { - const response = await fetch(`/api/chat/${identifier}/otp`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Requested-With': 'XMLHttpRequest', - }, - body: JSON.stringify({ email }), - }) - - if (!response.ok) { - const errorData = await response.json() - setAuthError(errorData.error || 'Failed to resend verification code') - setIsResendDisabled(false) - setCountdown(0) - return - } - + await requestOtp.mutateAsync({ email }) setOtpValue('') } catch (error) { logger.error('Error resending OTP:', error) - setAuthError('An error occurred while resending the verification code') - setIsResendDisabled(false) + setAuthError(toError(error).message || 'Failed to resend verification code') setCountdown(0) - } finally { - setIsSendingOtp(false) } } @@ -224,7 +157,6 @@ export default function EmailAuth({ identifier, onAuthSuccess }: EmailAuthProps) autoCorrect='off' value={email} onChange={handleEmailChange} - onKeyDown={handleEmailKeyDown} className={cn( showEmailValidationError && emailErrors.length > 0 && @@ -241,8 +173,12 @@ export default function EmailAuth({ identifier, onAuthSuccess }: EmailAuthProps) )} - diff --git a/apps/sim/app/chat/components/auth/password/password-auth.tsx b/apps/sim/app/chat/components/auth/password/password-auth.tsx index a89f25c16d..669aca42aa 100644 --- a/apps/sim/app/chat/components/auth/password/password-auth.tsx +++ b/apps/sim/app/chat/components/auth/password/password-auth.tsx @@ -1,7 +1,8 @@ 'use client' -import { type KeyboardEvent, useState } from 'react' +import { useState } from 'react' import { createLogger } from '@sim/logger' +import { toError } from '@sim/utils/errors' import { Eye, EyeOff, Loader2 } from 'lucide-react' import { Input, Label } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' @@ -9,27 +10,20 @@ import AuthBackground from '@/app/(auth)/components/auth-background' import { AUTH_SUBMIT_BTN } from '@/app/(auth)/components/auth-button-classes' import { SupportFooter } from '@/app/(auth)/components/support-footer' import Navbar from '@/app/(landing)/components/navbar/navbar' +import { useChatPasswordAuth } from '@/hooks/queries/chats' const logger = createLogger('PasswordAuth') interface PasswordAuthProps { identifier: string - onAuthSuccess: () => void } -export default function PasswordAuth({ identifier, onAuthSuccess }: PasswordAuthProps) { +export default function PasswordAuth({ identifier }: PasswordAuthProps) { const [password, setPassword] = useState('') const [showPassword, setShowPassword] = useState(false) const [showValidationError, setShowValidationError] = useState(false) const [passwordErrors, setPasswordErrors] = useState([]) - const [isAuthenticating, setIsAuthenticating] = useState(false) - - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Enter') { - e.preventDefault() - handleAuthenticate() - } - } + const authenticate = useChatPasswordAuth(identifier) const handlePasswordChange = (e: React.ChangeEvent) => { const newPassword = e.target.value @@ -45,36 +39,13 @@ export default function PasswordAuth({ identifier, onAuthSuccess }: PasswordAuth return } - setIsAuthenticating(true) - try { - const payload = { password } - - const response = await fetch(`/api/chat/${identifier}`, { - method: 'POST', - credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json', - 'X-Requested-With': 'XMLHttpRequest', - }, - body: JSON.stringify(payload), - }) - - if (!response.ok) { - const errorData = await response.json() - setPasswordErrors([errorData.error || 'Invalid password. Please try again.']) - setShowValidationError(true) - return - } - - onAuthSuccess() + await authenticate.mutateAsync({ password }) setPassword('') } catch (error) { logger.error('Authentication error:', error) - setPasswordErrors(['An error occurred during authentication']) + setPasswordErrors([toError(error).message || 'Invalid password. Please try again.']) setShowValidationError(true) - } finally { - setIsAuthenticating(false) } } @@ -120,7 +91,6 @@ export default function PasswordAuth({ identifier, onAuthSuccess }: PasswordAuth placeholder='Enter password' value={password} onChange={handlePasswordChange} - onKeyDown={handleKeyDown} className={cn( 'pr-10', showValidationError && @@ -160,10 +130,10 @@ export default function PasswordAuth({ identifier, onAuthSuccess }: PasswordAuth