v0.6.51: tables improvements, billing fixes, 404 pages, code hygiene

This commit is contained in:
Waleed
2026-04-19 23:35:29 -07:00
committed by GitHub
847 changed files with 25748 additions and 8207 deletions

View File

@@ -10,7 +10,7 @@ Use TSDoc for documentation. No `====` separators. No non-TSDoc comments.
Never update global styles. Keep all styling local to components.
## ID Generation
Never use `crypto.randomUUID()`, `nanoid`, or the `uuid` package directly. Use the utilities from `@/lib/core/utils/uuid`:
Never use `crypto.randomUUID()`, `nanoid`, or the `uuid` package directly. Use the utilities from `@sim/utils/id`:
- `generateId()` — UUID v4, use by default
- `generateShortId(size?)` — short URL-safe ID (default 21 chars), for compact identifiers
@@ -24,14 +24,14 @@ import { v4 as uuidv4 } from 'uuid'
const id = crypto.randomUUID()
// ✓ Good
import { generateId, generateShortId } from '@/lib/core/utils/uuid'
import { generateId, generateShortId } from '@sim/utils/id'
const uuid = generateId()
const shortId = generateShortId()
const tiny = generateShortId(8)
```
## Common Utilities
Use shared helpers from `@/lib/core/utils/helpers` instead of writing inline implementations:
Use shared helpers from `@sim/utils` instead of writing inline implementations:
- `sleep(ms)` — async delay. Never write `new Promise(resolve => setTimeout(resolve, ms))`
- `toError(value)` — normalize unknown caught values to `Error`. Never write `e instanceof Error ? e : new Error(String(e))`
@@ -44,7 +44,8 @@ const msg = error instanceof Error ? error.message : String(error)
const err = error instanceof Error ? error : new Error(String(error))
// ✓ Good
import { sleep, toError } from '@/lib/core/utils/helpers'
import { sleep } from '@sim/utils/helpers'
import { toError } from '@sim/utils/errors'
await sleep(1000)
const msg = toError(error).message
const err = toError(error)

View File

@@ -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`
@@ -102,10 +106,6 @@ vi.mock('@/lib/workspaces/utils', () => ({
}))
```
### NEVER use `mockAuth()`, `mockConsoleLogger()`, or `setupCommonApiMocks()` from `@sim/testing`
These helpers internally use `vi.doMock()` which is slow. Use direct `vi.hoisted()` + `vi.mock()` instead.
### Mock heavy transitive dependencies
If a module under test imports `@/blocks` (200+ files), `@/tools/registry`, or other heavy modules, mock them:
@@ -135,83 +135,122 @@ await new Promise(r => setTimeout(r, 1))
vi.useFakeTimers()
```
## Mock Pattern Reference
## Centralized Mocks (prefer over local declarations)
`@sim/testing` exports ready-to-use mock modules for common dependencies. Import and pass directly to `vi.mock()` — no `vi.hoisted()` boilerplate needed. Each paired `*MockFns` object exposes the underlying `vi.fn()`s for per-test overrides.
| Module mocked | Import | Factory form |
|---|---|---|
| `@/app/api/auth/oauth/utils` | `authOAuthUtilsMock`, `authOAuthUtilsMockFns` | `vi.mock('@/app/api/auth/oauth/utils', () => authOAuthUtilsMock)` |
| `@/app/api/knowledge/utils` | `knowledgeApiUtilsMock`, `knowledgeApiUtilsMockFns` | `vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock)` |
| `@/app/api/workflows/utils` | `workflowsApiUtilsMock`, `workflowsApiUtilsMockFns` | `vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock)` |
| `@/lib/audit/log` | `auditMock`, `auditMockFns` | `vi.mock('@/lib/audit/log', () => auditMock)` |
| `@/lib/auth` | `authMock`, `authMockFns` | `vi.mock('@/lib/auth', () => authMock)` |
| `@/lib/auth/hybrid` | `hybridAuthMock`, `hybridAuthMockFns` | `vi.mock('@/lib/auth/hybrid', () => hybridAuthMock)` |
| `@/lib/copilot/request/http` | `copilotHttpMock`, `copilotHttpMockFns` | `vi.mock('@/lib/copilot/request/http', () => copilotHttpMock)` |
| `@/lib/core/config/env` | `envMock`, `createEnvMock(overrides)` | `vi.mock('@/lib/core/config/env', () => envMock)` |
| `@/lib/core/config/feature-flags` | `featureFlagsMock` | `vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock)` |
| `@/lib/core/config/redis` | `redisConfigMock`, `redisConfigMockFns` | `vi.mock('@/lib/core/config/redis', () => redisConfigMock)` |
| `@/lib/core/security/encryption` | `encryptionMock`, `encryptionMockFns` | `vi.mock('@/lib/core/security/encryption', () => encryptionMock)` |
| `@/lib/core/security/input-validation.server` | `inputValidationMock`, `inputValidationMockFns` | `vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock)` |
| `@/lib/core/utils/request` | `requestUtilsMock`, `requestUtilsMockFns` | `vi.mock('@/lib/core/utils/request', () => requestUtilsMock)` |
| `@/lib/core/utils/urls` | `urlsMock`, `urlsMockFns` | `vi.mock('@/lib/core/utils/urls', () => urlsMock)` |
| `@/lib/execution/preprocessing` | `executionPreprocessingMock`, `executionPreprocessingMockFns` | `vi.mock('@/lib/execution/preprocessing', () => executionPreprocessingMock)` |
| `@/lib/logs/execution/logging-session` | `loggingSessionMock`, `loggingSessionMockFns`, `LoggingSessionMock` | `vi.mock('@/lib/logs/execution/logging-session', () => loggingSessionMock)` |
| `@/lib/workflows/orchestration` | `workflowsOrchestrationMock`, `workflowsOrchestrationMockFns` | `vi.mock('@/lib/workflows/orchestration', () => workflowsOrchestrationMock)` |
| `@/lib/workflows/persistence/utils` | `workflowsPersistenceUtilsMock`, `workflowsPersistenceUtilsMockFns` | `vi.mock('@/lib/workflows/persistence/utils', () => workflowsPersistenceUtilsMock)` |
| `@/lib/workflows/utils` | `workflowsUtilsMock`, `workflowsUtilsMockFns` | `vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock)` |
| `@/lib/workspaces/permissions/utils` | `permissionsMock`, `permissionsMockFns` | `vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock)` |
| `@sim/db/schema` | `schemaMock` | `vi.mock('@sim/db/schema', () => schemaMock)` |
### Auth mocking (API routes)
```typescript
const { mockGetSession } = vi.hoisted(() => ({
mockGetSession: vi.fn(),
}))
import { authMock, authMockFns } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('@/lib/auth', () => ({
auth: { api: { getSession: vi.fn() } },
getSession: mockGetSession,
}))
vi.mock('@/lib/auth', () => authMock)
// In tests:
mockGetSession.mockResolvedValue({ user: { id: 'user-1', email: 'test@example.com' } })
mockGetSession.mockResolvedValue(null) // unauthenticated
import { GET } from '@/app/api/my-route/route'
beforeEach(() => {
vi.clearAllMocks()
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
})
```
Only define a local `vi.mock('@/lib/auth', ...)` if the module under test consumes exports outside the centralized shape (e.g., `auth.api.verifyOneTimeToken`, `auth.api.resetPassword`).
### Hybrid auth mocking
```typescript
const { mockCheckSessionOrInternalAuth } = vi.hoisted(() => ({
mockCheckSessionOrInternalAuth: vi.fn(),
}))
import { hybridAuthMock, hybridAuthMockFns } from '@sim/testing'
vi.mock('@/lib/auth/hybrid', () => ({
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
}))
vi.mock('@/lib/auth/hybrid', () => hybridAuthMock)
// In tests:
mockCheckSessionOrInternalAuth.mockResolvedValue({
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true, userId: 'user-1', authType: 'session',
})
```
### 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.<terminal>.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.
| Category | Utilities |
|----------|-----------|
| **Mocks** | `loggerMock`, `databaseMock`, `drizzleOrmMock`, `setupGlobalFetchMock()` |
| **Module mocks** | See "Centralized Mocks" table above |
| **Logger helpers** | `loggerMock`, `createMockLogger()`, `getLoggerCalls()`, `clearLoggerMocks()` |
| **Database helpers** | `databaseMock`, `drizzleOrmMock`, `createMockDb()`, `createMockSql()`, `createMockSqlOperators()` |
| **Fetch helpers** | `setupGlobalFetchMock()`, `createMockFetch()`, `createMockResponse()`, `mockFetchError()` |
| **Factories** | `createSession()`, `createWorkflowRecord()`, `createBlock()`, `createExecutionContext()` |
| **Builders** | `WorkflowBuilder`, `ExecutionContextBuilder` |
| **Assertions** | `expectWorkflowAccessGranted()`, `expectBlockExecuted()` |
| **Requests** | `createMockRequest()`, `createEnvMock()` |
| **Requests** | `createMockRequest()`, `createMockFormDataRequest()` |
## Rules Summary
1. `@vitest-environment node` unless DOM is required
2. `vi.hoisted()` + `vi.mock()` + static imports — never `vi.resetModules()` + `vi.doMock()` + dynamic imports
3. `vi.mock()` calls before importing mocked modules
4. `@sim/testing` utilities over local mocks
2. Prefer centralized mocks from `@sim/testing` (see table above) over local `vi.hoisted()` + `vi.mock()` boilerplate
3. `vi.hoisted()` + `vi.mock()` + static imports — never `vi.resetModules()` + `vi.doMock()` + dynamic imports
4. `vi.mock()` calls before importing mocked modules
5. `beforeEach(() => vi.clearAllMocks())` to reset state — no redundant `afterEach`
6. No `vi.importActual()` — mock everything explicitly
7. No `mockAuth()`, `mockConsoleLogger()`, `setupCommonApiMocks()` — use direct mocks
8. Mock heavy deps (`@/blocks`, `@/tools/registry`, `@/triggers`) in tests that don't need them
9. Use absolute imports in test files
10. Avoid real timers — use 1ms delays or `vi.useFakeTimers()`
7. Mock heavy deps (`@/blocks`, `@/tools/registry`, `@/triggers`) in tests that don't need them
8. Use absolute imports in test files
9. Avoid real timers — use 1ms delays or `vi.useFakeTimers()`

View File

@@ -17,7 +17,7 @@ Use TSDoc for documentation. No `====` separators. No non-TSDoc comments.
Never update global styles. Keep all styling local to components.
## ID Generation
Never use `crypto.randomUUID()`, `nanoid`, or the `uuid` package directly. Use the utilities from `@/lib/core/utils/uuid`:
Never use `crypto.randomUUID()`, `nanoid`, or the `uuid` package directly. Use the utilities from `@sim/utils/id`:
- `generateId()` — UUID v4, use by default
- `generateShortId(size?)` — short URL-safe ID (default 21 chars), for compact identifiers
@@ -31,14 +31,14 @@ import { v4 as uuidv4 } from 'uuid'
const id = crypto.randomUUID()
// ✓ Good
import { generateId, generateShortId } from '@/lib/core/utils/uuid'
import { generateId, generateShortId } from '@sim/utils/id'
const uuid = generateId()
const shortId = generateShortId()
const tiny = generateShortId(8)
```
## Common Utilities
Use shared helpers from `@/lib/core/utils/helpers` instead of writing inline implementations:
Use shared helpers from `@sim/utils` instead of writing inline implementations:
- `sleep(ms)` — async delay. Never write `new Promise(resolve => setTimeout(resolve, ms))`
- `toError(value)` — normalize unknown caught values to `Error`. Never write `e instanceof Error ? e : new Error(String(e))`
@@ -51,7 +51,8 @@ const msg = error instanceof Error ? error.message : String(error)
const err = error instanceof Error ? error : new Error(String(error))
// ✓ Good
import { sleep, toError } from '@/lib/core/utils/helpers'
import { sleep } from '@sim/utils/helpers'
import { toError } from '@sim/utils/errors'
await sleep(1000)
const msg = toError(error).message
const err = toError(error)

View File

@@ -3,6 +3,7 @@ description: Testing patterns with Vitest and @sim/testing
globs: ["apps/sim/**/*.test.ts", "apps/sim/**/*.test.tsx"]
---
# Testing Patterns
Use Vitest. Test files: `feature.ts` → `feature.test.ts`
@@ -12,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`
@@ -101,10 +106,6 @@ vi.mock('@/lib/workspaces/utils', () => ({
}))
```
### NEVER use `mockAuth()`, `mockConsoleLogger()`, or `setupCommonApiMocks()` from `@sim/testing`
These helpers internally use `vi.doMock()` which is slow. Use direct `vi.hoisted()` + `vi.mock()` instead.
### Mock heavy transitive dependencies
If a module under test imports `@/blocks` (200+ files), `@/tools/registry`, or other heavy modules, mock them:
@@ -134,38 +135,61 @@ await new Promise(r => setTimeout(r, 1))
vi.useFakeTimers()
```
## Mock Pattern Reference
## Centralized Mocks (prefer over local declarations)
`@sim/testing` exports ready-to-use mock modules for common dependencies. Import and pass directly to `vi.mock()` — no `vi.hoisted()` boilerplate needed. Each paired `*MockFns` object exposes the underlying `vi.fn()`s for per-test overrides.
| Module mocked | Import | Factory form |
|---|---|---|
| `@/app/api/auth/oauth/utils` | `authOAuthUtilsMock`, `authOAuthUtilsMockFns` | `vi.mock('@/app/api/auth/oauth/utils', () => authOAuthUtilsMock)` |
| `@/app/api/knowledge/utils` | `knowledgeApiUtilsMock`, `knowledgeApiUtilsMockFns` | `vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock)` |
| `@/app/api/workflows/utils` | `workflowsApiUtilsMock`, `workflowsApiUtilsMockFns` | `vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock)` |
| `@/lib/audit/log` | `auditMock`, `auditMockFns` | `vi.mock('@/lib/audit/log', () => auditMock)` |
| `@/lib/auth` | `authMock`, `authMockFns` | `vi.mock('@/lib/auth', () => authMock)` |
| `@/lib/auth/hybrid` | `hybridAuthMock`, `hybridAuthMockFns` | `vi.mock('@/lib/auth/hybrid', () => hybridAuthMock)` |
| `@/lib/copilot/request/http` | `copilotHttpMock`, `copilotHttpMockFns` | `vi.mock('@/lib/copilot/request/http', () => copilotHttpMock)` |
| `@/lib/core/config/env` | `envMock`, `createEnvMock(overrides)` | `vi.mock('@/lib/core/config/env', () => envMock)` |
| `@/lib/core/config/feature-flags` | `featureFlagsMock` | `vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock)` |
| `@/lib/core/config/redis` | `redisConfigMock`, `redisConfigMockFns` | `vi.mock('@/lib/core/config/redis', () => redisConfigMock)` |
| `@/lib/core/security/encryption` | `encryptionMock`, `encryptionMockFns` | `vi.mock('@/lib/core/security/encryption', () => encryptionMock)` |
| `@/lib/core/security/input-validation.server` | `inputValidationMock`, `inputValidationMockFns` | `vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock)` |
| `@/lib/core/utils/request` | `requestUtilsMock`, `requestUtilsMockFns` | `vi.mock('@/lib/core/utils/request', () => requestUtilsMock)` |
| `@/lib/core/utils/urls` | `urlsMock`, `urlsMockFns` | `vi.mock('@/lib/core/utils/urls', () => urlsMock)` |
| `@/lib/execution/preprocessing` | `executionPreprocessingMock`, `executionPreprocessingMockFns` | `vi.mock('@/lib/execution/preprocessing', () => executionPreprocessingMock)` |
| `@/lib/logs/execution/logging-session` | `loggingSessionMock`, `loggingSessionMockFns`, `LoggingSessionMock` | `vi.mock('@/lib/logs/execution/logging-session', () => loggingSessionMock)` |
| `@/lib/workflows/orchestration` | `workflowsOrchestrationMock`, `workflowsOrchestrationMockFns` | `vi.mock('@/lib/workflows/orchestration', () => workflowsOrchestrationMock)` |
| `@/lib/workflows/persistence/utils` | `workflowsPersistenceUtilsMock`, `workflowsPersistenceUtilsMockFns` | `vi.mock('@/lib/workflows/persistence/utils', () => workflowsPersistenceUtilsMock)` |
| `@/lib/workflows/utils` | `workflowsUtilsMock`, `workflowsUtilsMockFns` | `vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock)` |
| `@/lib/workspaces/permissions/utils` | `permissionsMock`, `permissionsMockFns` | `vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock)` |
| `@sim/db/schema` | `schemaMock` | `vi.mock('@sim/db/schema', () => schemaMock)` |
### Auth mocking (API routes)
```typescript
const { mockGetSession } = vi.hoisted(() => ({
mockGetSession: vi.fn(),
}))
import { authMock, authMockFns } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('@/lib/auth', () => ({
auth: { api: { getSession: vi.fn() } },
getSession: mockGetSession,
}))
vi.mock('@/lib/auth', () => authMock)
// In tests:
mockGetSession.mockResolvedValue({ user: { id: 'user-1', email: 'test@example.com' } })
mockGetSession.mockResolvedValue(null) // unauthenticated
import { GET } from '@/app/api/my-route/route'
beforeEach(() => {
vi.clearAllMocks()
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
})
```
Only define a local `vi.mock('@/lib/auth', ...)` if the module under test consumes exports outside the centralized shape (e.g., `auth.api.verifyOneTimeToken`, `auth.api.resetPassword`).
### Hybrid auth mocking
```typescript
const { mockCheckSessionOrInternalAuth } = vi.hoisted(() => ({
mockCheckSessionOrInternalAuth: vi.fn(),
}))
import { hybridAuthMock, hybridAuthMockFns } from '@sim/testing'
vi.mock('@/lib/auth/hybrid', () => ({
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
}))
vi.mock('@/lib/auth/hybrid', () => hybridAuthMock)
// In tests:
mockCheckSessionOrInternalAuth.mockResolvedValue({
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true, userId: 'user-1', authType: 'session',
})
```
@@ -196,21 +220,23 @@ Always prefer over local test data.
| Category | Utilities |
|----------|-----------|
| **Mocks** | `loggerMock`, `databaseMock`, `drizzleOrmMock`, `setupGlobalFetchMock()` |
| **Module mocks** | See "Centralized Mocks" table above |
| **Logger helpers** | `loggerMock`, `createMockLogger()`, `getLoggerCalls()`, `clearLoggerMocks()` |
| **Database helpers** | `databaseMock`, `drizzleOrmMock`, `createMockDb()`, `createMockSql()`, `createMockSqlOperators()` |
| **Fetch helpers** | `setupGlobalFetchMock()`, `createMockFetch()`, `createMockResponse()`, `mockFetchError()` |
| **Factories** | `createSession()`, `createWorkflowRecord()`, `createBlock()`, `createExecutionContext()` |
| **Builders** | `WorkflowBuilder`, `ExecutionContextBuilder` |
| **Assertions** | `expectWorkflowAccessGranted()`, `expectBlockExecuted()` |
| **Requests** | `createMockRequest()`, `createEnvMock()` |
| **Requests** | `createMockRequest()`, `createMockFormDataRequest()` |
## Rules Summary
1. `@vitest-environment node` unless DOM is required
2. `vi.hoisted()` + `vi.mock()` + static imports — never `vi.resetModules()` + `vi.doMock()` + dynamic imports
3. `vi.mock()` calls before importing mocked modules
4. `@sim/testing` utilities over local mocks
2. Prefer centralized mocks from `@sim/testing` (see table above) over local `vi.hoisted()` + `vi.mock()` boilerplate
3. `vi.hoisted()` + `vi.mock()` + static imports — never `vi.resetModules()` + `vi.doMock()` + dynamic imports
4. `vi.mock()` calls before importing mocked modules
5. `beforeEach(() => vi.clearAllMocks())` to reset state — no redundant `afterEach`
6. No `vi.importActual()` — mock everything explicitly
7. No `mockAuth()`, `mockConsoleLogger()`, `setupCommonApiMocks()` — use direct mocks
8. Mock heavy deps (`@/blocks`, `@/tools/registry`, `@/triggers`) in tests that don't need them
9. Use absolute imports in test files
10. Avoid real timers — use 1ms delays or `vi.useFakeTimers()`
7. Mock heavy deps (`@/blocks`, `@/tools/registry`, `@/triggers`) in tests that don't need them
8. Use absolute imports in test files
9. Avoid real timers — use 1ms delays or `vi.useFakeTimers()`

View File

@@ -7,7 +7,7 @@ You are a professional software engineer. All code must follow best practices: a
- **Logging**: Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`
- **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments
- **Styling**: Never update global styles. Keep all styling local to components
- **ID Generation**: Never use `crypto.randomUUID()`, `nanoid`, or `uuid` package. Use `generateId()` (UUID v4) or `generateShortId()` (compact) from `@/lib/core/utils/uuid`
- **ID Generation**: Never use `crypto.randomUUID()`, `nanoid`, or `uuid` package. Use `generateId()` (UUID v4) or `generateShortId()` (compact) from `@sim/utils/id`
- **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx`
## Architecture

View File

@@ -7,8 +7,8 @@ You are a professional software engineer. All code must follow best practices: a
- **Logging**: Import `createLogger` from `@sim/logger`. Use `logger.info`, `logger.warn`, `logger.error` instead of `console.log`
- **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments
- **Styling**: Never update global styles. Keep all styling local to components
- **ID Generation**: Never use `crypto.randomUUID()`, `nanoid`, or `uuid` package. Use `generateId()` (UUID v4) or `generateShortId()` (compact) from `@/lib/core/utils/uuid`
- **Common Utilities**: Use shared helpers from `@/lib/core/utils/helpers` instead of inline implementations. `sleep(ms)` for delays, `toError(e)` to normalize caught values.
- **ID Generation**: Never use `crypto.randomUUID()`, `nanoid`, or `uuid` package. Use `generateId()` (UUID v4) or `generateShortId()` (compact) from `@sim/utils/id`
- **Common Utilities**: Use shared helpers from `@sim/utils` instead of inline implementations. `sleep(ms)` from `@sim/utils/helpers` for delays, `toError(e)` from `@sim/utils/errors` to normalize caught values.
- **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx`
## Architecture

View File

@@ -142,13 +142,15 @@ See the [environment variables reference](https://docs.sim.ai/self-hosting/envir
- **Database**: PostgreSQL with [Drizzle ORM](https://orm.drizzle.team)
- **Authentication**: [Better Auth](https://better-auth.com)
- **UI**: [Shadcn](https://ui.shadcn.com/), [Tailwind CSS](https://tailwindcss.com)
- **State Management**: [Zustand](https://zustand-demo.pmnd.rs/)
- **Streaming Markdown**: [Streamdown](https://github.com/vercel/streamdown)
- **State Management**: [Zustand](https://zustand-demo.pmnd.rs/), [TanStack Query](https://tanstack.com/query)
- **Flow Editor**: [ReactFlow](https://reactflow.dev/)
- **Docs**: [Fumadocs](https://fumadocs.vercel.app/)
- **Monorepo**: [Turborepo](https://turborepo.org/)
- **Realtime**: [Socket.io](https://socket.io/)
- **Background Jobs**: [Trigger.dev](https://trigger.dev/)
- **Remote Code Execution**: [E2B](https://www.e2b.dev/)
- **Isolated Code Execution**: [isolated-vm](https://github.com/laverdet/isolated-vm)
## Contributing

View File

@@ -1,26 +0,0 @@
import { createLogger } from '@sim/logger'
const DEFAULT_STARS = '19.4k'
const logger = createLogger('GitHubStars')
export async function getFormattedGitHubStars(): Promise<string> {
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
}
}

View File

@@ -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 (
<a

View File

@@ -0,0 +1,28 @@
import type { Metadata } from 'next'
import Link from 'next/link'
import { AUTH_PRIMARY_CTA_BASE } from '@/app/(auth)/components/auth-button-classes'
export const metadata: Metadata = {
title: 'Page Not Found',
robots: { index: false, follow: true },
}
export default function IntegrationsNotFound() {
return (
<div className='flex min-h-[60vh] items-center justify-center px-4 py-24'>
<div className='flex flex-col items-center gap-3'>
<h1 className='text-balance font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
Page not found
</h1>
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-lg leading-[125%] tracking-[0.02em]'>
The page you&apos;re looking for doesn&apos;t exist or has been moved.
</p>
<div className='mt-3 flex items-center gap-2'>
<Link href='/' className={AUTH_PRIMARY_CTA_BASE}>
Return to Home
</Link>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,28 @@
import type { Metadata } from 'next'
import Link from 'next/link'
import { AUTH_PRIMARY_CTA_BASE } from '@/app/(auth)/components/auth-button-classes'
export const metadata: Metadata = {
title: 'Page Not Found',
robots: { index: false, follow: true },
}
export default function ModelsNotFound() {
return (
<div className='flex min-h-[60vh] items-center justify-center px-4 py-24'>
<div className='flex flex-col items-center gap-3'>
<h1 className='text-balance font-[430] font-season text-[40px] text-white leading-[110%] tracking-[-0.02em]'>
Page not found
</h1>
<p className='font-[430] font-season text-[color-mix(in_srgb,var(--landing-text-subtle)_60%,transparent)] text-lg leading-[125%] tracking-[0.02em]'>
The page you&apos;re looking for doesn&apos;t exist or has been moved.
</p>
<div className='mt-3 flex items-center gap-2'>
<Link href='/' className={AUTH_PRIMARY_CTA_BASE}>
Return to Home
</Link>
</div>
</div>
</div>
)
}

View File

@@ -2,6 +2,7 @@
import { useCallback, useEffect, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { sleep } from '@sim/utils/helpers'
import type { Edge } from 'reactflow'
import { buildMockExecutionPlan } from '@/lib/academy/mock-execution'
import type {
@@ -12,7 +13,6 @@ import type {
} from '@/lib/academy/types'
import { validateExercise } from '@/lib/academy/validation'
import { cn } from '@/lib/core/utils/cn'
import { sleep } from '@/lib/core/utils/helpers'
import { getEffectiveBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
import { getQueryClient } from '@/app/_shell/providers/get-query-client'
import { GlobalCommandsProvider } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'

View File

@@ -7,13 +7,13 @@
import { db } from '@sim/db'
import { a2aAgent, workflow } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq, isNull, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { generateSkillsFromWorkflow } from '@/lib/a2a/agent-card'
import { A2A_DEFAULT_CAPABILITIES } from '@/lib/a2a/constants'
import { sanitizeAgentName } from '@/lib/a2a/utils'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import { captureServerEvent } from '@/lib/posthog/server'
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'

View File

@@ -2,6 +2,7 @@ import type { Artifact, Message, PushNotificationConfig, TaskState } from '@a2a-
import { db } from '@sim/db'
import { a2aAgent, a2aPushNotificationConfig, a2aTask, workflow } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq, isNull } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { A2A_DEFAULT_TIMEOUT, A2A_MAX_HISTORY_LENGTH } from '@/lib/a2a/constants'
@@ -18,7 +19,6 @@ import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
import { getClientIp } from '@/lib/core/utils/request'
import { SSE_HEADERS } from '@/lib/core/utils/sse'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { generateId } from '@/lib/core/utils/uuid'
import { markExecutionCancelled } from '@/lib/execution/cancellation'
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils'

View File

@@ -1,7 +1,7 @@
import type { Artifact, Message, PushNotificationConfig, Task, TaskState } from '@a2a-js/sdk'
import { generateId } from '@sim/utils/id'
import { generateInternalToken } from '@/lib/auth/internal'
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
import { generateId } from '@/lib/core/utils/uuid'
/** A2A v0.3 JSON-RPC method names */
export const A2A_METHODS = {

View File

@@ -1,6 +1,7 @@
import { db } from '@sim/db'
import { academyCertificate, user } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateShortId } from '@sim/utils/id'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
@@ -9,7 +10,6 @@ import type { CertificateMetadata } from '@/lib/academy/types'
import { getSession } from '@/lib/auth'
import type { TokenBucketConfig } from '@/lib/core/rate-limiter'
import { RateLimiter } from '@/lib/core/rate-limiter'
import { generateShortId } from '@/lib/core/utils/uuid'
const logger = createLogger('AcademyCertificatesAPI')

View File

@@ -3,43 +3,23 @@
*
* @vitest-environment node
*/
import { createMockRequest } from '@sim/testing'
import {
authMockFns,
createMockRequest,
dbChainMock,
dbChainMockFns,
resetDbChainMock,
} from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockGetSession, mockDb, mockLogger, mockParseProvider, mockJwtDecode, mockEq } = vi.hoisted(
() => {
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 {
mockGetSession: vi.fn(),
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', () => ({
getSession: mockGetSession,
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,
@@ -53,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,
}))
@@ -66,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,
@@ -78,7 +51,7 @@ describe('OAuth Connections API Route', () => {
})
it('should return connections successfully', async () => {
mockGetSession.mockResolvedValueOnce({
authMockFns.mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
@@ -103,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')
@@ -134,7 +101,7 @@ describe('OAuth Connections API Route', () => {
})
it('should handle unauthenticated user', async () => {
mockGetSession.mockResolvedValueOnce(null)
authMockFns.mockGetSession.mockResolvedValueOnce(null)
const req = createMockRequest('GET')
@@ -143,22 +110,15 @@ 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 () => {
mockGetSession.mockResolvedValueOnce({
authMockFns.mockGetSession.mockResolvedValueOnce({
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')
@@ -170,13 +130,11 @@ describe('OAuth Connections API Route', () => {
})
it('should handle database error', async () => {
mockGetSession.mockResolvedValueOnce({
authMockFns.mockGetSession.mockResolvedValueOnce({
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')
@@ -185,11 +143,10 @@ 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 () => {
mockGetSession.mockResolvedValueOnce({
authMockFns.mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
@@ -209,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')

View File

@@ -4,74 +4,17 @@
* @vitest-environment node
*/
import { hybridAuthMockFns, permissionsMock, workflowsUtilsMock } from '@sim/testing'
import { NextRequest } from 'next/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockCheckSessionOrInternalAuth, 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 {
mockCheckSessionOrInternalAuth: vi.fn(),
mockLogger: logger,
}
})
vi.mock('@/lib/auth/hybrid', () => ({
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
}))
vi.mock('@/lib/core/utils/request', () => ({
generateRequestId: vi.fn().mockReturnValue('mock-request-id'),
}))
vi.mock('@/lib/credentials/oauth', () => ({
syncWorkspaceOAuthCredentialsForUser: vi.fn(),
}))
vi.mock('@/lib/workflows/utils', () => ({
authorizeWorkflowByWorkspacePermission: vi.fn(),
}))
vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock)
vi.mock('@/lib/workspaces/permissions/utils', () => ({
checkWorkspaceAccess: vi.fn(),
}))
vi.mock('@sim/db/schema', () => ({
account: {
userId: 'userId',
providerId: 'providerId',
id: 'id',
scope: 'scope',
updatedAt: 'updatedAt',
},
credential: {
id: 'id',
workspaceId: 'workspaceId',
type: 'type',
displayName: 'displayName',
providerId: 'providerId',
accountId: 'accountId',
},
credentialMember: {
id: 'id',
credentialId: 'credentialId',
userId: 'userId',
status: 'status',
},
user: { email: 'email', id: 'id' },
}))
vi.mock('@sim/logger', () => ({
createLogger: vi.fn().mockReturnValue(mockLogger),
}))
vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock)
import { GET } from '@/app/api/auth/oauth/credentials/route'
@@ -86,7 +29,7 @@ describe('OAuth Credentials API Route', () => {
})
it('should handle unauthenticated user', async () => {
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
success: false,
error: 'Authentication required',
})
@@ -98,11 +41,10 @@ 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 () => {
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
success: true,
userId: 'user-123',
authType: 'session',
@@ -115,11 +57,10 @@ 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 () => {
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
success: true,
userId: 'user-123',
authType: 'session',
@@ -135,7 +76,7 @@ describe('OAuth Credentials API Route', () => {
})
it('should return empty credentials when no workspace context', async () => {
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
success: true,
userId: 'user-123',
authType: 'session',

View File

@@ -3,112 +3,42 @@
*
* @vitest-environment node
*/
import { createMockRequest } from '@sim/testing'
import {
auditMock,
authMockFns,
createMockRequest,
dbChainMock,
dbChainMockFns,
resetDbChainMock,
} from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockGetSession, 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 {
mockGetSession: vi.fn(),
mockDb: db,
mockSelectChain: selectChain,
mockLogger: logger,
mockSyncAllWebhooksForCredentialSet: vi.fn().mockResolvedValue({}),
}
})
vi.mock('@/lib/auth', () => ({
getSession: mockGetSession,
const { mockSyncAllWebhooksForCredentialSet } = vi.hoisted(() => ({
mockSyncAllWebhooksForCredentialSet: vi.fn().mockResolvedValue({}),
}))
vi.mock('@sim/db', () => ({
db: mockDb,
}))
vi.mock('@sim/db/schema', () => ({
account: { userId: 'userId', providerId: 'providerId' },
credentialSetMember: {
id: 'id',
credentialSetId: 'credentialSetId',
userId: 'userId',
status: 'status',
},
credentialSet: { id: 'id', providerId: 'providerId' },
}))
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', () => ({
generateRequestId: vi.fn().mockReturnValue('test-request-id'),
}))
vi.mock('@sim/db', () => dbChainMock)
vi.mock('@/lib/webhooks/utils.server', () => ({
syncAllWebhooksForCredentialSet: mockSyncAllWebhooksForCredentialSet,
}))
vi.mock('@/lib/audit/log', () => ({
recordAudit: vi.fn(),
AuditAction: {
CREDENTIAL_SET_CREATED: 'credential_set.created',
CREDENTIAL_SET_UPDATED: 'credential_set.updated',
CREDENTIAL_SET_DELETED: 'credential_set.deleted',
OAUTH_CONNECTED: 'oauth.connected',
OAUTH_DISCONNECTED: 'oauth.disconnected',
},
AuditResourceType: {
CREDENTIAL_SET: 'credential_set',
OAUTH_CONNECTION: 'oauth_connection',
},
}))
vi.mock('@/lib/audit/log', () => auditMock)
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 () => {
mockGetSession.mockResolvedValueOnce({
authMockFns.mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
mockDb.delete.mockReturnValueOnce(mockDb)
mockDb.where.mockResolvedValueOnce(undefined)
const req = createMockRequest('POST', {
provider: 'google',
})
@@ -118,17 +48,13 @@ 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 () => {
mockGetSession.mockResolvedValueOnce({
authMockFns.mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
mockDb.delete.mockReturnValueOnce(mockDb)
mockDb.where.mockResolvedValueOnce(undefined)
const req = createMockRequest('POST', {
provider: 'google',
providerId: 'google-email',
@@ -139,11 +65,10 @@ 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 () => {
mockGetSession.mockResolvedValueOnce(null)
authMockFns.mockGetSession.mockResolvedValueOnce(null)
const req = createMockRequest('POST', {
provider: 'google',
@@ -154,11 +79,10 @@ 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 () => {
mockGetSession.mockResolvedValueOnce({
authMockFns.mockGetSession.mockResolvedValueOnce({
user: { id: 'user-123' },
})
@@ -169,16 +93,14 @@ 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 () => {
mockGetSession.mockResolvedValueOnce({
authMockFns.mockGetSession.mockResolvedValueOnce({
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',
@@ -189,6 +111,5 @@ describe('OAuth Disconnect API Route', () => {
expect(response.status).toBe(500)
expect(data.error).toBe('Internal server error')
expect(mockLogger.error).toHaveBeenCalled()
})
})

View File

@@ -3,76 +3,30 @@
*
* @vitest-environment node
*/
import { createMockRequest } from '@sim/testing'
import {
authOAuthUtilsMock,
authOAuthUtilsMockFns,
createMockRequest,
hybridAuthMockFns,
} from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
mockGetUserId,
mockGetCredential,
mockRefreshTokenIfNeeded,
mockGetOAuthToken,
mockResolveOAuthAccountId,
mockGetServiceAccountToken,
mockAuthorizeCredentialUse,
mockCheckSessionOrInternalAuth,
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 {
mockGetUserId: vi.fn(),
mockGetCredential: vi.fn(),
mockRefreshTokenIfNeeded: vi.fn(),
mockGetOAuthToken: vi.fn(),
mockResolveOAuthAccountId: vi.fn(),
mockGetServiceAccountToken: vi.fn(),
mockAuthorizeCredentialUse: vi.fn(),
mockCheckSessionOrInternalAuth: vi.fn(),
mockLogger: logger,
}
})
vi.mock('@/app/api/auth/oauth/utils', () => ({
getUserId: mockGetUserId,
getCredential: mockGetCredential,
refreshTokenIfNeeded: mockRefreshTokenIfNeeded,
getOAuthToken: mockGetOAuthToken,
resolveOAuthAccountId: mockResolveOAuthAccountId,
getServiceAccountToken: mockGetServiceAccountToken,
const { mockAuthorizeCredentialUse } = vi.hoisted(() => ({
mockAuthorizeCredentialUse: vi.fn(),
}))
vi.mock('@sim/logger', () => ({
createLogger: vi.fn().mockReturnValue(mockLogger),
}))
vi.mock('@/app/api/auth/oauth/utils', () => authOAuthUtilsMock)
vi.mock('@/lib/auth/credential-access', () => ({
authorizeCredentialUse: mockAuthorizeCredentialUse,
}))
vi.mock('@/lib/core/utils/request', () => ({
generateRequestId: vi.fn().mockReturnValue('test-request-id'),
}))
vi.mock('@/lib/auth/hybrid', () => ({
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
checkHybridAuth: vi.fn(),
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
checkInternalAuth: vi.fn(),
}))
import { GET, POST } from '@/app/api/auth/oauth/token/route'
describe('OAuth Token API Routes', () => {
beforeEach(() => {
vi.clearAllMocks()
mockResolveOAuthAccountId.mockResolvedValue(null)
authOAuthUtilsMockFns.mockResolveOAuthAccountId.mockResolvedValue(null)
})
/**
@@ -86,14 +40,14 @@ describe('OAuth Token API Routes', () => {
requesterUserId: 'test-user-id',
credentialOwnerUserId: 'owner-user-id',
})
mockGetCredential.mockResolvedValueOnce({
authOAuthUtilsMockFns.mockGetCredential.mockResolvedValueOnce({
id: 'credential-id',
accessToken: 'test-token',
refreshToken: 'refresh-token',
accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000),
providerId: 'google',
})
mockRefreshTokenIfNeeded.mockResolvedValueOnce({
authOAuthUtilsMockFns.mockRefreshTokenIfNeeded.mockResolvedValueOnce({
accessToken: 'fresh-token',
refreshed: false,
})
@@ -109,8 +63,8 @@ describe('OAuth Token API Routes', () => {
expect(data).toHaveProperty('accessToken', 'fresh-token')
expect(mockAuthorizeCredentialUse).toHaveBeenCalled()
expect(mockGetCredential).toHaveBeenCalled()
expect(mockRefreshTokenIfNeeded).toHaveBeenCalled()
expect(authOAuthUtilsMockFns.mockGetCredential).toHaveBeenCalled()
expect(authOAuthUtilsMockFns.mockRefreshTokenIfNeeded).toHaveBeenCalled()
})
it('should handle workflowId for server-side authentication', async () => {
@@ -120,14 +74,14 @@ describe('OAuth Token API Routes', () => {
requesterUserId: 'workflow-owner-id',
credentialOwnerUserId: 'workflow-owner-id',
})
mockGetCredential.mockResolvedValueOnce({
authOAuthUtilsMockFns.mockGetCredential.mockResolvedValueOnce({
id: 'credential-id',
accessToken: 'test-token',
refreshToken: 'refresh-token',
accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000),
providerId: 'google',
})
mockRefreshTokenIfNeeded.mockResolvedValueOnce({
authOAuthUtilsMockFns.mockRefreshTokenIfNeeded.mockResolvedValueOnce({
accessToken: 'fresh-token',
refreshed: false,
})
@@ -144,7 +98,7 @@ describe('OAuth Token API Routes', () => {
expect(data).toHaveProperty('accessToken', 'fresh-token')
expect(mockAuthorizeCredentialUse).toHaveBeenCalled()
expect(mockGetCredential).toHaveBeenCalled()
expect(authOAuthUtilsMockFns.mockGetCredential).toHaveBeenCalled()
})
it('should handle missing credentialId', async () => {
@@ -158,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 () => {
@@ -199,7 +152,7 @@ describe('OAuth Token API Routes', () => {
requesterUserId: 'test-user-id',
credentialOwnerUserId: 'owner-user-id',
})
mockGetCredential.mockResolvedValueOnce(undefined)
authOAuthUtilsMockFns.mockGetCredential.mockResolvedValueOnce(undefined)
const req = createMockRequest('POST', {
credentialId: 'nonexistent-credential-id',
@@ -219,14 +172,16 @@ describe('OAuth Token API Routes', () => {
requesterUserId: 'test-user-id',
credentialOwnerUserId: 'owner-user-id',
})
mockGetCredential.mockResolvedValueOnce({
authOAuthUtilsMockFns.mockGetCredential.mockResolvedValueOnce({
id: 'credential-id',
accessToken: 'test-token',
refreshToken: 'refresh-token',
accessTokenExpiresAt: new Date(Date.now() - 3600 * 1000), // Expired
providerId: 'google',
})
mockRefreshTokenIfNeeded.mockRejectedValueOnce(new Error('Refresh failure'))
authOAuthUtilsMockFns.mockRefreshTokenIfNeeded.mockRejectedValueOnce(
new Error('Refresh failure')
)
const req = createMockRequest('POST', {
credentialId: 'credential-id',
@@ -241,7 +196,7 @@ describe('OAuth Token API Routes', () => {
describe('credentialAccountUserId + providerId path', () => {
it('should reject unauthenticated requests', async () => {
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
success: false,
error: 'Authentication required',
})
@@ -256,11 +211,11 @@ describe('OAuth Token API Routes', () => {
expect(response.status).toBe(401)
expect(data).toHaveProperty('error', 'User not authenticated')
expect(mockGetOAuthToken).not.toHaveBeenCalled()
expect(authOAuthUtilsMockFns.mockGetOAuthToken).not.toHaveBeenCalled()
})
it('should reject internal JWT authentication', async () => {
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
success: true,
authType: 'internal_jwt',
userId: 'test-user-id',
@@ -276,11 +231,11 @@ describe('OAuth Token API Routes', () => {
expect(response.status).toBe(401)
expect(data).toHaveProperty('error', 'User not authenticated')
expect(mockGetOAuthToken).not.toHaveBeenCalled()
expect(authOAuthUtilsMockFns.mockGetOAuthToken).not.toHaveBeenCalled()
})
it('should reject requests for other users credentials', async () => {
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
success: true,
authType: 'session',
userId: 'attacker-user-id',
@@ -296,16 +251,16 @@ describe('OAuth Token API Routes', () => {
expect(response.status).toBe(403)
expect(data).toHaveProperty('error', 'Unauthorized')
expect(mockGetOAuthToken).not.toHaveBeenCalled()
expect(authOAuthUtilsMockFns.mockGetOAuthToken).not.toHaveBeenCalled()
})
it('should allow session-authenticated users to access their own credentials', async () => {
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
success: true,
authType: 'session',
userId: 'test-user-id',
})
mockGetOAuthToken.mockResolvedValueOnce('valid-access-token')
authOAuthUtilsMockFns.mockGetOAuthToken.mockResolvedValueOnce('valid-access-token')
const req = createMockRequest('POST', {
credentialAccountUserId: 'test-user-id',
@@ -317,16 +272,19 @@ describe('OAuth Token API Routes', () => {
expect(response.status).toBe(200)
expect(data).toHaveProperty('accessToken', 'valid-access-token')
expect(mockGetOAuthToken).toHaveBeenCalledWith('test-user-id', 'google')
expect(authOAuthUtilsMockFns.mockGetOAuthToken).toHaveBeenCalledWith(
'test-user-id',
'google'
)
})
it('should return 404 when credential not found for user', async () => {
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
success: true,
authType: 'session',
userId: 'test-user-id',
})
mockGetOAuthToken.mockResolvedValueOnce(null)
authOAuthUtilsMockFns.mockGetOAuthToken.mockResolvedValueOnce(null)
const req = createMockRequest('POST', {
credentialAccountUserId: 'test-user-id',
@@ -353,14 +311,14 @@ describe('OAuth Token API Routes', () => {
requesterUserId: 'test-user-id',
credentialOwnerUserId: 'test-user-id',
})
mockGetCredential.mockResolvedValueOnce({
authOAuthUtilsMockFns.mockGetCredential.mockResolvedValueOnce({
id: 'credential-id',
accessToken: 'test-token',
refreshToken: 'refresh-token',
accessTokenExpiresAt: new Date(Date.now() + 3600 * 1000),
providerId: 'google',
})
mockRefreshTokenIfNeeded.mockResolvedValueOnce({
authOAuthUtilsMockFns.mockRefreshTokenIfNeeded.mockResolvedValueOnce({
accessToken: 'fresh-token',
refreshed: false,
})
@@ -376,8 +334,8 @@ describe('OAuth Token API Routes', () => {
expect(data).toHaveProperty('accessToken', 'fresh-token')
expect(mockAuthorizeCredentialUse).toHaveBeenCalled()
expect(mockGetCredential).toHaveBeenCalled()
expect(mockRefreshTokenIfNeeded).toHaveBeenCalled()
expect(authOAuthUtilsMockFns.mockGetCredential).toHaveBeenCalled()
expect(authOAuthUtilsMockFns.mockRefreshTokenIfNeeded).toHaveBeenCalled()
})
it('should handle missing credentialId', async () => {
@@ -388,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 () => {
@@ -415,7 +372,7 @@ describe('OAuth Token API Routes', () => {
requesterUserId: 'test-user-id',
credentialOwnerUserId: 'test-user-id',
})
mockGetCredential.mockResolvedValueOnce(undefined)
authOAuthUtilsMockFns.mockGetCredential.mockResolvedValueOnce(undefined)
const req = new Request(
'http://localhost:3000/api/auth/oauth/token?credentialId=nonexistent-credential-id'
@@ -435,7 +392,7 @@ describe('OAuth Token API Routes', () => {
requesterUserId: 'test-user-id',
credentialOwnerUserId: 'test-user-id',
})
mockGetCredential.mockResolvedValueOnce({
authOAuthUtilsMockFns.mockGetCredential.mockResolvedValueOnce({
id: 'credential-id',
accessToken: null,
refreshToken: 'refresh-token',
@@ -460,14 +417,16 @@ describe('OAuth Token API Routes', () => {
requesterUserId: 'test-user-id',
credentialOwnerUserId: 'test-user-id',
})
mockGetCredential.mockResolvedValueOnce({
authOAuthUtilsMockFns.mockGetCredential.mockResolvedValueOnce({
id: 'credential-id',
accessToken: 'test-token',
refreshToken: 'refresh-token',
accessTokenExpiresAt: new Date(Date.now() - 3600 * 1000), // Expired
providerId: 'google',
})
mockRefreshTokenIfNeeded.mockRejectedValueOnce(new Error('Refresh failure'))
authOAuthUtilsMockFns.mockRefreshTokenIfNeeded.mockRejectedValueOnce(
new Error('Refresh failure')
)
const req = new Request(
'http://localhost:3000/api/auth/oauth/token?credentialId=credential-id'

View File

@@ -4,18 +4,13 @@
* @vitest-environment node
*/
import { databaseMock, loggerMock } from '@sim/testing'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('@sim/db', () => databaseMock)
vi.mock('@/lib/oauth/oauth', () => ({
refreshOAuthToken: vi.fn(),
OAUTH_PROVIDERS: {},
}))
vi.mock('@sim/logger', () => loggerMock)
import { db } from '@sim/db'
import { refreshOAuthToken } from '@/lib/oauth'
import {

View File

@@ -2,9 +2,9 @@ import { createSign } from 'crypto'
import { db } from '@sim/db'
import { account, credential, credentialSetMember } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { and, desc, eq, inArray } from 'drizzle-orm'
import { decryptSecret } from '@/lib/core/security/encryption'
import { toError } from '@/lib/core/utils/helpers'
import { refreshOAuthToken } from '@/lib/oauth'
import {
getMicrosoftRefreshTokenExpiry,

View File

@@ -1,9 +1,9 @@
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { env } from '@/lib/core/config/env'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { generateId } from '@/lib/core/utils/uuid'
import { isSameOrigin } from '@/lib/core/utils/validation'
import { getScopesForService } from '@/lib/oauth/utils'

View File

@@ -1,9 +1,9 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { headers } from 'next/headers'
import { NextResponse } from 'next/server'
import { auth } from '@/lib/auth'
import { isAuthDisabled } from '@/lib/core/config/feature-flags'
import { toError } from '@/lib/core/utils/helpers'
const logger = createLogger('SocketTokenAPI')

View File

@@ -7,8 +7,6 @@ import { getSession } from '@/lib/auth'
import { getEffectiveBillingStatus } from '@/lib/billing/core/access'
import { getSimplifiedBillingSummary } from '@/lib/billing/core/billing'
import { getOrganizationBillingData } from '@/lib/billing/core/organization'
import { dollarsToCredits } from '@/lib/billing/credits/conversion'
import { getPlanTierCredits } from '@/lib/billing/plan-helpers'
const logger = createLogger('UnifiedBillingAPI')
@@ -47,7 +45,20 @@ export async function GET(request: NextRequest) {
let billingData
if (context === 'user') {
// Get user billing and billing blocked status in parallel
if (contextId) {
const membership = await db
.select({ role: member.role })
.from(member)
.where(and(eq(member.organizationId, contextId), eq(member.userId, session.user.id)))
.limit(1)
if (membership.length === 0) {
return NextResponse.json(
{ error: 'Access denied - not a member of this organization' },
{ status: 403 }
)
}
}
const [billingResult, billingStatus] = await Promise.all([
getSimplifiedBillingSummary(session.user.id, contextId || undefined),
getEffectiveBillingStatus(session.user.id),
@@ -107,7 +118,6 @@ export async function GET(request: NextRequest) {
)
}
// Transform data to match component expectations
billingData = {
organizationId: rawBillingData.organizationId,
organizationName: rawBillingData.organizationName,
@@ -122,17 +132,10 @@ export async function GET(request: NextRequest) {
averageUsagePerMember: rawBillingData.averageUsagePerMember,
billingPeriodStart: rawBillingData.billingPeriodStart?.toISOString() || null,
billingPeriodEnd: rawBillingData.billingPeriodEnd?.toISOString() || null,
tierCredits: getPlanTierCredits(rawBillingData.subscriptionPlan),
totalCurrentUsageCredits: dollarsToCredits(rawBillingData.totalCurrentUsage),
totalUsageLimitCredits: dollarsToCredits(rawBillingData.totalUsageLimit),
minimumBillingAmountCredits: dollarsToCredits(rawBillingData.minimumBillingAmount),
averageUsagePerMemberCredits: dollarsToCredits(rawBillingData.averageUsagePerMember),
members: rawBillingData.members.map((m) => ({
...m,
joinedAt: m.joinedAt.toISOString(),
lastActive: m.lastActive?.toISOString() || null,
currentUsageCredits: dollarsToCredits(m.currentUsage),
usageLimitCredits: dollarsToCredits(m.usageLimit),
})),
}

View File

@@ -1,6 +1,7 @@
import { db } from '@sim/db'
import { subscription as subscriptionTable } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
@@ -9,15 +10,15 @@ import { getEffectiveBillingStatus } from '@/lib/billing/core/access'
import { isOrganizationOwnerOrAdmin } from '@/lib/billing/core/organization'
import { getHighestPrioritySubscription } from '@/lib/billing/core/plan'
import { writeBillingInterval } from '@/lib/billing/core/subscription'
import { getPlanType, isEnterprise, isOrgPlan } from '@/lib/billing/plan-helpers'
import { getPlanType, isEnterprise } from '@/lib/billing/plan-helpers'
import { getPlanByName } from '@/lib/billing/plans'
import { requireStripeClient } from '@/lib/billing/stripe-client'
import {
hasUsableSubscriptionAccess,
hasUsableSubscriptionStatus,
isOrgScopedSubscription,
} from '@/lib/billing/subscriptions/utils'
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
import { toError } from '@/lib/core/utils/helpers'
import { captureServerEvent } from '@/lib/posthog/server'
const logger = createLogger('SwitchPlan')
@@ -93,7 +94,7 @@ export async function POST(request: NextRequest) {
)
}
if (isOrgPlan(sub.plan)) {
if (isOrgScopedSubscription(sub, userId)) {
const hasPermission = await isOrganizationOwnerOrAdmin(userId, sub.referenceId)
if (!hasPermission) {
return NextResponse.json({ error: 'Only team admins can change the plan' }, { status: 403 })

View File

@@ -1,4 +1,5 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
@@ -7,7 +8,6 @@ import { checkAndBillOverageThreshold } from '@/lib/billing/threshold-billing'
import { checkInternalApiKey } from '@/lib/copilot/request/http'
import { isBillingEnabled } from '@/lib/core/config/feature-flags'
import { type AtomicClaimResult, billingIdempotency } from '@/lib/core/idempotency/service'
import { toError } from '@/lib/core/utils/helpers'
import { generateRequestId } from '@/lib/core/utils/request'
const logger = createLogger('BillingUpdateCostAPI')

View File

@@ -3,6 +3,13 @@
*
* @vitest-environment node
*/
import {
redisConfigMock,
redisConfigMockFns,
requestUtilsMockFns,
workflowsApiUtilsMock,
workflowsApiUtilsMockFns,
} from '@sim/testing'
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@@ -12,7 +19,6 @@ const {
mockRedisDel,
mockRedisTtl,
mockRedisEval,
mockGetRedisClient,
mockRedisClient,
mockDbSelect,
mockDbInsert,
@@ -21,10 +27,7 @@ const {
mockSendEmail,
mockRenderOTPEmail,
mockAddCorsHeaders,
mockCreateSuccessResponse,
mockCreateErrorResponse,
mockSetChatAuthCookie,
mockGenerateRequestId,
mockGetStorageMethod,
mockZodParse,
mockGetEnv,
@@ -41,7 +44,6 @@ const {
ttl: mockRedisTtl,
eval: mockRedisEval,
}
const mockGetRedisClient = vi.fn()
const mockDbSelect = vi.fn()
const mockDbInsert = vi.fn()
const mockDbDelete = vi.fn()
@@ -49,10 +51,7 @@ const {
const mockSendEmail = vi.fn()
const mockRenderOTPEmail = vi.fn()
const mockAddCorsHeaders = vi.fn()
const mockCreateSuccessResponse = vi.fn()
const mockCreateErrorResponse = vi.fn()
const mockSetChatAuthCookie = vi.fn()
const mockGenerateRequestId = vi.fn()
const mockGetStorageMethod = vi.fn()
const mockZodParse = vi.fn()
const mockGetEnv = vi.fn()
@@ -63,7 +62,6 @@ const {
mockRedisDel,
mockRedisTtl,
mockRedisEval,
mockGetRedisClient,
mockRedisClient,
mockDbSelect,
mockDbInsert,
@@ -72,19 +70,18 @@ const {
mockSendEmail,
mockRenderOTPEmail,
mockAddCorsHeaders,
mockCreateSuccessResponse,
mockCreateErrorResponse,
mockSetChatAuthCookie,
mockGenerateRequestId,
mockGetStorageMethod,
mockZodParse,
mockGetEnv,
}
})
vi.mock('@/lib/core/config/redis', () => ({
getRedisClient: mockGetRedisClient,
}))
const mockGetRedisClient = redisConfigMockFns.mockGetRedisClient
const mockCreateSuccessResponse = workflowsApiUtilsMockFns.mockCreateSuccessResponse
const mockCreateErrorResponse = workflowsApiUtilsMockFns.mockCreateErrorResponse
vi.mock('@/lib/core/config/redis', () => redisConfigMock)
vi.mock('@sim/db', () => ({
db: {
@@ -103,26 +100,6 @@ vi.mock('@sim/db', () => ({
},
}))
vi.mock('@sim/db/schema', () => ({
chat: {
id: 'id',
identifier: 'identifier',
authType: 'authType',
allowedEmails: 'allowedEmails',
title: 'title',
isActive: 'isActive',
archivedAt: 'archivedAt',
},
verification: {
id: 'id',
identifier: 'identifier',
value: 'value',
expiresAt: 'expiresAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
},
}))
vi.mock('drizzle-orm', () => ({
eq: vi.fn((field: string, value: string) => ({ field, value, type: 'eq' })),
and: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'and' })),
@@ -160,19 +137,7 @@ vi.mock('@/app/api/chat/utils', () => ({
setChatAuthCookie: mockSetChatAuthCookie,
}))
vi.mock('@/app/api/workflows/utils', () => ({
createSuccessResponse: mockCreateSuccessResponse,
createErrorResponse: mockCreateErrorResponse,
}))
vi.mock('@sim/logger', () => ({
createLogger: vi.fn().mockReturnValue({
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
}),
}))
vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock)
vi.mock('@/lib/core/config/env', () => ({
env: {
@@ -207,10 +172,6 @@ vi.mock('zod', () => {
}
})
vi.mock('@/lib/core/utils/request', () => ({
generateRequestId: mockGenerateRequestId,
}))
import { POST, PUT } from './route'
describe('Chat OTP API Route', () => {
@@ -272,7 +233,7 @@ describe('Chat OTP API Route', () => {
status,
}))
mockGenerateRequestId.mockReturnValue('req-123')
requestUtilsMockFns.mockGenerateRequestId.mockReturnValue('req-123')
mockZodParse.mockImplementation((data: unknown) => data)

View File

@@ -2,6 +2,7 @@ import { randomInt } from 'crypto'
import { db } from '@sim/db'
import { chat, verification } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq, gt, isNull } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { z } from 'zod'
@@ -10,7 +11,6 @@ import { getRedisClient } from '@/lib/core/config/redis'
import { addCorsHeaders, isEmailAllowed } from '@/lib/core/security/deployment'
import { getStorageMethod } from '@/lib/core/storage'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import { sendEmail } from '@/lib/messaging/email/mailer'
import { setChatAuthCookie } from '@/app/api/chat/utils'
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
@@ -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

View File

@@ -3,8 +3,17 @@
*
* @vitest-environment node
*/
import { loggerMock, requestUtilsMock } from '@sim/testing'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
dbChainMock,
dbChainMockFns,
encryptionMock,
executionPreprocessingMock,
executionPreprocessingMockFns,
loggingSessionMock,
workflowsApiUtilsMock,
workflowsApiUtilsMockFns,
} from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
/**
* Creates a mock NextRequest with cookies support for testing.
@@ -51,38 +60,19 @@ const createMockStream = () => {
})
}
const {
mockDbSelect,
mockAddCorsHeaders,
mockValidateChatAuth,
mockSetChatAuthCookie,
mockValidateAuthToken,
mockCreateErrorResponse,
mockCreateSuccessResponse,
} = 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),
mockCreateErrorResponse: vi
.fn()
.mockImplementation((message: string, status: number, code?: string) => {
return new Response(
JSON.stringify({
error: code || 'Error',
message,
}),
{ status }
)
}),
mockCreateSuccessResponse: vi.fn().mockImplementation((data: unknown) => {
return new Response(JSON.stringify(data), { status: 200 })
}),
}))
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: {},
}))
@@ -99,42 +89,11 @@ vi.mock('@/app/api/chat/utils', () => ({
setChatAuthCookie: mockSetChatAuthCookie,
}))
vi.mock('@sim/logger', () => loggerMock)
vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock)
vi.mock('@/app/api/workflows/utils', () => ({
createErrorResponse: mockCreateErrorResponse,
createSuccessResponse: mockCreateSuccessResponse,
}))
vi.mock('@/lib/execution/preprocessing', () => executionPreprocessingMock)
vi.mock('@/lib/execution/preprocessing', () => ({
preprocessExecution: vi.fn().mockResolvedValue({
success: true,
actorUserId: 'test-user-id',
workflowRecord: {
id: 'test-workflow-id',
userId: 'test-user-id',
isDeployed: true,
workspaceId: 'test-workspace-id',
variables: {},
},
userSubscription: {
plan: 'pro',
status: 'active',
},
rateLimitInfo: {
allowed: true,
remaining: 100,
resetAt: new Date(),
},
}),
}))
vi.mock('@/lib/logs/execution/logging-session', () => ({
LoggingSession: vi.fn().mockImplementation(() => ({
safeStart: vi.fn().mockResolvedValue(undefined),
safeCompleteWithError: vi.fn().mockResolvedValue(undefined),
})),
}))
vi.mock('@/lib/logs/execution/logging-session', () => loggingSessionMock)
vi.mock('@/lib/workflows/streaming/streaming', () => ({
createStreamingResponse: vi.fn().mockImplementation(async () => createMockStream()),
@@ -153,11 +112,7 @@ vi.mock('@/lib/core/utils/sse', () => ({
},
}))
vi.mock('@/lib/core/utils/request', () => requestUtilsMock)
vi.mock('@/lib/core/security/encryption', () => ({
decryptSecret: vi.fn().mockResolvedValue({ decrypted: 'test-password' }),
}))
vi.mock('@/lib/core/security/encryption', () => encryptionMock)
import { preprocessExecution } from '@/lib/execution/preprocessing'
import { createStreamingResponse } from '@/lib/workflows/streaming/streaming'
@@ -202,6 +157,27 @@ describe('Chat Identifier API Route', () => {
beforeEach(() => {
vi.clearAllMocks()
executionPreprocessingMockFns.mockPreprocessExecution.mockResolvedValue({
success: true,
actorUserId: 'test-user-id',
workflowRecord: {
id: 'test-workflow-id',
userId: 'test-user-id',
isDeployed: true,
workspaceId: 'test-workspace-id',
variables: {},
},
userSubscription: {
plan: 'pro',
status: 'active',
},
rateLimitInfo: {
allowed: true,
remaining: 100,
resetAt: new Date(),
},
})
mockAddCorsHeaders.mockImplementation((response: Response) => response)
mockValidateChatAuth.mockResolvedValue({ authorized: true })
mockValidateAuthToken.mockReturnValue(false)
@@ -218,7 +194,7 @@ describe('Chat Identifier API Route', () => {
return new Response(JSON.stringify(data), { status: 200 })
})
mockDbSelect.mockImplementation((fields: Record<string, unknown>) => {
dbChainMockFns.select.mockImplementation((fields: Record<string, unknown>) => {
if (fields && fields.isDeployed !== undefined) {
return {
from: vi.fn().mockReturnValue({
@@ -238,10 +214,6 @@ describe('Chat Identifier API Route', () => {
})
})
afterEach(() => {
vi.clearAllMocks()
})
describe('GET endpoint', () => {
it('should return chat info for a valid identifier', async () => {
const req = createMockNextRequest('GET')
@@ -260,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({
@@ -283,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({
@@ -331,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' })
@@ -340,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()
})

View File

@@ -1,12 +1,12 @@
import { db } from '@sim/db'
import { chat, workflow } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq, isNull } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { addCorsHeaders, validateAuthToken } from '@/lib/core/security/deployment'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import { preprocessExecution } from '@/lib/execution/preprocessing'
import { LoggingSession } from '@/lib/logs/execution/logging-session'
import { ChatFiles } from '@/lib/uploads'
@@ -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(

View File

@@ -3,52 +3,35 @@
*
* @vitest-environment node
*/
import { auditMock } from '@sim/testing'
import {
auditMock,
authMockFns,
dbChainMock,
dbChainMockFns,
encryptionMock,
encryptionMockFns,
resetDbChainMock,
workflowsApiUtilsMock,
workflowsApiUtilsMockFns,
workflowsOrchestrationMock,
workflowsOrchestrationMockFns,
workflowsPersistenceUtilsMock,
workflowsPersistenceUtilsMockFns,
} from '@sim/testing'
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
mockGetSession,
mockSelect,
mockFrom,
mockWhere,
mockLimit,
mockUpdate,
mockSet,
mockCreateSuccessResponse,
mockCreateErrorResponse,
mockEncryptSecret,
mockCheckChatAccess,
mockDeployWorkflow,
mockPerformChatUndeploy,
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 {
mockGetSession: vi.fn(),
mockSelect: vi.fn(),
mockFrom: vi.fn(),
mockWhere: vi.fn(),
mockLimit: vi.fn(),
mockUpdate: vi.fn(),
mockSet: vi.fn(),
mockCreateSuccessResponse: vi.fn(),
mockCreateErrorResponse: vi.fn(),
mockEncryptSecret: vi.fn(),
mockCheckChatAccess: vi.fn(),
mockDeployWorkflow: vi.fn(),
mockPerformChatUndeploy: vi.fn(),
mockLogger: logger,
}
})
const { mockCheckChatAccess } = vi.hoisted(() => ({
mockCheckChatAccess: vi.fn(),
}))
const mockCreateSuccessResponse = workflowsApiUtilsMockFns.mockCreateSuccessResponse
const mockCreateErrorResponse = workflowsApiUtilsMockFns.mockCreateErrorResponse
const mockEncryptSecret = encryptionMockFns.mockEncryptSecret
const mockDeployWorkflow = workflowsPersistenceUtilsMockFns.mockDeployWorkflow
const mockPerformChatUndeploy = workflowsOrchestrationMockFns.mockPerformChatUndeploy
const mockNotifySocketDeploymentChanged =
workflowsOrchestrationMockFns.mockNotifySocketDeploymentChanged
vi.mock('@/lib/audit/log', () => auditMock)
vi.mock('@/lib/core/config/feature-flags', () => ({
@@ -56,59 +39,24 @@ vi.mock('@/lib/core/config/feature-flags', () => ({
isHosted: false,
isProd: false,
}))
vi.mock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
vi.mock('@sim/logger', () => ({
createLogger: vi.fn().mockReturnValue(mockLogger),
}))
vi.mock('@sim/db', () => ({
db: {
select: mockSelect,
update: mockUpdate,
},
}))
vi.mock('@sim/db/schema', () => ({
chat: { id: 'id', identifier: 'identifier', userId: 'userId', archivedAt: 'archivedAt' },
}))
vi.mock('@/app/api/workflows/utils', () => ({
createSuccessResponse: mockCreateSuccessResponse,
createErrorResponse: mockCreateErrorResponse,
}))
vi.mock('@/lib/core/security/encryption', () => ({
encryptSecret: mockEncryptSecret,
}))
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', () => ({
getEmailDomain: vi.fn().mockReturnValue('localhost:3000'),
}))
vi.mock('@/app/api/chat/utils', () => ({
checkChatAccess: mockCheckChatAccess,
}))
vi.mock('@/lib/workflows/persistence/utils', () => ({
deployWorkflow: mockDeployWorkflow,
}))
vi.mock('@/lib/workflows/orchestration', () => ({
performChatUndeploy: mockPerformChatUndeploy,
notifySocketDeploymentChanged: vi.fn().mockResolvedValue(undefined),
}))
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('@/lib/workflows/persistence/utils', () => workflowsPersistenceUtilsMock)
vi.mock('@/lib/workflows/orchestration', () => workflowsOrchestrationMock)
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) => {
@@ -126,15 +74,12 @@ describe('Chat Edit API Route', () => {
mockEncryptSecret.mockResolvedValue({ encrypted: 'encrypted-password' })
mockDeployWorkflow.mockResolvedValue({ success: true, version: 1 })
})
afterEach(() => {
vi.clearAllMocks()
mockNotifySocketDeploymentChanged.mockResolvedValue(undefined)
})
describe('GET', () => {
it('should return 401 when user is not authenticated', async () => {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123')
const response = await GET(req, { params: Promise.resolve({ id: 'chat-123' }) })
@@ -145,7 +90,7 @@ describe('Chat Edit API Route', () => {
})
it('should return 404 when chat not found or access denied', async () => {
mockGetSession.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-id' },
})
@@ -161,7 +106,7 @@ describe('Chat Edit API Route', () => {
})
it('should return chat details when user has access', async () => {
mockGetSession.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-id' },
})
@@ -191,7 +136,7 @@ describe('Chat Edit API Route', () => {
describe('PATCH', () => {
it('should return 401 when user is not authenticated', async () => {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
method: 'PATCH',
@@ -205,7 +150,7 @@ describe('Chat Edit API Route', () => {
})
it('should return 404 when chat not found or access denied', async () => {
mockGetSession.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-id' },
})
@@ -224,7 +169,7 @@ describe('Chat Edit API Route', () => {
})
it('should update chat when user has access', async () => {
mockGetSession.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-id' },
})
@@ -249,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')
@@ -257,7 +202,7 @@ describe('Chat Edit API Route', () => {
})
it('should handle identifier conflicts', async () => {
mockGetSession.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-id' },
})
@@ -270,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',
@@ -286,7 +231,7 @@ describe('Chat Edit API Route', () => {
})
it('should validate password requirement for password auth', async () => {
mockGetSession.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-id' },
})
@@ -313,7 +258,7 @@ describe('Chat Edit API Route', () => {
})
it('should keep the existing password when updating a password-protected chat', async () => {
mockGetSession.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-id' },
})
@@ -340,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: [],
@@ -348,12 +293,12 @@ describe('Chat Edit API Route', () => {
})
)
const updatePayload = mockSet.mock.calls[0]?.[0]
const updatePayload = dbChainMockFns.set.mock.calls[0]?.[0]
expect(updatePayload.password).toBeUndefined()
})
it('should allow access when user has workspace admin permission', async () => {
mockGetSession.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'admin-user-id' },
})
@@ -384,7 +329,7 @@ describe('Chat Edit API Route', () => {
describe('DELETE', () => {
it('should return 401 when user is not authenticated', async () => {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
const req = new NextRequest('http://localhost:3000/api/chat/manage/chat-123', {
method: 'DELETE',
@@ -397,7 +342,7 @@ describe('Chat Edit API Route', () => {
})
it('should return 404 when chat not found or access denied', async () => {
mockGetSession.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-id' },
})
@@ -415,7 +360,7 @@ describe('Chat Edit API Route', () => {
})
it('should delete chat when user has access', async () => {
mockGetSession.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-id' },
})
@@ -441,7 +386,7 @@ describe('Chat Edit API Route', () => {
})
it('should allow deletion when user has workspace admin permission', async () => {
mockGetSession.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'admin-user-id' },
})

View File

@@ -3,65 +3,36 @@
*
* @vitest-environment node
*/
import { createEnvMock } from '@sim/testing'
import {
authMockFns,
createEnvMock,
dbChainMock,
dbChainMockFns,
workflowsApiUtilsMock,
workflowsApiUtilsMockFns,
workflowsOrchestrationMock,
workflowsOrchestrationMockFns,
} from '@sim/testing'
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
mockSelect,
mockFrom,
mockWhere,
mockLimit,
mockCreateSuccessResponse,
mockCreateErrorResponse,
mockCheckWorkflowAccessForChatCreation,
mockPerformChatDeploy,
mockGetSession,
} = vi.hoisted(() => ({
mockSelect: vi.fn(),
mockFrom: vi.fn(),
mockWhere: vi.fn(),
mockLimit: vi.fn(),
mockCreateSuccessResponse: vi.fn(),
mockCreateErrorResponse: vi.fn(),
const { mockCheckWorkflowAccessForChatCreation } = vi.hoisted(() => ({
mockCheckWorkflowAccessForChatCreation: vi.fn(),
mockPerformChatDeploy: vi.fn(),
mockGetSession: vi.fn(),
}))
vi.mock('@sim/db', () => ({
db: {
select: mockSelect,
},
}))
const mockCreateSuccessResponse = workflowsApiUtilsMockFns.mockCreateSuccessResponse
const mockCreateErrorResponse = workflowsApiUtilsMockFns.mockCreateErrorResponse
const mockPerformChatDeploy = workflowsOrchestrationMockFns.mockPerformChatDeploy
vi.mock('@sim/db/schema', () => ({
chat: { userId: 'userId', identifier: 'identifier', archivedAt: 'archivedAt' },
workflow: { id: 'id', userId: 'userId', isDeployed: 'isDeployed' },
}))
vi.mock('@sim/db', () => dbChainMock)
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('@/app/api/workflows/utils', () => ({
createSuccessResponse: mockCreateSuccessResponse,
createErrorResponse: mockCreateErrorResponse,
}))
vi.mock('@/app/api/workflows/utils', () => workflowsApiUtilsMock)
vi.mock('@/app/api/chat/utils', () => ({
checkWorkflowAccessForChatCreation: mockCheckWorkflowAccessForChatCreation,
}))
vi.mock('@/lib/workflows/orchestration', () => ({
performChatDeploy: mockPerformChatDeploy,
}))
vi.mock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
vi.mock('@/lib/workflows/orchestration', () => workflowsOrchestrationMock)
vi.mock('@/lib/core/config/env', () =>
createEnvMock({
@@ -76,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,
@@ -101,13 +68,9 @@ describe('Chat API Route', () => {
})
})
afterEach(() => {
vi.clearAllMocks()
})
describe('GET', () => {
it('should return 401 when user is not authenticated', async () => {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
const req = new NextRequest('http://localhost:3000/api/chat')
const response = await GET(req)
@@ -117,27 +80,27 @@ describe('Chat API Route', () => {
})
it('should return chat deployments for authenticated user', async () => {
mockGetSession.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-id' },
})
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 () => {
mockGetSession.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({
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)
@@ -149,7 +112,7 @@ describe('Chat API Route', () => {
describe('POST', () => {
it('should return 401 when user is not authenticated', async () => {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
const req = new NextRequest('http://localhost:3000/api/chat', {
method: 'POST',
@@ -162,7 +125,7 @@ describe('Chat API Route', () => {
})
it('should validate request data', async () => {
mockGetSession.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-id' },
})
@@ -178,7 +141,7 @@ describe('Chat API Route', () => {
})
it('should reject if identifier already exists', async () => {
mockGetSession.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-id' },
})
@@ -192,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 +169,7 @@ describe('Chat API Route', () => {
})
it('should reject if workflow not found', async () => {
mockGetSession.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-id' },
})
@@ -220,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 +200,7 @@ describe('Chat API Route', () => {
})
it('should allow chat deployment when user owns workflow directly', async () => {
mockGetSession.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-id', email: 'user@example.com' },
})
@@ -251,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 },
@@ -275,7 +238,7 @@ describe('Chat API Route', () => {
})
it('passes chat customizations and outputConfigs through in the API request shape', async () => {
mockGetSession.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-id', email: 'user@example.com' },
})
@@ -291,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 +282,7 @@ describe('Chat API Route', () => {
})
it('should allow chat deployment when user has workspace admin permission', async () => {
mockGetSession.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-id', email: 'user@example.com' },
})
@@ -333,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 +319,7 @@ describe('Chat API Route', () => {
})
it('should reject when workflow is in workspace but user lacks admin permission', async () => {
mockGetSession.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-id' },
})
@@ -370,7 +333,7 @@ describe('Chat API Route', () => {
},
}
mockLimit.mockResolvedValueOnce([]) // Identifier is available
dbChainMockFns.limit.mockResolvedValueOnce([]) // Identifier is available
mockCheckWorkflowAccessForChatCreation.mockResolvedValue({
hasAccess: false,
})
@@ -390,7 +353,7 @@ describe('Chat API Route', () => {
})
it('should handle workspace permission check errors gracefully', async () => {
mockGetSession.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-id' },
})
@@ -404,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 +381,7 @@ describe('Chat API Route', () => {
})
it('should call performChatDeploy for undeployed workflow', async () => {
mockGetSession.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-id', email: 'user@example.com' },
})
@@ -432,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 },

View File

@@ -3,12 +3,16 @@
*
* @vitest-environment node
*/
import { databaseMock, loggerMock, requestUtilsMock } from '@sim/testing'
import {
encryptionMock,
encryptionMockFns,
loggingSessionMock,
workflowsUtilsMock,
} from '@sim/testing'
import type { NextResponse } from 'next/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
mockDecryptSecret,
mockMergeSubblockStateWithValues,
mockMergeSubBlockValues,
mockValidateAuthToken,
@@ -16,7 +20,6 @@ const {
mockAddCorsHeaders,
mockIsEmailAllowed,
} = vi.hoisted(() => ({
mockDecryptSecret: vi.fn(),
mockMergeSubblockStateWithValues: vi.fn().mockReturnValue({}),
mockMergeSubBlockValues: vi.fn().mockReturnValue({}),
mockValidateAuthToken: vi.fn().mockReturnValue(false),
@@ -25,16 +28,9 @@ const {
mockIsEmailAllowed: vi.fn(),
}))
vi.mock('@sim/db', () => databaseMock)
vi.mock('@sim/logger', () => loggerMock)
const mockDecryptSecret = encryptionMockFns.mockDecryptSecret
vi.mock('@/lib/logs/execution/logging-session', () => ({
LoggingSession: vi.fn().mockImplementation(() => ({
safeStart: vi.fn().mockResolvedValue(undefined),
safeComplete: vi.fn().mockResolvedValue(undefined),
safeCompleteWithError: vi.fn().mockResolvedValue(undefined),
})),
}))
vi.mock('@/lib/logs/execution/logging-session', () => loggingSessionMock)
vi.mock('@/executor', () => ({
Executor: vi.fn(),
@@ -49,11 +45,7 @@ vi.mock('@/lib/workflows/subblocks', () => ({
mergeSubBlockValues: mockMergeSubBlockValues,
}))
vi.mock('@/lib/core/security/encryption', () => ({
decryptSecret: mockDecryptSecret,
}))
vi.mock('@/lib/core/utils/request', () => requestUtilsMock)
vi.mock('@/lib/core/security/encryption', () => encryptionMock)
vi.mock('@/lib/core/security/deployment', () => ({
validateAuthToken: mockValidateAuthToken,
@@ -68,9 +60,7 @@ vi.mock('@/lib/core/config/feature-flags', () => ({
isProd: false,
}))
vi.mock('@/lib/workflows/utils', () => ({
authorizeWorkflowByWorkspacePermission: vi.fn(),
}))
vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock)
import { decryptSecret } from '@/lib/core/security/encryption'
import { setChatAuthCookie, validateChatAuth } from '@/app/api/chat/utils'

View File

@@ -3,33 +3,20 @@
*
* @vitest-environment node
*/
import { authMockFns, createEnvMock } from '@sim/testing'
import { NextRequest } from 'next/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockGetSession, mockFetch } = vi.hoisted(() => ({
mockGetSession: vi.fn(),
const { mockFetch } = vi.hoisted(() => ({
mockFetch: vi.fn(),
}))
vi.mock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
vi.mock('@/lib/copilot/constants', () => ({
SIM_AGENT_API_URL_DEFAULT: 'https://agent.sim.example.com',
SIM_AGENT_API_URL: 'https://agent.sim.example.com',
}))
vi.mock('@/lib/core/config/env', () => ({
env: {
COPILOT_API_KEY: 'test-api-key',
},
getEnv: vi.fn(),
isTruthy: (value: string | boolean | number | undefined) =>
typeof value === 'string' ? value.toLowerCase() === 'true' || value === '1' : Boolean(value),
isFalsy: (value: string | boolean | number | undefined) =>
typeof value === 'string' ? value.toLowerCase() === 'false' || value === '0' : value === false,
}))
vi.mock('@/lib/core/config/env', () => createEnvMock({ COPILOT_API_KEY: 'test-api-key' }))
import { DELETE, GET } from '@/app/api/copilot/api-keys/route'
@@ -41,7 +28,7 @@ describe('Copilot API Keys API Route', () => {
describe('GET', () => {
it('should return 401 when user is not authenticated', async () => {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
const request = new NextRequest('http://localhost:3000/api/copilot/api-keys')
const response = await GET(request)
@@ -52,7 +39,9 @@ describe('Copilot API Keys API Route', () => {
})
it('should return list of API keys with masked values', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
const mockApiKeys = [
{
@@ -90,7 +79,9 @@ describe('Copilot API Keys API Route', () => {
})
it('should return empty array when user has no API keys', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
mockFetch.mockResolvedValueOnce({
ok: true,
@@ -106,7 +97,9 @@ describe('Copilot API Keys API Route', () => {
})
it('should forward userId to Sim Agent', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
mockFetch.mockResolvedValueOnce({
ok: true,
@@ -130,7 +123,9 @@ describe('Copilot API Keys API Route', () => {
})
it('should return error when Sim Agent returns non-ok response', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
mockFetch.mockResolvedValueOnce({
ok: false,
@@ -147,7 +142,9 @@ describe('Copilot API Keys API Route', () => {
})
it('should return 500 when Sim Agent returns invalid response', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
mockFetch.mockResolvedValueOnce({
ok: true,
@@ -163,7 +160,9 @@ describe('Copilot API Keys API Route', () => {
})
it('should handle network errors gracefully', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
mockFetch.mockRejectedValueOnce(new Error('Network error'))
@@ -176,7 +175,9 @@ describe('Copilot API Keys API Route', () => {
})
it('should handle API keys with empty apiKey string', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
const mockApiKeys = [
{
@@ -202,7 +203,9 @@ describe('Copilot API Keys API Route', () => {
})
it('should handle JSON parsing errors from Sim Agent', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
mockFetch.mockResolvedValueOnce({
ok: true,
@@ -220,7 +223,7 @@ describe('Copilot API Keys API Route', () => {
describe('DELETE', () => {
it('should return 401 when user is not authenticated', async () => {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
const request = new NextRequest('http://localhost:3000/api/copilot/api-keys?id=key-123')
const response = await DELETE(request)
@@ -231,7 +234,9 @@ describe('Copilot API Keys API Route', () => {
})
it('should return 400 when id parameter is missing', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
const request = new NextRequest('http://localhost:3000/api/copilot/api-keys')
const response = await DELETE(request)
@@ -242,7 +247,9 @@ describe('Copilot API Keys API Route', () => {
})
it('should successfully delete an API key', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
mockFetch.mockResolvedValueOnce({
ok: true,
@@ -270,7 +277,9 @@ describe('Copilot API Keys API Route', () => {
})
it('should return error when Sim Agent returns non-ok response', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
mockFetch.mockResolvedValueOnce({
ok: false,
@@ -287,7 +296,9 @@ describe('Copilot API Keys API Route', () => {
})
it('should return 500 when Sim Agent returns invalid response', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
mockFetch.mockResolvedValueOnce({
ok: true,
@@ -303,7 +314,9 @@ describe('Copilot API Keys API Route', () => {
})
it('should handle network errors gracefully', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
mockFetch.mockRejectedValueOnce(new Error('Network error'))
@@ -316,7 +329,9 @@ describe('Copilot API Keys API Route', () => {
})
it('should handle JSON parsing errors from Sim Agent on delete', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
mockFetch.mockResolvedValueOnce({
ok: true,

View File

@@ -1,11 +1,11 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { NextResponse } from 'next/server'
import { getLatestRunForStream } from '@/lib/copilot/async-runs/repository'
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
import { authenticateCopilotRequestSessionOnly } from '@/lib/copilot/request/http'
import { abortActiveStream, waitForPendingChatStream } from '@/lib/copilot/request/session'
import { env } from '@/lib/core/config/env'
import { toError } from '@/lib/core/utils/helpers'
const logger = createLogger('CopilotChatAbortAPI')
const GO_EXPLICIT_ABORT_TIMEOUT_MS = 3000

View File

@@ -3,38 +3,15 @@
*
* @vitest-environment node
*/
import { authMockFns, dbChainMock, dbChainMockFns } from '@sim/testing'
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { mockDelete, mockWhere, mockGetSession, mockGetAccessibleCopilotChat } = vi.hoisted(() => ({
mockDelete: vi.fn(),
mockWhere: vi.fn(),
mockGetSession: vi.fn(),
const { mockGetAccessibleCopilotChat } = vi.hoisted(() => ({
mockGetAccessibleCopilotChat: vi.fn(),
}))
vi.mock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
vi.mock('@sim/db', () => ({
db: {
delete: mockDelete,
},
}))
vi.mock('@sim/db/schema', () => ({
copilotChats: {
id: 'id',
userId: 'userId',
workspaceId: 'workspaceId',
},
}))
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,
@@ -58,11 +35,9 @@ describe('Copilot Chat Delete API Route', () => {
beforeEach(() => {
vi.clearAllMocks()
mockGetSession.mockResolvedValue(null)
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' })
})
@@ -72,7 +47,7 @@ describe('Copilot Chat Delete API Route', () => {
describe('DELETE', () => {
it('should return 401 when user is not authenticated', async () => {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
const req = createMockRequest('DELETE', {
chatId: 'chat-123',
@@ -86,7 +61,7 @@ describe('Copilot Chat Delete API Route', () => {
})
it('should successfully delete a chat', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
const req = createMockRequest('DELETE', {
chatId: 'chat-123',
@@ -98,12 +73,12 @@ 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 () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
const req = createMockRequest('DELETE', {})
@@ -115,7 +90,7 @@ describe('Copilot Chat Delete API Route', () => {
})
it('should return 500 for invalid request body - chatId is not a string', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
const req = createMockRequest('DELETE', {
chatId: 12345,
@@ -129,9 +104,9 @@ describe('Copilot Chat Delete API Route', () => {
})
it('should handle database errors gracefully', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
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',
@@ -145,7 +120,7 @@ describe('Copilot Chat Delete API Route', () => {
})
it('should handle JSON parsing errors in request body', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
const req = new NextRequest('http://localhost:3000/api/copilot/chat/delete', {
method: 'DELETE',
@@ -163,7 +138,7 @@ describe('Copilot Chat Delete API Route', () => {
})
it('should delete chat even if it does not exist (idempotent)', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
mockGetAccessibleCopilotChat.mockResolvedValueOnce(null)
@@ -179,7 +154,7 @@ describe('Copilot Chat Delete API Route', () => {
})
it('should delete chat with empty string chatId (validation should fail)', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
const req = createMockRequest('DELETE', {
chatId: '',
@@ -188,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()
})
})
})

View File

@@ -1,6 +1,7 @@
import { db } from '@sim/db'
import { copilotChats } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { and, desc, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getLatestRunForStream } from '@/lib/copilot/async-runs/repository'
@@ -16,7 +17,6 @@ import {
import { readFilePreviewSessions } from '@/lib/copilot/request/session'
import { readEvents } from '@/lib/copilot/request/session/buffer'
import { toStreamBatchEvent } from '@/lib/copilot/request/session/types'
import { toError } from '@/lib/core/utils/helpers'
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
import { assertActiveWorkspaceAccess } from '@/lib/workspaces/permissions/utils'

View File

@@ -1,11 +1,11 @@
/**
* @vitest-environment node
*/
import { authMockFns } from '@sim/testing'
import { NextRequest } from 'next/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
mockGetSession,
mockSelect,
mockFrom,
mockWhereSelect,
@@ -17,7 +17,6 @@ const {
mockPublishStatusChanged,
mockSql,
} = vi.hoisted(() => ({
mockGetSession: vi.fn(),
mockSelect: vi.fn(),
mockFrom: vi.fn(),
mockWhereSelect: vi.fn(),
@@ -30,10 +29,6 @@ const {
mockSql: vi.fn((strings: TemplateStringsArray, ...values: unknown[]) => ({ strings, values })),
}))
vi.mock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
vi.mock('@sim/db', () => ({
db: {
select: mockSelect,
@@ -41,16 +36,6 @@ vi.mock('@sim/db', () => ({
},
}))
vi.mock('@sim/db/schema', () => ({
copilotChats: {
id: 'id',
userId: 'userId',
workspaceId: 'workspaceId',
messages: 'messages',
conversationId: 'conversationId',
},
}))
vi.mock('drizzle-orm', () => ({
and: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'and' })),
eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })),
@@ -77,7 +62,7 @@ describe('copilot chat stop route', () => {
beforeEach(() => {
vi.clearAllMocks()
mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-1' } })
mockLimit.mockResolvedValue([
{
@@ -96,7 +81,7 @@ describe('copilot chat stop route', () => {
})
it('returns 401 when unauthenticated', async () => {
mockGetSession.mockResolvedValueOnce(null)
authMockFns.mockGetSession.mockResolvedValueOnce(null)
const response = await POST(
createRequest({

View File

@@ -1,13 +1,13 @@
import { db } from '@sim/db'
import { copilotChats } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { normalizeMessage, type PersistedMessage } from '@/lib/copilot/chat/persisted-message'
import { taskPubSub } from '@/lib/copilot/tasks'
import { generateId } from '@/lib/core/utils/uuid'
const logger = createLogger('CopilotChatStopAPI')

View File

@@ -2,6 +2,7 @@
* @vitest-environment node
*/
import { copilotHttpMock, copilotHttpMockFns } from '@sim/testing'
import { NextRequest } from 'next/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
@@ -9,19 +10,13 @@ import {
MothershipStreamV1EventType,
} from '@/lib/copilot/generated/mothership-stream-v1'
const {
getLatestRunForStream,
readEvents,
readFilePreviewSessions,
checkForReplayGap,
authenticateCopilotRequestSessionOnly,
} = vi.hoisted(() => ({
getLatestRunForStream: vi.fn(),
readEvents: vi.fn(),
readFilePreviewSessions: vi.fn(),
checkForReplayGap: vi.fn(),
authenticateCopilotRequestSessionOnly: vi.fn(),
}))
const { getLatestRunForStream, readEvents, readFilePreviewSessions, checkForReplayGap } =
vi.hoisted(() => ({
getLatestRunForStream: vi.fn(),
readEvents: vi.fn(),
readFilePreviewSessions: vi.fn(),
checkForReplayGap: vi.fn(),
}))
vi.mock('@/lib/copilot/async-runs/repository', () => ({
getLatestRunForStream,
@@ -48,9 +43,7 @@ vi.mock('@/lib/copilot/request/session', () => ({
},
}))
vi.mock('@/lib/copilot/request/http', () => ({
authenticateCopilotRequestSessionOnly,
}))
vi.mock('@/lib/copilot/request/http', () => copilotHttpMock)
import { GET } from './route'
@@ -72,7 +65,7 @@ async function readAllChunks(response: Response): Promise<string[]> {
describe('copilot chat stream replay route', () => {
beforeEach(() => {
vi.clearAllMocks()
authenticateCopilotRequestSessionOnly.mockResolvedValue({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValue({
userId: 'user-1',
isAuthenticated: true,
})

View File

@@ -1,4 +1,6 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { sleep } from '@sim/utils/helpers'
import { type NextRequest, NextResponse } from 'next/server'
import { getLatestRunForStream } from '@/lib/copilot/async-runs/repository'
import {
@@ -15,7 +17,6 @@ import {
SSE_RESPONSE_HEADERS,
} from '@/lib/copilot/request/session'
import { toStreamBatchEvent } from '@/lib/copilot/request/session/types'
import { sleep, toError } from '@/lib/core/utils/helpers'
export const maxDuration = 3600

View File

@@ -3,32 +3,20 @@
*
* @vitest-environment node
*/
import { authMockFns } from '@sim/testing'
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const {
mockSelect,
mockFrom,
mockWhere,
mockLimit,
mockUpdate,
mockSet,
mockUpdateWhere,
mockGetSession,
} = vi.hoisted(() => ({
mockSelect: vi.fn(),
mockFrom: vi.fn(),
mockWhere: vi.fn(),
mockLimit: vi.fn(),
mockUpdate: vi.fn(),
mockSet: vi.fn(),
mockUpdateWhere: vi.fn(),
mockGetSession: vi.fn(),
}))
vi.mock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
const { mockSelect, mockFrom, mockWhere, mockLimit, mockUpdate, mockSet, mockUpdateWhere } =
vi.hoisted(() => ({
mockSelect: vi.fn(),
mockFrom: vi.fn(),
mockWhere: vi.fn(),
mockLimit: vi.fn(),
mockUpdate: vi.fn(),
mockSet: vi.fn(),
mockUpdateWhere: vi.fn(),
}))
vi.mock('@sim/db', () => ({
db: {
@@ -37,15 +25,6 @@ vi.mock('@sim/db', () => ({
},
}))
vi.mock('@sim/db/schema', () => ({
copilotChats: {
id: 'id',
userId: 'userId',
messages: 'messages',
updatedAt: 'updatedAt',
},
}))
vi.mock('drizzle-orm', () => ({
and: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'and' })),
eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })),
@@ -65,7 +44,7 @@ describe('Copilot Chat Update Messages API Route', () => {
beforeEach(() => {
vi.clearAllMocks()
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
mockSelect.mockReturnValue({ from: mockFrom })
mockFrom.mockReturnValue({ where: mockWhere })
@@ -82,7 +61,7 @@ describe('Copilot Chat Update Messages API Route', () => {
describe('POST', () => {
it('should return 401 when user is not authenticated', async () => {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
const req = createMockRequest('POST', {
chatId: 'chat-123',
@@ -104,7 +83,7 @@ describe('Copilot Chat Update Messages API Route', () => {
})
it('should return 400 for invalid request body - missing chatId', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
const req = createMockRequest('POST', {
messages: [
@@ -125,7 +104,7 @@ describe('Copilot Chat Update Messages API Route', () => {
})
it('should return 400 for invalid request body - missing messages', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
const req = createMockRequest('POST', {
chatId: 'chat-123',
@@ -139,7 +118,7 @@ describe('Copilot Chat Update Messages API Route', () => {
})
it('should return 400 for invalid message structure - missing required fields', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
const req = createMockRequest('POST', {
chatId: 'chat-123',
@@ -158,7 +137,7 @@ describe('Copilot Chat Update Messages API Route', () => {
})
it('should return 400 for invalid message role', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
const req = createMockRequest('POST', {
chatId: 'chat-123',
@@ -180,7 +159,7 @@ describe('Copilot Chat Update Messages API Route', () => {
})
it('should return 404 when chat is not found', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
mockLimit.mockResolvedValueOnce([])
@@ -204,7 +183,7 @@ describe('Copilot Chat Update Messages API Route', () => {
})
it('should return 404 when chat belongs to different user', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
mockLimit.mockResolvedValueOnce([])
@@ -228,7 +207,7 @@ describe('Copilot Chat Update Messages API Route', () => {
})
it('should successfully update chat messages', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
const existingChat = {
id: 'chat-123',
@@ -275,7 +254,7 @@ describe('Copilot Chat Update Messages API Route', () => {
})
it('should successfully update chat messages with optional fields', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
const existingChat = {
id: 'chat-456',
@@ -361,7 +340,7 @@ describe('Copilot Chat Update Messages API Route', () => {
})
it('should handle empty messages array', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
const existingChat = {
id: 'chat-789',
@@ -391,7 +370,7 @@ describe('Copilot Chat Update Messages API Route', () => {
})
it('should handle database errors during chat lookup', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
mockLimit.mockRejectedValueOnce(new Error('Database connection failed'))
@@ -415,7 +394,7 @@ describe('Copilot Chat Update Messages API Route', () => {
})
it('should handle database errors during update operation', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
const existingChat = {
id: 'chat-123',
@@ -448,7 +427,7 @@ describe('Copilot Chat Update Messages API Route', () => {
})
it('should handle JSON parsing errors in request body', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
const req = new NextRequest('http://localhost:3000/api/copilot/chat/update-messages', {
method: 'POST',
@@ -466,7 +445,7 @@ describe('Copilot Chat Update Messages API Route', () => {
})
it('should handle large message arrays', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
const existingChat = {
id: 'chat-large',
@@ -503,7 +482,7 @@ describe('Copilot Chat Update Messages API Route', () => {
})
it('should handle messages with both user and assistant roles', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
const existingChat = {
id: 'chat-mixed',

View File

@@ -3,26 +3,15 @@
*
* @vitest-environment node
*/
import { copilotHttpMock, copilotHttpMockFns } from '@sim/testing'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const {
mockSelectDistinctOn,
mockFrom,
mockLeftJoin,
mockWhere,
mockOrderBy,
mockAuthenticate,
mockCreateUnauthorizedResponse,
mockCreateInternalServerErrorResponse,
} = vi.hoisted(() => ({
const { mockSelectDistinctOn, mockFrom, mockLeftJoin, mockWhere, mockOrderBy } = vi.hoisted(() => ({
mockSelectDistinctOn: vi.fn(),
mockFrom: vi.fn(),
mockLeftJoin: vi.fn(),
mockWhere: vi.fn(),
mockOrderBy: vi.fn(),
mockAuthenticate: vi.fn(),
mockCreateUnauthorizedResponse: vi.fn(),
mockCreateInternalServerErrorResponse: vi.fn(),
}))
vi.mock('@sim/db', () => ({
@@ -31,32 +20,6 @@ vi.mock('@sim/db', () => ({
},
}))
vi.mock('@sim/db/schema', () => ({
copilotChats: {
id: 'id',
title: 'title',
workflowId: 'workflowId',
workspaceId: 'workspaceId',
userId: 'userId',
updatedAt: 'updatedAt',
},
workflow: {
id: 'id',
workspaceId: 'workspaceId',
archivedAt: 'archivedAt',
},
workspace: {
id: 'id',
archivedAt: 'archivedAt',
},
permissions: {
id: 'id',
entityType: 'entityType',
entityId: 'entityId',
userId: 'userId',
},
}))
vi.mock('drizzle-orm', () => ({
and: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'and' })),
eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })),
@@ -66,11 +29,7 @@ vi.mock('drizzle-orm', () => ({
sql: vi.fn(),
}))
vi.mock('@/lib/copilot/request/http', () => ({
authenticateCopilotRequestSessionOnly: mockAuthenticate,
createUnauthorizedResponse: mockCreateUnauthorizedResponse,
createInternalServerErrorResponse: mockCreateInternalServerErrorResponse,
}))
vi.mock('@/lib/copilot/request/http', () => copilotHttpMock)
import { GET } from '@/app/api/copilot/chats/route'
@@ -83,13 +42,6 @@ describe('Copilot Chats List API Route', () => {
mockLeftJoin.mockReturnValue({ leftJoin: mockLeftJoin, where: mockWhere })
mockWhere.mockReturnValue({ orderBy: mockOrderBy })
mockOrderBy.mockResolvedValue([])
mockCreateUnauthorizedResponse.mockReturnValue(
new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })
)
mockCreateInternalServerErrorResponse.mockImplementation(
(message: string) => new Response(JSON.stringify({ error: message }), { status: 500 })
)
})
afterEach(() => {
@@ -98,7 +50,7 @@ describe('Copilot Chats List API Route', () => {
describe('GET', () => {
it('should return 401 when user is not authenticated', async () => {
mockAuthenticate.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: null,
isAuthenticated: false,
})
@@ -112,7 +64,7 @@ describe('Copilot Chats List API Route', () => {
})
it('should return empty chats array when user has no chats', async () => {
mockAuthenticate.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -131,7 +83,7 @@ describe('Copilot Chats List API Route', () => {
})
it('should return list of chats for authenticated user', async () => {
mockAuthenticate.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -165,7 +117,7 @@ describe('Copilot Chats List API Route', () => {
})
it('should return chats ordered by updatedAt descending', async () => {
mockAuthenticate.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -202,7 +154,7 @@ describe('Copilot Chats List API Route', () => {
})
it('should handle chats with null workflowId', async () => {
mockAuthenticate.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -226,7 +178,7 @@ describe('Copilot Chats List API Route', () => {
})
it('should handle database errors gracefully', async () => {
mockAuthenticate.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -242,7 +194,7 @@ describe('Copilot Chats List API Route', () => {
})
it('should only return chats belonging to authenticated user', async () => {
mockAuthenticate.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -265,7 +217,7 @@ describe('Copilot Chats List API Route', () => {
})
it('should return 401 when userId is null despite isAuthenticated being true', async () => {
mockAuthenticate.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: null,
isAuthenticated: true,
})

View File

@@ -3,6 +3,7 @@
*
* @vitest-environment node
*/
import { authMockFns, workflowsUtilsMock, workflowsUtilsMockFns } from '@sim/testing'
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@@ -13,8 +14,6 @@ const {
mockThen,
mockDelete,
mockDeleteWhere,
mockAuthorize,
mockGetSession,
mockGetAccessibleCopilotChat,
} = vi.hoisted(() => ({
mockSelect: vi.fn(),
@@ -23,15 +22,9 @@ const {
mockThen: vi.fn(),
mockDelete: vi.fn(),
mockDeleteWhere: vi.fn(),
mockAuthorize: vi.fn(),
mockGetSession: vi.fn(),
mockGetAccessibleCopilotChat: vi.fn(),
}))
vi.mock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
vi.mock('@/lib/core/utils/urls', () => ({
getBaseUrl: vi.fn(() => 'http://localhost:3000'),
getInternalApiBaseUrl: vi.fn(() => 'http://localhost:3000'),
@@ -39,9 +32,7 @@ vi.mock('@/lib/core/utils/urls', () => ({
getEmailDomain: vi.fn(() => 'localhost:3000'),
}))
vi.mock('@/lib/workflows/utils', () => ({
authorizeWorkflowByWorkspacePermission: mockAuthorize,
}))
vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock)
vi.mock('@/lib/copilot/chat/lifecycle', () => ({
getAccessibleCopilotChat: mockGetAccessibleCopilotChat,
@@ -54,19 +45,6 @@ vi.mock('@sim/db', () => ({
},
}))
vi.mock('@sim/db/schema', () => ({
workflowCheckpoints: {
id: 'id',
userId: 'userId',
workflowId: 'workflowId',
workflowState: 'workflowState',
},
workflow: {
id: 'id',
userId: 'userId',
},
}))
vi.mock('drizzle-orm', () => ({
and: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'and' })),
eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })),
@@ -83,9 +61,9 @@ describe('Copilot Checkpoints Revert API Route', () => {
thenResults = []
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
mockAuthorize.mockResolvedValue({
workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
allowed: true,
status: 200,
})
@@ -134,12 +112,12 @@ describe('Copilot Checkpoints Revert API Route', () => {
/** Helper to set authenticated state */
function setAuthenticated(user = { id: 'user-123', email: 'test@example.com' }) {
mockGetSession.mockResolvedValue({ user })
authMockFns.mockGetSession.mockResolvedValue({ user })
}
/** Helper to set unauthenticated state */
function setUnauthenticated() {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
}
describe('POST', () => {
@@ -273,7 +251,7 @@ describe('Copilot Checkpoints Revert API Route', () => {
thenResults.push(mockCheckpoint) // Checkpoint found
thenResults.push(mockWorkflow) // Workflow found but different user
mockAuthorize.mockResolvedValueOnce({
workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({
allowed: false,
status: 403,
})

View File

@@ -3,6 +3,7 @@
*
* @vitest-environment node
*/
import { authMockFns, workflowsUtilsMock, workflowsUtilsMockFns } from '@sim/testing'
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@@ -15,9 +16,7 @@ const {
mockInsert,
mockValues,
mockReturning,
mockGetSession,
mockGetAccessibleCopilotChat,
mockAuthorizeWorkflowByWorkspacePermission,
} = vi.hoisted(() => ({
mockSelect: vi.fn(),
mockFrom: vi.fn(),
@@ -27,13 +26,7 @@ const {
mockInsert: vi.fn(),
mockValues: vi.fn(),
mockReturning: vi.fn(),
mockGetSession: vi.fn(),
mockGetAccessibleCopilotChat: vi.fn(),
mockAuthorizeWorkflowByWorkspacePermission: vi.fn(),
}))
vi.mock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
vi.mock('@sim/db', () => ({
@@ -43,19 +36,6 @@ vi.mock('@sim/db', () => ({
},
}))
vi.mock('@sim/db/schema', () => ({
copilotChats: { id: 'id', userId: 'userId' },
workflowCheckpoints: {
id: 'id',
userId: 'userId',
workflowId: 'workflowId',
chatId: 'chatId',
messageId: 'messageId',
createdAt: 'createdAt',
updatedAt: 'updatedAt',
},
}))
vi.mock('drizzle-orm', () => ({
and: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'and' })),
eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })),
@@ -66,9 +46,7 @@ vi.mock('@/lib/copilot/chat/lifecycle', () => ({
getAccessibleCopilotChat: mockGetAccessibleCopilotChat,
}))
vi.mock('@/lib/workflows/utils', () => ({
authorizeWorkflowByWorkspacePermission: mockAuthorizeWorkflowByWorkspacePermission,
}))
vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock)
import { GET, POST } from './route'
@@ -84,7 +62,7 @@ describe('Copilot Checkpoints API Route', () => {
beforeEach(() => {
vi.clearAllMocks()
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
mockSelect.mockReturnValue({ from: mockFrom })
mockFrom.mockReturnValue({ where: mockWhere })
@@ -101,7 +79,9 @@ describe('Copilot Checkpoints API Route', () => {
userId: 'user-123',
workflowId: 'workflow-123',
})
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({ allowed: true })
workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValue({
allowed: true,
})
})
afterEach(() => {
@@ -110,7 +90,7 @@ describe('Copilot Checkpoints API Route', () => {
describe('POST', () => {
it('should return 401 when user is not authenticated', async () => {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
const req = createMockRequest('POST', {
workflowId: 'workflow-123',
@@ -126,7 +106,7 @@ describe('Copilot Checkpoints API Route', () => {
})
it('should return 500 for invalid request body', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
const req = createMockRequest('POST', {
workflowId: 'workflow-123',
@@ -140,7 +120,7 @@ describe('Copilot Checkpoints API Route', () => {
})
it('should return 400 when chat not found or unauthorized', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
mockGetAccessibleCopilotChat.mockResolvedValueOnce(null)
const req = createMockRequest('POST', {
@@ -157,7 +137,7 @@ describe('Copilot Checkpoints API Route', () => {
})
it('should return 400 for invalid workflow state JSON', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
const req = createMockRequest('POST', {
workflowId: 'workflow-123',
@@ -173,7 +153,7 @@ describe('Copilot Checkpoints API Route', () => {
})
it('should successfully create a checkpoint', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
const checkpoint = {
id: 'checkpoint-123',
@@ -222,7 +202,7 @@ describe('Copilot Checkpoints API Route', () => {
})
it('should create checkpoint without messageId', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
const checkpoint = {
id: 'checkpoint-123',
@@ -251,7 +231,7 @@ describe('Copilot Checkpoints API Route', () => {
})
it('should handle database errors during checkpoint creation', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
mockReturning.mockRejectedValue(new Error('Database insert failed'))
@@ -269,7 +249,7 @@ describe('Copilot Checkpoints API Route', () => {
})
it('should handle database errors during chat lookup', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
mockGetAccessibleCopilotChat.mockRejectedValueOnce(new Error('Database query failed'))
@@ -289,7 +269,7 @@ describe('Copilot Checkpoints API Route', () => {
describe('GET', () => {
it('should return 401 when user is not authenticated', async () => {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
const req = new NextRequest('http://localhost:3000/api/copilot/checkpoints?chatId=chat-123')
@@ -301,7 +281,7 @@ describe('Copilot Checkpoints API Route', () => {
})
it('should return 400 when chatId is missing', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
const req = new NextRequest('http://localhost:3000/api/copilot/checkpoints')
@@ -313,7 +293,7 @@ describe('Copilot Checkpoints API Route', () => {
})
it('should return checkpoints for authenticated user and chat', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
const mockCheckpoints = [
{
@@ -374,7 +354,7 @@ describe('Copilot Checkpoints API Route', () => {
})
it('should handle database errors when fetching checkpoints', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
mockOrderBy.mockRejectedValue(new Error('Database query failed'))
@@ -388,7 +368,7 @@ describe('Copilot Checkpoints API Route', () => {
})
it('should return empty array when no checkpoints found', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'user-123' } })
mockOrderBy.mockResolvedValue([])

View File

@@ -1,36 +1,17 @@
/**
* @vitest-environment node
*/
import { copilotHttpMock, copilotHttpMockFns } from '@sim/testing'
import { NextRequest } from 'next/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
authenticateCopilotRequestSessionOnly,
createBadRequestResponse,
createInternalServerErrorResponse,
createNotFoundResponse,
createRequestTracker,
createUnauthorizedResponse,
getAsyncToolCall,
getRunSegment,
upsertAsyncToolCall,
completeAsyncToolCall,
publishToolConfirmation,
} = vi.hoisted(() => ({
authenticateCopilotRequestSessionOnly: vi.fn(),
createBadRequestResponse: vi.fn((message: string) =>
Response.json({ error: message }, { status: 400 })
),
createInternalServerErrorResponse: vi.fn((message: string) =>
Response.json({ error: message }, { status: 500 })
),
createNotFoundResponse: vi.fn((message: string) =>
Response.json({ error: message }, { status: 404 })
),
createRequestTracker: vi.fn(() => ({ requestId: 'req-1', getDuration: () => 1 })),
createUnauthorizedResponse: vi.fn(() =>
Response.json({ error: 'Unauthorized' }, { status: 401 })
),
getAsyncToolCall: vi.fn(),
getRunSegment: vi.fn(),
upsertAsyncToolCall: vi.fn(),
@@ -38,14 +19,7 @@ const {
publishToolConfirmation: vi.fn(),
}))
vi.mock('@/lib/copilot/request/http', () => ({
authenticateCopilotRequestSessionOnly,
createBadRequestResponse,
createInternalServerErrorResponse,
createNotFoundResponse,
createRequestTracker,
createUnauthorizedResponse,
}))
vi.mock('@/lib/copilot/request/http', () => copilotHttpMock)
vi.mock('@/lib/copilot/async-runs/repository', () => ({
getAsyncToolCall,
@@ -71,7 +45,7 @@ describe('Copilot Confirm API Route', () => {
beforeEach(() => {
vi.clearAllMocks()
authenticateCopilotRequestSessionOnly.mockResolvedValue({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValue({
userId: 'user-1',
isAuthenticated: true,
})
@@ -90,7 +64,7 @@ describe('Copilot Confirm API Route', () => {
}
it('returns 401 when the session is unauthenticated', async () => {
authenticateCopilotRequestSessionOnly.mockResolvedValue({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValue({
userId: null,
isAuthenticated: false,
})

View File

@@ -1,4 +1,5 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import {
@@ -22,7 +23,6 @@ import {
createRequestTracker,
createUnauthorizedResponse,
} from '@/lib/copilot/request/http'
import { toError } from '@/lib/core/utils/helpers'
const logger = createLogger('CopilotConfirmAPI')

View File

@@ -3,67 +3,19 @@
*
* @vitest-environment node
*/
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,
mockAuthenticate,
mockCreateUnauthorizedResponse,
mockCreateBadRequestResponse,
mockCreateInternalServerErrorResponse,
mockCreateRequestTracker,
} = vi.hoisted(() => ({
mockInsert: vi.fn(),
mockValues: vi.fn(),
mockReturning: vi.fn(),
mockSelect: vi.fn(),
mockFrom: vi.fn(),
mockWhere: vi.fn(),
mockAuthenticate: vi.fn(),
mockCreateUnauthorizedResponse: vi.fn(),
mockCreateBadRequestResponse: vi.fn(),
mockCreateInternalServerErrorResponse: vi.fn(),
mockCreateRequestTracker: vi.fn(),
}))
vi.mock('@sim/db', () => dbChainMock)
vi.mock('@sim/db', () => ({
db: {
insert: mockInsert,
select: mockSelect,
},
}))
vi.mock('@sim/db/schema', () => ({
copilotFeedback: {
feedbackId: 'feedbackId',
userId: 'userId',
chatId: 'chatId',
userQuery: 'userQuery',
agentResponse: 'agentResponse',
isPositive: 'isPositive',
feedback: 'feedback',
workflowYaml: 'workflowYaml',
createdAt: 'createdAt',
},
}))
vi.mock('drizzle-orm', () => ({
eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })),
}))
vi.mock('@/lib/copilot/request/http', () => ({
authenticateCopilotRequestSessionOnly: mockAuthenticate,
createUnauthorizedResponse: mockCreateUnauthorizedResponse,
createBadRequestResponse: mockCreateBadRequestResponse,
createInternalServerErrorResponse: mockCreateInternalServerErrorResponse,
createRequestTracker: mockCreateRequestTracker,
}))
vi.mock('@/lib/copilot/request/http', () => copilotHttpMock)
import { GET, POST } from '@/app/api/copilot/feedback/route'
@@ -78,27 +30,7 @@ function createMockRequest(method: string, body: Record<string, unknown>): 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([])
mockCreateRequestTracker.mockReturnValue({
requestId: 'test-request-id',
getDuration: vi.fn().mockReturnValue(100),
})
mockCreateUnauthorizedResponse.mockReturnValue(
new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })
)
mockCreateBadRequestResponse.mockImplementation(
(message: string) => new Response(JSON.stringify({ error: message }), { status: 400 })
)
mockCreateInternalServerErrorResponse.mockImplementation(
(message: string) => new Response(JSON.stringify({ error: message }), { status: 500 })
)
resetDbChainMock()
})
afterEach(() => {
@@ -107,7 +39,7 @@ describe('Copilot Feedback API Route', () => {
describe('POST', () => {
it('should return 401 when user is not authenticated', async () => {
mockAuthenticate.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: null,
isAuthenticated: false,
})
@@ -127,7 +59,7 @@ describe('Copilot Feedback API Route', () => {
})
it('should successfully submit positive feedback', async () => {
mockAuthenticate.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -143,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',
@@ -162,7 +94,7 @@ describe('Copilot Feedback API Route', () => {
})
it('should successfully submit negative feedback with text', async () => {
mockAuthenticate.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -178,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',
@@ -197,7 +129,7 @@ describe('Copilot Feedback API Route', () => {
})
it('should successfully submit feedback with workflow YAML', async () => {
mockAuthenticate.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -224,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',
@@ -240,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,
})
@@ -248,7 +180,7 @@ edges:
})
it('should return 400 for invalid chatId format', async () => {
mockAuthenticate.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -268,7 +200,7 @@ edges:
})
it('should return 400 for empty userQuery', async () => {
mockAuthenticate.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -288,7 +220,7 @@ edges:
})
it('should return 400 for empty agentResponse', async () => {
mockAuthenticate.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -308,7 +240,7 @@ edges:
})
it('should return 400 for missing isPositiveFeedback', async () => {
mockAuthenticate.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -327,12 +259,12 @@ edges:
})
it('should handle database errors gracefully', async () => {
mockAuthenticate.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
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',
@@ -349,7 +281,7 @@ edges:
})
it('should handle JSON parsing errors in request body', async () => {
mockAuthenticate.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -370,7 +302,7 @@ edges:
describe('GET', () => {
it('should return 401 when user is not authenticated', async () => {
mockAuthenticate.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: null,
isAuthenticated: false,
})
@@ -384,12 +316,12 @@ edges:
})
it('should return empty feedback array when no feedback exists', async () => {
mockAuthenticate.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
mockWhere.mockResolvedValueOnce([])
dbChainMockFns.where.mockResolvedValueOnce([])
const request = new Request('http://localhost:3000/api/copilot/feedback')
const response = await GET(request as any)
@@ -401,7 +333,7 @@ edges:
})
it('should only return feedback records for the authenticated user', async () => {
mockAuthenticate.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -419,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)
@@ -431,19 +363,18 @@ 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')
})
it('should handle database errors gracefully', async () => {
mockAuthenticate.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
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)
@@ -454,12 +385,12 @@ edges:
})
it('should return metadata with response', async () => {
mockAuthenticate.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
mockWhere.mockResolvedValueOnce([])
dbChainMockFns.where.mockResolvedValueOnce([])
const request = new Request('http://localhost:3000/api/copilot/feedback')
const response = await GET(request as any)

View File

@@ -1,8 +1,8 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { SIM_AGENT_API_URL } from '@/lib/copilot/constants'
import { authenticateCopilotRequestSessionOnly } from '@/lib/copilot/request/http'
import { toError } from '@/lib/core/utils/helpers'
interface AvailableModel {
id: string

View File

@@ -3,54 +3,22 @@
*
* @vitest-environment node
*/
import { createMockRequest } from '@sim/testing'
import { copilotHttpMock, copilotHttpMockFns, createEnvMock, createMockRequest } from '@sim/testing'
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const {
mockAuthenticateCopilotRequestSessionOnly,
mockCreateUnauthorizedResponse,
mockCreateBadRequestResponse,
mockCreateInternalServerErrorResponse,
mockCreateRequestTracker,
mockFetch,
} = vi.hoisted(() => ({
mockAuthenticateCopilotRequestSessionOnly: vi.fn(),
mockCreateUnauthorizedResponse: vi.fn(),
mockCreateBadRequestResponse: vi.fn(),
mockCreateInternalServerErrorResponse: vi.fn(),
mockCreateRequestTracker: vi.fn(),
const { mockFetch } = vi.hoisted(() => ({
mockFetch: vi.fn(),
}))
vi.mock('@/lib/copilot/request/http', () => ({
authenticateCopilotRequestSessionOnly: mockAuthenticateCopilotRequestSessionOnly,
createUnauthorizedResponse: mockCreateUnauthorizedResponse,
createBadRequestResponse: mockCreateBadRequestResponse,
createInternalServerErrorResponse: mockCreateInternalServerErrorResponse,
createRequestTracker: mockCreateRequestTracker,
}))
vi.mock('@/lib/copilot/request/http', () => copilotHttpMock)
vi.mock('@/lib/copilot/constants', () => ({
SIM_AGENT_API_URL_DEFAULT: 'https://agent.sim.example.com',
SIM_AGENT_API_URL: 'https://agent.sim.example.com',
}))
vi.mock('@/lib/core/config/env', () => ({
env: {
COPILOT_API_KEY: 'test-api-key',
},
getEnv: vi.fn((key: string) => {
const vals: Record<string, string | undefined> = {
COPILOT_API_KEY: 'test-api-key',
}
return vals[key]
}),
isTruthy: (value: string | boolean | number | undefined) =>
typeof value === 'string' ? value.toLowerCase() === 'true' || value === '1' : Boolean(value),
isFalsy: (value: string | boolean | number | undefined) =>
typeof value === 'string' ? value.toLowerCase() === 'false' || value === '0' : value === false,
}))
vi.mock('@/lib/core/config/env', () => createEnvMock({ COPILOT_API_KEY: 'test-api-key' }))
import { POST } from '@/app/api/copilot/stats/route'
@@ -58,20 +26,6 @@ describe('Copilot Stats API Route', () => {
beforeEach(() => {
vi.clearAllMocks()
global.fetch = mockFetch
mockCreateUnauthorizedResponse.mockReturnValue(
new Response(JSON.stringify({ error: 'Unauthorized' }), { status: 401 })
)
mockCreateBadRequestResponse.mockImplementation(
(message: string) => new Response(JSON.stringify({ error: message }), { status: 400 })
)
mockCreateInternalServerErrorResponse.mockImplementation(
(message: string) => new Response(JSON.stringify({ error: message }), { status: 500 })
)
mockCreateRequestTracker.mockReturnValue({
requestId: 'test-request-id',
getDuration: vi.fn().mockReturnValue(100),
})
})
afterEach(() => {
@@ -80,7 +34,7 @@ describe('Copilot Stats API Route', () => {
describe('POST', () => {
it('should return 401 when user is not authenticated', async () => {
mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: null,
isAuthenticated: false,
})
@@ -99,7 +53,7 @@ describe('Copilot Stats API Route', () => {
})
it('should successfully forward stats to Sim Agent', async () => {
mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -139,7 +93,7 @@ describe('Copilot Stats API Route', () => {
})
it('should return 400 for invalid request body - missing messageId', async () => {
mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -157,7 +111,7 @@ describe('Copilot Stats API Route', () => {
})
it('should return 400 for invalid request body - missing diffCreated', async () => {
mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -175,7 +129,7 @@ describe('Copilot Stats API Route', () => {
})
it('should return 400 for invalid request body - missing diffAccepted', async () => {
mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -193,7 +147,7 @@ describe('Copilot Stats API Route', () => {
})
it('should return 400 when upstream Sim Agent returns error', async () => {
mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -217,7 +171,7 @@ describe('Copilot Stats API Route', () => {
})
it('should handle upstream error with message field', async () => {
mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -241,7 +195,7 @@ describe('Copilot Stats API Route', () => {
})
it('should handle upstream error with no JSON response', async () => {
mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -265,7 +219,7 @@ describe('Copilot Stats API Route', () => {
})
it('should handle network errors gracefully', async () => {
mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -286,7 +240,7 @@ describe('Copilot Stats API Route', () => {
})
it('should handle JSON parsing errors in request body', async () => {
mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})
@@ -307,7 +261,7 @@ describe('Copilot Stats API Route', () => {
})
it('should forward stats with diffCreated=false and diffAccepted=false', async () => {
mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
copilotHttpMockFns.mockAuthenticateCopilotRequestSessionOnly.mockResolvedValueOnce({
userId: 'user-123',
isAuthenticated: true,
})

View File

@@ -1,12 +1,12 @@
import { db } from '@sim/db'
import { member, templateCreators } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq, or } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import type { CreatorProfileDetails } from '@/app/_types/creator-profile'
const logger = createLogger('CreatorProfilesAPI')

View File

@@ -1,6 +1,7 @@
import { db } from '@sim/db'
import { credentialSet, credentialSetInvitation, member, organization, user } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
@@ -9,7 +10,6 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { hasCredentialSetsAccess } from '@/lib/billing'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { generateId } from '@/lib/core/utils/uuid'
import { sendEmail } from '@/lib/messaging/email/mailer'
const logger = createLogger('CredentialSetInvite')

View File

@@ -1,12 +1,12 @@
import { db } from '@sim/db'
import { account, credentialSet, credentialSetMember, member, user } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq, inArray } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { hasCredentialSetsAccess } from '@/lib/billing'
import { generateId } from '@/lib/core/utils/uuid'
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
const logger = createLogger('CredentialSetMembers')

View File

@@ -6,11 +6,11 @@ import {
organization,
} from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { generateId } from '@/lib/core/utils/uuid'
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
const logger = createLogger('CredentialSetInviteToken')

View File

@@ -1,11 +1,11 @@
import { db } from '@sim/db'
import { credentialSet, credentialSetMember, organization } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { generateId } from '@/lib/core/utils/uuid'
import { syncAllWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
const logger = createLogger('CredentialSetMemberships')

View File

@@ -1,13 +1,13 @@
import { db } from '@sim/db'
import { credentialSet, credentialSetMember, member, organization, user } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, count, desc, eq } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { hasCredentialSetsAccess } from '@/lib/billing'
import { generateId } from '@/lib/core/utils/uuid'
const logger = createLogger('CredentialSets')

View File

@@ -1,11 +1,11 @@
import { db } from '@sim/db'
import { credential, credentialMember, user } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { generateId } from '@/lib/core/utils/uuid'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
const logger = createLogger('CredentialMembersAPI')

View File

@@ -1,17 +1,17 @@
import { db } from '@sim/db'
import { credential, credentialMember, environment, workspaceEnvironment } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { encryptSecret } from '@/lib/core/security/encryption'
import { generateId } from '@/lib/core/utils/uuid'
import { getCredentialActorContext } from '@/lib/credentials/access'
import {
deleteWorkspaceEnvCredentials,
syncPersonalEnvCredentialsForUser,
syncWorkspaceEnvCredentials,
} from '@/lib/credentials/environment'
import { captureServerEvent } from '@/lib/posthog/server'
@@ -317,10 +317,9 @@ export async function DELETE(
set: { variables: current, updatedAt: new Date() },
})
await syncWorkspaceEnvCredentials({
await deleteWorkspaceEnvCredentials({
workspaceId: access.credential.workspaceId,
envKeys: Object.keys(current),
actingUserId: session.user.id,
removedKeys: [access.credential.envKey],
})
captureServerEvent(

View File

@@ -1,11 +1,11 @@
import { db } from '@sim/db'
import { credential, credentialMember, pendingCredentialDraft } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq, lt } from 'drizzle-orm'
import { NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { generateId } from '@/lib/core/utils/uuid'
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
const logger = createLogger('CredentialDraftAPI')

View File

@@ -1,6 +1,7 @@
import { db } from '@sim/db'
import { account, credential, credentialMember, workspace } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
@@ -8,7 +9,6 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { encryptSecret } from '@/lib/core/security/encryption'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import { getWorkspaceMemberUserIds } from '@/lib/credentials/environment'
import { syncWorkspaceOAuthCredentialsForUser } from '@/lib/credentials/oauth'
import { getServiceConfigByProviderId } from '@/lib/oauth'

View File

@@ -1,12 +1,12 @@
import { asyncJobs, db } from '@sim/db'
import { workflowExecutionLogs } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { and, eq, inArray, lt, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { verifyCronAuth } from '@/lib/auth/internal'
import { JOB_RETENTION_HOURS, JOB_STATUS } from '@/lib/core/async-jobs'
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
import { toError } from '@/lib/core/utils/helpers'
const logger = createLogger('CleanupStaleExecutions')

View File

@@ -1,6 +1,7 @@
import { db } from '@sim/db'
import { environment } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
@@ -8,7 +9,6 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { decryptSecret, encryptSecret } from '@/lib/core/security/encryption'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import { syncPersonalEnvCredentialsForUser } from '@/lib/credentials/environment'
import type { EnvironmentVariable } from '@/lib/environment/api'

View File

@@ -1,13 +1,10 @@
/**
* @vitest-environment node
*/
import { authMockFns, hybridAuthMockFns } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => {
const mockGetSession = vi.fn()
const mockCheckHybridAuth = vi.fn()
const mockCheckSessionOrInternalAuth = vi.fn()
const mockCheckInternalAuth = vi.fn()
const mockVerifyFileAccess = vi.fn()
const mockVerifyWorkspaceFileAccess = vi.fn()
const mockDeleteFile = vi.fn()
@@ -18,10 +15,6 @@ const mocks = vi.hoisted(() => {
const mockDownloadFile = vi.fn()
return {
mockGetSession,
mockCheckHybridAuth,
mockCheckSessionOrInternalAuth,
mockCheckInternalAuth,
mockVerifyFileAccess,
mockVerifyWorkspaceFileAccess,
mockDeleteFile,
@@ -33,30 +26,6 @@ const mocks = vi.hoisted(() => {
}
})
vi.mock('@sim/logger', () => ({
createLogger: vi.fn().mockReturnValue({
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
}),
}))
vi.mock('@sim/db/schema', () => ({
workflowFolder: {
id: 'id',
userId: 'userId',
parentId: 'parentId',
updatedAt: 'updatedAt',
workspaceId: 'workspaceId',
sortOrder: 'sortOrder',
createdAt: 'createdAt',
},
workflow: { id: 'id', folderId: 'folderId', userId: 'userId', updatedAt: 'updatedAt' },
account: { userId: 'userId', providerId: 'providerId' },
user: { email: 'email', id: 'id' },
}))
vi.mock('drizzle-orm', () => ({
and: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'and' })),
eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })),
@@ -82,7 +51,7 @@ vi.mock('drizzle-orm', () => ({
sql: vi.fn((strings: unknown, ...values: unknown[]) => ({ type: 'sql', sql: strings, values })),
}))
vi.mock('@/lib/core/utils/uuid', () => ({
vi.mock('@sim/utils/id', () => ({
generateId: vi.fn(() => 'test-uuid'),
generateShortId: vi.fn(() => 'mock-short-id'),
isValidUuid: vi.fn((v: string) =>
@@ -90,17 +59,6 @@ vi.mock('@/lib/core/utils/uuid', () => ({
),
}))
vi.mock('@/lib/auth', () => ({
getSession: mocks.mockGetSession,
}))
vi.mock('@/lib/auth/hybrid', () => ({
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
checkHybridAuth: mocks.mockCheckHybridAuth,
checkSessionOrInternalAuth: mocks.mockCheckSessionOrInternalAuth,
checkInternalAuth: mocks.mockCheckInternalAuth,
}))
vi.mock('@/app/api/files/authorization', () => ({
verifyFileAccess: mocks.mockVerifyFileAccess,
verifyWorkspaceFileAccess: mocks.mockVerifyWorkspaceFileAccess,
@@ -151,8 +109,8 @@ describe('File Delete API Route', () => {
randomUUID: vi.fn().mockReturnValue('mock-uuid-1234-5678'),
})
mocks.mockGetSession.mockResolvedValue({ user: { id: 'test-user-id' } })
mocks.mockCheckSessionOrInternalAuth.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'test-user-id' } })
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true,
userId: 'test-user-id',
error: undefined,

View File

@@ -3,7 +3,14 @@
*
* @vitest-environment node
*/
import { createMockRequest } from '@sim/testing'
import {
authMockFns,
createMockRequest,
hybridAuthMockFns,
inputValidationMock,
permissionsMock,
permissionsMockFns,
} from '@sim/testing'
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@@ -22,10 +29,6 @@ const {
mockFsReadFile,
mockFsWriteFile,
mockJoin,
mockGetSession,
mockCheckInternalAuth,
mockCheckHybridAuth,
mockCheckSessionOrInternalAuth,
actualPath,
} = vi.hoisted(() => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
@@ -56,10 +59,6 @@ const {
}
return actualPath.join(...args)
}),
mockGetSession: vi.fn(),
mockCheckInternalAuth: vi.fn(),
mockCheckHybridAuth: vi.fn(),
mockCheckSessionOrInternalAuth: vi.fn(),
actualPath,
}
})
@@ -98,24 +97,7 @@ 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', () => ({
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
checkInternalAuth: mockCheckInternalAuth,
checkHybridAuth: mockCheckHybridAuth,
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
}))
vi.mock('@/lib/core/security/input-validation.server', () => ({
secureFetchWithPinnedIP: vi.fn(),
validateUrlWithDNS: vi.fn(),
}))
vi.mock('@/lib/core/security/input-validation.server', () => inputValidationMock)
vi.mock('@/lib/core/utils/logging', () => ({
sanitizeUrlForLog: vi.fn((url: string) => url),
@@ -129,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: {
@@ -158,26 +138,26 @@ 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)
}
mockCheckInternalAuth.mockResolvedValue({
hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValue({
success: authenticated,
userId: authenticated ? 'test-user-id' : undefined,
error: authenticated ? undefined : 'Unauthorized',
})
mockCheckHybridAuth.mockResolvedValue({
hybridAuthMockFns.mockCheckHybridAuth.mockResolvedValue({
success: authenticated,
userId: authenticated ? 'test-user-id' : undefined,
error: authenticated ? undefined : 'Unauthorized',
})
mockCheckSessionOrInternalAuth.mockResolvedValue({
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: authenticated,
userId: authenticated ? 'test-user-id' : undefined,
error: authenticated ? undefined : 'Unauthorized',
@@ -195,6 +175,7 @@ describe('File Parse API Route', () => {
authenticated: true,
})
permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue({ canView: true })
mockIsSupportedFileType.mockReturnValue(true)
mockParseFile.mockResolvedValue({
content: 'parsed content',
@@ -389,6 +370,7 @@ describe('Files Parse API - Path Traversal Security', () => {
setupFileApiMocks({
authenticated: true,
})
permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue({ canView: true })
})
describe('Path Traversal Prevention', () => {

View File

@@ -4,11 +4,11 @@
* @vitest-environment node
*/
import { authMockFns } from '@sim/testing'
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const {
mockGetSession,
mockVerifyFileAccess,
mockVerifyWorkspaceFileAccess,
mockUseBlobStorage,
@@ -25,7 +25,6 @@ const {
mockGetStorageProviderUploads,
mockIsUsingCloudStorageUploads,
} = vi.hoisted(() => ({
mockGetSession: vi.fn(),
mockVerifyFileAccess: vi.fn().mockResolvedValue(true),
mockVerifyWorkspaceFileAccess: vi.fn().mockResolvedValue(true),
mockUseBlobStorage: { value: false },
@@ -46,10 +45,6 @@ const {
mockIsUsingCloudStorageUploads: vi.fn(),
}))
vi.mock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
vi.mock('@/app/api/files/authorization', () => ({
verifyFileAccess: mockVerifyFileAccess,
verifyWorkspaceFileAccess: mockVerifyWorkspaceFileAccess,
@@ -108,9 +103,9 @@ function setupFileApiMocks(
const { authenticated = true, storageProvider = 's3', cloudEnabled = true } = options
if (authenticated) {
mockGetSession.mockResolvedValue({ user: defaultMockUser })
authMockFns.mockGetSession.mockResolvedValue({ user: defaultMockUser })
} else {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
}
const useBlobStorage = storageProvider === 'blob' && cloudEnabled

View File

@@ -3,11 +3,11 @@
*
* @vitest-environment node
*/
import { hybridAuthMockFns } from '@sim/testing'
import { NextRequest } from 'next/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
mockCheckSessionOrInternalAuth,
mockVerifyFileAccess,
mockReadFile,
mockIsUsingCloudStorage,
@@ -27,7 +27,6 @@ const {
}
}
return {
mockCheckSessionOrInternalAuth: vi.fn(),
mockVerifyFileAccess: vi.fn(),
mockReadFile: vi.fn(),
mockIsUsingCloudStorage: vi.fn(),
@@ -48,11 +47,6 @@ vi.mock('fs/promises', () => ({
stat: vi.fn().mockResolvedValue({ isFile: () => true, size: 100 }),
}))
vi.mock('@/lib/auth/hybrid', () => ({
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
}))
vi.mock('@/app/api/files/authorization', () => ({
verifyFileAccess: mockVerifyFileAccess,
}))
@@ -103,7 +97,7 @@ describe('File Serve API Route', () => {
beforeEach(() => {
vi.clearAllMocks()
mockCheckSessionOrInternalAuth.mockResolvedValue({
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true,
userId: 'test-user-id',
})

View File

@@ -3,19 +3,15 @@
*
* @vitest-environment node
*/
import { authMockFns, hybridAuthMockFns, permissionsMock, permissionsMockFns } from '@sim/testing'
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => {
const mockGetSession = vi.fn()
const mockCheckHybridAuth = vi.fn()
const mockCheckSessionOrInternalAuth = vi.fn()
const mockCheckInternalAuth = vi.fn()
const mockVerifyFileAccess = vi.fn()
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()
@@ -24,15 +20,10 @@ const mocks = vi.hoisted(() => {
const mockStorageUploadFile = vi.fn()
return {
mockGetSession,
mockCheckHybridAuth,
mockCheckSessionOrInternalAuth,
mockCheckInternalAuth,
mockVerifyFileAccess,
mockVerifyWorkspaceFileAccess,
mockVerifyKBFileAccess,
mockVerifyCopilotFileAccess,
mockGetUserEntityPermissions,
mockUploadWorkspaceFile,
mockGetStorageProvider,
mockIsUsingCloudStorage,
@@ -42,30 +33,6 @@ const mocks = vi.hoisted(() => {
}
})
vi.mock('@sim/logger', () => ({
createLogger: vi.fn().mockReturnValue({
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
}),
}))
vi.mock('@sim/db/schema', () => ({
workflowFolder: {
id: 'id',
userId: 'userId',
parentId: 'parentId',
updatedAt: 'updatedAt',
workspaceId: 'workspaceId',
sortOrder: 'sortOrder',
createdAt: 'createdAt',
},
workflow: { id: 'id', folderId: 'folderId', userId: 'userId', updatedAt: 'updatedAt' },
account: { userId: 'userId', providerId: 'providerId' },
user: { email: 'email', id: 'id' },
}))
vi.mock('drizzle-orm', () => ({
and: vi.fn((...conditions: unknown[]) => ({ conditions, type: 'and' })),
eq: vi.fn((field: unknown, value: unknown) => ({ field, value, type: 'eq' })),
@@ -91,7 +58,7 @@ vi.mock('drizzle-orm', () => ({
sql: vi.fn((strings: unknown, ...values: unknown[]) => ({ type: 'sql', sql: strings, values })),
}))
vi.mock('@/lib/core/utils/uuid', () => ({
vi.mock('@sim/utils/id', () => ({
generateId: vi.fn(() => 'test-uuid'),
generateShortId: vi.fn(() => 'mock-short-id'),
isValidUuid: vi.fn((v: string) =>
@@ -99,17 +66,6 @@ vi.mock('@/lib/core/utils/uuid', () => ({
),
}))
vi.mock('@/lib/auth', () => ({
getSession: mocks.mockGetSession,
}))
vi.mock('@/lib/auth/hybrid', () => ({
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
checkHybridAuth: mocks.mockCheckHybridAuth,
checkSessionOrInternalAuth: mocks.mockCheckSessionOrInternalAuth,
checkInternalAuth: mocks.mockCheckInternalAuth,
}))
vi.mock('@/app/api/files/authorization', () => ({
verifyFileAccess: mocks.mockVerifyFileAccess,
verifyWorkspaceFileAccess: mocks.mockVerifyWorkspaceFileAccess,
@@ -117,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,
@@ -160,12 +114,12 @@ function setupFileApiMocks(
})
if (authenticated) {
mocks.mockGetSession.mockResolvedValue({ user: { id: 'test-user-id' } })
authMockFns.mockGetSession.mockResolvedValue({ user: { id: 'test-user-id' } })
} else {
mocks.mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
}
mocks.mockCheckHybridAuth.mockResolvedValue({
hybridAuthMockFns.mockCheckHybridAuth.mockResolvedValue({
success: authenticated,
userId: authenticated ? 'test-user-id' : undefined,
error: authenticated ? undefined : 'Unauthorized',
@@ -176,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',
@@ -367,7 +321,7 @@ describe('File Upload Security Tests', () => {
beforeEach(() => {
vi.clearAllMocks()
mocks.mockGetSession.mockResolvedValue({
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'test-user-id' },
})
@@ -529,7 +483,7 @@ describe('File Upload Security Tests', () => {
describe('Authentication Requirements', () => {
it('should reject uploads without authentication', async () => {
mocks.mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
const formData = new FormData()
const file = new File(['test content'], 'test.pdf', { type: 'application/pdf' })

View File

@@ -1,13 +1,13 @@
import { db } from '@sim/db'
import { workflow, workflowFolder } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq, isNull, min } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import { duplicateWorkflow } from '@/lib/workflows/persistence/duplicate'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'

View File

@@ -3,17 +3,21 @@
*
* @vitest-environment node
*/
import { auditMock, createMockRequest, type MockUser } from '@sim/testing'
import {
auditMock,
authMockFns,
createMockRequest,
type MockUser,
permissionsMock,
permissionsMockFns,
workflowsOrchestrationMock,
workflowsOrchestrationMockFns,
workflowsUtilsMock,
workflowsUtilsMockFns,
} from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
mockGetSession,
mockGetUserEntityPermissions,
mockLogger,
mockDbRef,
mockPerformDeleteFolder,
mockCheckForCircularReference,
} = vi.hoisted(() => {
const { mockLogger, mockDbRef } = vi.hoisted(() => {
const logger = {
info: vi.fn(),
warn: vi.fn(),
@@ -24,36 +28,27 @@ const {
child: vi.fn(),
}
return {
mockGetSession: vi.fn(),
mockGetUserEntityPermissions: vi.fn(),
mockLogger: logger,
mockDbRef: { current: null as any },
mockPerformDeleteFolder: vi.fn(),
mockCheckForCircularReference: vi.fn(),
}
})
const mockPerformDeleteFolder = workflowsOrchestrationMockFns.mockPerformDeleteFolder
const mockGetUserEntityPermissions = permissionsMockFns.mockGetUserEntityPermissions
vi.mock('@/lib/audit/log', () => auditMock)
vi.mock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
vi.mock('@sim/logger', () => ({
createLogger: vi.fn().mockReturnValue(mockLogger),
}))
vi.mock('@/lib/workspaces/permissions/utils', () => ({
getUserEntityPermissions: mockGetUserEntityPermissions,
}))
vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock)
vi.mock('@sim/db', () => ({
get db() {
return mockDbRef.current
},
}))
vi.mock('@/lib/workflows/orchestration', () => ({
performDeleteFolder: mockPerformDeleteFolder,
}))
vi.mock('@/lib/workflows/utils', () => ({
checkForCircularReference: mockCheckForCircularReference,
}))
vi.mock('@/lib/workflows/orchestration', () => workflowsOrchestrationMock)
vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock)
import { DELETE, PUT } from '@/app/api/folders/[id]/route'
@@ -146,11 +141,11 @@ function createFolderDbMock(options: FolderDbMockOptions = {}) {
}
function mockAuthenticatedUser(user?: MockUser) {
mockGetSession.mockResolvedValue({ user: user || TEST_USER })
authMockFns.mockGetSession.mockResolvedValue({ user: user || TEST_USER })
}
function mockUnauthenticated() {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
}
describe('Individual Folder API Route', () => {
@@ -163,7 +158,7 @@ describe('Individual Folder API Route', () => {
success: true,
deletedItems: { folders: 1, workflows: 0 },
})
mockCheckForCircularReference.mockResolvedValue(false)
workflowsUtilsMockFns.mockCheckForCircularReference.mockResolvedValue(false)
})
describe('PUT /api/folders/[id]', () => {
@@ -398,7 +393,7 @@ describe('Individual Folder API Route', () => {
},
})
mockCheckForCircularReference.mockResolvedValue(true)
workflowsUtilsMockFns.mockCheckForCircularReference.mockResolvedValue(true)
const req = createMockRequest('PUT', {
name: 'Updated Folder 3',
@@ -412,7 +407,10 @@ describe('Individual Folder API Route', () => {
const data = await response.json()
expect(data).toHaveProperty('error', 'Cannot create circular folder reference')
expect(mockCheckForCircularReference).toHaveBeenCalledWith('folder-3', 'folder-1')
expect(workflowsUtilsMockFns.mockCheckForCircularReference).toHaveBeenCalledWith(
'folder-3',
'folder-1'
)
})
})

View File

@@ -3,11 +3,17 @@
*
* @vitest-environment node
*/
import { auditMock, createMockRequest } from '@sim/testing'
import {
auditMock,
authMockFns,
createMockRequest,
permissionsMock,
permissionsMockFns,
} from '@sim/testing'
import { drizzleOrmMock } from '@sim/testing/mocks'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockGetSession, mockGetUserEntityPermissions, mockLogger } = vi.hoisted(() => {
const { mockLogger } = vi.hoisted(() => {
const logger = {
info: vi.fn(),
warn: vi.fn(),
@@ -18,26 +24,21 @@ const { mockGetSession, mockGetUserEntityPermissions, mockLogger } = vi.hoisted(
child: vi.fn(),
}
return {
mockGetSession: vi.fn(),
mockGetUserEntityPermissions: vi.fn(),
mockLogger: logger,
}
})
const mockGetUserEntityPermissions = permissionsMockFns.mockGetUserEntityPermissions
vi.mock('@/lib/audit/log', () => auditMock)
vi.mock('drizzle-orm', () => ({
...drizzleOrmMock,
min: vi.fn((field) => ({ type: 'min', field })),
}))
vi.mock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
vi.mock('@sim/logger', () => ({
createLogger: vi.fn().mockReturnValue(mockLogger),
}))
vi.mock('@/lib/workspaces/permissions/utils', () => ({
getUserEntityPermissions: mockGetUserEntityPermissions,
}))
vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock)
import { db } from '@sim/db'
import { GET, POST } from '@/app/api/folders/route'
@@ -131,11 +132,11 @@ describe('Folders API Route', () => {
const mockTransaction = mockDb.transaction
function mockAuthenticatedUser() {
mockGetSession.mockResolvedValue({ user: defaultMockUser })
authMockFns.mockGetSession.mockResolvedValue({ user: defaultMockUser })
}
function mockUnauthenticated() {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
}
beforeEach(() => {

View File

@@ -1,12 +1,12 @@
import { db } from '@sim/db'
import { workflow, workflowFolder } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, asc, eq, isNotNull, isNull, min } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { generateId } from '@/lib/core/utils/uuid'
import { captureServerEvent } from '@/lib/posthog/server'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'

View File

@@ -1,12 +1,12 @@
import { db } from '@sim/db'
import { form, workflow, workflowBlocks } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq, isNull } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { addCorsHeaders, validateAuthToken } from '@/lib/core/security/deployment'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import { preprocessExecution } from '@/lib/execution/preprocessing'
import { LoggingSession } from '@/lib/logs/execution/logging-session'
import { executeWorkflow } from '@/lib/workflows/executor/execute-workflow'

View File

@@ -1,6 +1,7 @@
import { db } from '@sim/db'
import { form } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq, isNull } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { z } from 'zod'
@@ -9,7 +10,6 @@ import { getSession } from '@/lib/auth'
import { isDev } from '@/lib/core/config/feature-flags'
import { encryptSecret } from '@/lib/core/security/encryption'
import { getEmailDomain } from '@/lib/core/utils/urls'
import { generateId } from '@/lib/core/utils/uuid'
import { notifySocketDeploymentChanged } from '@/lib/workflows/orchestration'
import { deployWorkflow } from '@/lib/workflows/persistence/utils'
import {

View File

@@ -3,30 +3,25 @@
*
* @vitest-environment node
*/
import { databaseMock, loggerMock } from '@sim/testing'
import { encryptionMock, encryptionMockFns, workflowsUtilsMock } from '@sim/testing'
import type { NextResponse } from 'next/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
mockDecryptSecret,
mockValidateAuthToken,
mockSetDeploymentAuthCookie,
mockAddCorsHeaders,
mockIsEmailAllowed,
} = vi.hoisted(() => ({
mockDecryptSecret: vi.fn(),
mockValidateAuthToken: vi.fn().mockReturnValue(false),
mockSetDeploymentAuthCookie: vi.fn(),
mockAddCorsHeaders: vi.fn((response: unknown) => response),
mockIsEmailAllowed: vi.fn(),
}))
vi.mock('@sim/db', () => databaseMock)
vi.mock('@sim/logger', () => loggerMock)
const mockDecryptSecret = encryptionMockFns.mockDecryptSecret
vi.mock('@/lib/core/security/encryption', () => ({
decryptSecret: mockDecryptSecret,
}))
vi.mock('@/lib/core/security/encryption', () => encryptionMock)
vi.mock('@/lib/core/security/deployment', () => ({
validateAuthToken: mockValidateAuthToken,
@@ -41,9 +36,7 @@ vi.mock('@/lib/core/config/feature-flags', () => ({
isProd: false,
}))
vi.mock('@/lib/workflows/utils', () => ({
authorizeWorkflowByWorkspacePermission: vi.fn(),
}))
vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock)
import { decryptSecret } from '@/lib/core/security/encryption'
import {

View File

@@ -3,12 +3,16 @@
*
* @vitest-environment node
*/
import { createMockRequest } from '@sim/testing'
import {
createMockRequest,
featureFlagsMock,
hybridAuthMockFns,
workflowsUtilsMock,
} from '@sim/testing'
import { NextRequest } from 'next/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockCheckInternalAuth, mockExecuteInE2B, mockExecuteInIsolatedVM } = vi.hoisted(() => ({
mockCheckInternalAuth: vi.fn(),
const { mockExecuteInE2B, mockExecuteInIsolatedVM } = vi.hoisted(() => ({
mockExecuteInE2B: vi.fn(),
mockExecuteInIsolatedVM: vi.fn(),
}))
@@ -17,11 +21,6 @@ vi.mock('@/lib/execution/isolated-vm', () => ({
executeInIsolatedVM: mockExecuteInIsolatedVM,
}))
vi.mock('@/lib/auth/hybrid', () => ({
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
checkInternalAuth: mockCheckInternalAuth,
}))
vi.mock('@/lib/execution/e2b', () => ({
executeInE2B: mockExecuteInE2B,
executeShellInE2B: vi.fn(),
@@ -43,18 +42,9 @@ vi.mock('@/lib/uploads/contexts/workspace/workspace-file-manager', () => ({
uploadWorkspaceFile: vi.fn(),
}))
vi.mock('@/lib/workflows/utils', () => ({
getWorkflowById: vi.fn(),
}))
vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock)
vi.mock('@/lib/core/config/feature-flags', () => ({
isHosted: false,
isE2bEnabled: false,
isProd: false,
isDev: false,
isTest: true,
isEmailVerificationEnabled: false,
}))
vi.mock('@/lib/core/config/feature-flags', () => featureFlagsMock)
import { validateProxyUrl } from '@/lib/core/security/input-validation'
import { POST } from '@/app/api/function/execute/route'
@@ -147,7 +137,7 @@ describe('Function Execute API Route', () => {
beforeEach(() => {
vi.clearAllMocks()
mockCheckInternalAuth.mockResolvedValue({
hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValue({
success: true,
userId: 'user-123',
authType: 'internal_jwt',
@@ -164,7 +154,7 @@ describe('Function Execute API Route', () => {
describe('Security Tests', () => {
it('should reject unauthorized requests', async () => {
mockCheckInternalAuth.mockResolvedValueOnce({
hybridAuthMockFns.mockCheckInternalAuth.mockResolvedValueOnce({
success: false,
error: 'Unauthorized',
})

View File

@@ -1,51 +1,25 @@
/**
* @vitest-environment node
*/
import { hybridAuthMockFns, workflowsUtilsMock, workflowsUtilsMockFns } from '@sim/testing'
import type { NextRequest } from 'next/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
mockCheckHybridAuth,
mockGetJobQueue,
mockVerifyWorkflowAccess,
mockGetWorkflowById,
mockGetJob,
} = vi.hoisted(() => ({
mockCheckHybridAuth: vi.fn(),
const { mockGetJobQueue, mockVerifyWorkflowAccess, mockGetJob } = vi.hoisted(() => ({
mockGetJobQueue: vi.fn(),
mockVerifyWorkflowAccess: vi.fn(),
mockGetWorkflowById: vi.fn(),
mockGetJob: vi.fn(),
}))
vi.mock('@sim/logger', () => ({
createLogger: () => ({
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
}),
}))
vi.mock('@/lib/auth/hybrid', () => ({
checkHybridAuth: mockCheckHybridAuth,
}))
vi.mock('@/lib/core/async-jobs', () => ({
getJobQueue: mockGetJobQueue,
}))
vi.mock('@/lib/core/utils/request', () => ({
generateRequestId: vi.fn().mockReturnValue('request-1'),
}))
vi.mock('@/socket/middleware/permissions', () => ({
verifyWorkflowAccess: mockVerifyWorkflowAccess,
}))
vi.mock('@/lib/workflows/utils', () => ({
getWorkflowById: mockGetWorkflowById,
}))
vi.mock('@/lib/workflows/utils', () => workflowsUtilsMock)
import { GET } from './route'
@@ -61,7 +35,7 @@ describe('GET /api/jobs/[jobId]', () => {
beforeEach(() => {
vi.clearAllMocks()
mockCheckHybridAuth.mockResolvedValue({
hybridAuthMockFns.mockCheckHybridAuth.mockResolvedValue({
success: true,
userId: 'user-1',
apiKeyType: undefined,
@@ -69,7 +43,7 @@ describe('GET /api/jobs/[jobId]', () => {
})
mockVerifyWorkflowAccess.mockResolvedValue({ hasAccess: true })
mockGetWorkflowById.mockResolvedValue({
workflowsUtilsMockFns.mockGetWorkflowById.mockResolvedValue({
id: 'workflow-1',
workspaceId: 'workspace-1',
})

View File

@@ -1,8 +1,8 @@
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { type NextRequest, NextResponse } from 'next/server'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { getJobQueue } from '@/lib/core/async-jobs'
import { toError } from '@/lib/core/utils/helpers'
import { generateRequestId } from '@/lib/core/utils/request'
import { createErrorResponse } from '@/app/api/workflows/utils'

View File

@@ -1,10 +1,17 @@
/**
* @vitest-environment node
*/
import { createMockRequest } from '@sim/testing'
import {
auditMock,
createMockRequest,
hybridAuthMockFns,
knowledgeApiUtilsMock,
knowledgeApiUtilsMockFns,
requestUtilsMockFns,
} from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockCheckSession, mockCheckAccess, mockCheckWriteAccess, mockDbChain } = vi.hoisted(() => {
const { mockDbChain } = vi.hoisted(() => {
const chain = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
@@ -15,49 +22,15 @@ const { mockCheckSession, mockCheckAccess, mockCheckWriteAccess, mockDbChain } =
set: vi.fn().mockReturnThis(),
returning: vi.fn().mockResolvedValue([]),
}
return {
mockCheckSession: vi.fn(),
mockCheckAccess: vi.fn(),
mockCheckWriteAccess: vi.fn(),
mockDbChain: chain,
}
return { mockDbChain: chain }
})
const mockCheckAccess = knowledgeApiUtilsMockFns.mockCheckKnowledgeBaseAccess
const mockCheckWriteAccess = knowledgeApiUtilsMockFns.mockCheckKnowledgeBaseWriteAccess
vi.mock('@sim/db', () => ({ db: mockDbChain }))
vi.mock('@sim/db/schema', () => ({
document: {
id: 'id',
connectorId: 'connectorId',
deletedAt: 'deletedAt',
filename: 'filename',
externalId: 'externalId',
sourceUrl: 'sourceUrl',
enabled: 'enabled',
userExcluded: 'userExcluded',
uploadedAt: 'uploadedAt',
processingStatus: 'processingStatus',
},
knowledgeConnector: {
id: 'id',
knowledgeBaseId: 'knowledgeBaseId',
deletedAt: 'deletedAt',
},
}))
vi.mock('@/app/api/knowledge/utils', () => ({
checkKnowledgeBaseAccess: mockCheckAccess,
checkKnowledgeBaseWriteAccess: mockCheckWriteAccess,
}))
vi.mock('@/lib/auth/hybrid', () => ({
checkSessionOrInternalAuth: mockCheckSession,
}))
vi.mock('@/lib/core/utils/request', () => ({
generateRequestId: vi.fn().mockReturnValue('test-req-id'),
}))
vi.mock('@/lib/audit/log', () => ({
recordAudit: vi.fn(),
AuditAction: {},
AuditResourceType: {},
}))
vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock)
vi.mock('@/lib/audit/log', () => auditMock)
import { GET, PATCH } from '@/app/api/knowledge/[id]/connectors/[connectorId]/documents/route'
@@ -66,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()
@@ -78,7 +52,10 @@ describe('Connector Documents API Route', () => {
describe('GET', () => {
it('returns 401 when unauthenticated', async () => {
mockCheckSession.mockResolvedValue({ success: false, userId: null })
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: false,
userId: null,
})
const req = createMockRequest('GET')
const response = await GET(req as never, { params: mockParams })
@@ -87,7 +64,10 @@ describe('Connector Documents API Route', () => {
})
it('returns 404 when connector not found', async () => {
mockCheckSession.mockResolvedValue({ success: true, userId: 'user-1' })
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true,
userId: 'user-1',
})
mockCheckAccess.mockResolvedValue({ hasAccess: true })
mockDbChain.limit.mockResolvedValueOnce([])
@@ -98,7 +78,10 @@ describe('Connector Documents API Route', () => {
})
it('returns documents list on success', async () => {
mockCheckSession.mockResolvedValue({ success: true, userId: 'user-1' })
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true,
userId: 'user-1',
})
mockCheckAccess.mockResolvedValue({ hasAccess: true })
const doc = { id: 'doc-1', filename: 'test.txt', userExcluded: false }
@@ -118,7 +101,10 @@ describe('Connector Documents API Route', () => {
})
it('includes excluded documents when includeExcluded=true', async () => {
mockCheckSession.mockResolvedValue({ success: true, userId: 'user-1' })
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true,
userId: 'user-1',
})
mockCheckAccess.mockResolvedValue({ hasAccess: true })
mockDbChain.limit.mockResolvedValueOnce([{ id: 'conn-456' }])
@@ -142,7 +128,10 @@ describe('Connector Documents API Route', () => {
describe('PATCH', () => {
it('returns 401 when unauthenticated', async () => {
mockCheckSession.mockResolvedValue({ success: false, userId: null })
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: false,
userId: null,
})
const req = createMockRequest('PATCH', { operation: 'restore', documentIds: ['doc-1'] })
const response = await PATCH(req as never, { params: mockParams })
@@ -151,7 +140,10 @@ describe('Connector Documents API Route', () => {
})
it('returns 400 for invalid body', async () => {
mockCheckSession.mockResolvedValue({ success: true, userId: 'user-1' })
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true,
userId: 'user-1',
})
mockCheckWriteAccess.mockResolvedValue({ hasAccess: true })
mockDbChain.limit.mockResolvedValueOnce([{ id: 'conn-456' }])
@@ -162,7 +154,10 @@ describe('Connector Documents API Route', () => {
})
it('returns 404 when connector not found', async () => {
mockCheckSession.mockResolvedValue({ success: true, userId: 'user-1' })
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true,
userId: 'user-1',
})
mockCheckWriteAccess.mockResolvedValue({ hasAccess: true })
mockDbChain.limit.mockResolvedValueOnce([])
@@ -173,7 +168,7 @@ describe('Connector Documents API Route', () => {
})
it('returns success for restore operation', async () => {
mockCheckSession.mockResolvedValue({
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true,
userId: 'user-1',
userName: 'Test',
@@ -195,7 +190,7 @@ describe('Connector Documents API Route', () => {
})
it('returns success for exclude operation', async () => {
mockCheckSession.mockResolvedValue({
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true,
userId: 'user-1',
userName: 'Test',

View File

@@ -1,69 +1,44 @@
/**
* @vitest-environment node
*/
import { createMockRequest } from '@sim/testing'
import {
auditMock,
authOAuthUtilsMock,
createMockRequest,
hybridAuthMockFns,
knowledgeApiUtilsMock,
knowledgeApiUtilsMockFns,
} from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockCheckSession, mockCheckAccess, mockCheckWriteAccess, mockDbChain, mockValidateConfig } =
vi.hoisted(() => {
const chain = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockReturnThis(),
limit: vi.fn().mockResolvedValue([]),
execute: vi.fn().mockResolvedValue(undefined),
transaction: vi.fn(),
insert: vi.fn().mockReturnThis(),
values: vi.fn().mockResolvedValue(undefined),
update: vi.fn().mockReturnThis(),
delete: vi.fn().mockReturnThis(),
set: vi.fn().mockReturnThis(),
returning: vi.fn().mockResolvedValue([]),
}
return {
mockCheckSession: vi.fn(),
mockCheckAccess: vi.fn(),
mockCheckWriteAccess: vi.fn(),
mockDbChain: chain,
mockValidateConfig: vi.fn(),
}
})
const { mockDbChain, mockValidateConfig } = vi.hoisted(() => {
const chain = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
where: vi.fn().mockReturnThis(),
orderBy: vi.fn().mockReturnThis(),
limit: vi.fn().mockResolvedValue([]),
execute: vi.fn().mockResolvedValue(undefined),
transaction: vi.fn(),
insert: vi.fn().mockReturnThis(),
values: vi.fn().mockResolvedValue(undefined),
update: vi.fn().mockReturnThis(),
delete: vi.fn().mockReturnThis(),
set: vi.fn().mockReturnThis(),
returning: vi.fn().mockResolvedValue([]),
}
return {
mockDbChain: chain,
mockValidateConfig: vi.fn(),
}
})
const mockCheckAccess = knowledgeApiUtilsMockFns.mockCheckKnowledgeBaseAccess
const mockCheckWriteAccess = knowledgeApiUtilsMockFns.mockCheckKnowledgeBaseWriteAccess
vi.mock('@sim/db', () => ({ db: mockDbChain }))
vi.mock('@sim/db/schema', () => ({
document: {
id: 'id',
connectorId: 'connectorId',
fileUrl: 'fileUrl',
archivedAt: 'archivedAt',
deletedAt: 'deletedAt',
},
embedding: { documentId: 'documentId' },
knowledgeBase: { id: 'id', userId: 'userId' },
knowledgeConnector: {
id: 'id',
knowledgeBaseId: 'knowledgeBaseId',
archivedAt: 'archivedAt',
deletedAt: 'deletedAt',
connectorType: 'connectorType',
credentialId: 'credentialId',
},
knowledgeConnectorSyncLog: { connectorId: 'connectorId', startedAt: 'startedAt' },
}))
vi.mock('@/app/api/knowledge/utils', () => ({
checkKnowledgeBaseAccess: mockCheckAccess,
checkKnowledgeBaseWriteAccess: mockCheckWriteAccess,
}))
vi.mock('@/lib/auth/hybrid', () => ({
checkSessionOrInternalAuth: mockCheckSession,
}))
vi.mock('@/lib/core/utils/request', () => ({
generateRequestId: vi.fn().mockReturnValue('test-req-id'),
}))
vi.mock('@/app/api/auth/oauth/utils', () => ({
refreshAccessTokenIfNeeded: vi.fn(),
}))
vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock)
vi.mock('@/app/api/auth/oauth/utils', () => authOAuthUtilsMock)
vi.mock('@/connectors/registry', () => ({
CONNECTOR_REGISTRY: {
jira: { validateConfig: mockValidateConfig },
@@ -75,11 +50,7 @@ vi.mock('@/lib/knowledge/tags/service', () => ({
vi.mock('@/lib/knowledge/documents/service', () => ({
deleteDocumentStorageFiles: vi.fn().mockResolvedValue(undefined),
}))
vi.mock('@/lib/audit/log', () => ({
recordAudit: vi.fn(),
AuditAction: {},
AuditResourceType: {},
}))
vi.mock('@/lib/audit/log', () => auditMock)
import { DELETE, GET, PATCH } from '@/app/api/knowledge/[id]/connectors/[connectorId]/route'
@@ -105,7 +76,10 @@ describe('Knowledge Connector By ID API Route', () => {
describe('GET', () => {
it('returns 401 when unauthenticated', async () => {
mockCheckSession.mockResolvedValue({ success: false, userId: null })
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: false,
userId: null,
})
const req = createMockRequest('GET')
const response = await GET(req, { params: mockParams })
@@ -114,7 +88,10 @@ describe('Knowledge Connector By ID API Route', () => {
})
it('returns 404 when KB not found', async () => {
mockCheckSession.mockResolvedValue({ success: true, userId: 'user-1' })
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true,
userId: 'user-1',
})
mockCheckAccess.mockResolvedValue({ hasAccess: false, notFound: true })
const req = createMockRequest('GET')
@@ -124,7 +101,10 @@ describe('Knowledge Connector By ID API Route', () => {
})
it('returns 404 when connector not found', async () => {
mockCheckSession.mockResolvedValue({ success: true, userId: 'user-1' })
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true,
userId: 'user-1',
})
mockCheckAccess.mockResolvedValue({ hasAccess: true })
mockDbChain.limit.mockResolvedValueOnce([])
@@ -135,7 +115,10 @@ describe('Knowledge Connector By ID API Route', () => {
})
it('returns connector with sync logs on success', async () => {
mockCheckSession.mockResolvedValue({ success: true, userId: 'user-1' })
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true,
userId: 'user-1',
})
mockCheckAccess.mockResolvedValue({ hasAccess: true })
const mockConnector = { id: 'conn-456', connectorType: 'jira', status: 'active' }
@@ -156,7 +139,10 @@ describe('Knowledge Connector By ID API Route', () => {
describe('PATCH', () => {
it('returns 401 when unauthenticated', async () => {
mockCheckSession.mockResolvedValue({ success: false, userId: null })
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: false,
userId: null,
})
const req = createMockRequest('PATCH', { status: 'paused' })
const response = await PATCH(req, { params: mockParams })
@@ -165,7 +151,10 @@ describe('Knowledge Connector By ID API Route', () => {
})
it('returns 400 for invalid body', async () => {
mockCheckSession.mockResolvedValue({ success: true, userId: 'user-1' })
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true,
userId: 'user-1',
})
mockCheckWriteAccess.mockResolvedValue({ hasAccess: true })
const req = createMockRequest('PATCH', { syncIntervalMinutes: 'not a number' })
@@ -177,7 +166,10 @@ describe('Knowledge Connector By ID API Route', () => {
})
it('returns 404 when connector not found during sourceConfig validation', async () => {
mockCheckSession.mockResolvedValue({ success: true, userId: 'user-1' })
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true,
userId: 'user-1',
})
mockCheckWriteAccess.mockResolvedValue({ hasAccess: true })
mockDbChain.limit.mockResolvedValueOnce([])
@@ -188,7 +180,7 @@ describe('Knowledge Connector By ID API Route', () => {
})
it('returns 200 and updates status', async () => {
mockCheckSession.mockResolvedValue({
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true,
userId: 'user-1',
userName: 'Test',
@@ -214,7 +206,10 @@ describe('Knowledge Connector By ID API Route', () => {
describe('DELETE', () => {
it('returns 401 when unauthenticated', async () => {
mockCheckSession.mockResolvedValue({ success: false, userId: null })
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: false,
userId: null,
})
const req = createMockRequest('DELETE')
const response = await DELETE(req, { params: mockParams })
@@ -223,7 +218,7 @@ describe('Knowledge Connector By ID API Route', () => {
})
it('returns 200 on successful hard-delete', async () => {
mockCheckSession.mockResolvedValue({
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true,
userId: 'user-1',
userName: 'Test',

View File

@@ -1,10 +1,17 @@
/**
* @vitest-environment node
*/
import { createMockRequest } from '@sim/testing'
import {
auditMock,
createMockRequest,
hybridAuthMockFns,
knowledgeApiUtilsMock,
knowledgeApiUtilsMockFns,
requestUtilsMockFns,
} from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockCheckSession, mockCheckWriteAccess, mockDispatchSync, mockDbChain } = vi.hoisted(() => {
const { mockDispatchSync, mockDbChain } = vi.hoisted(() => {
const chain = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
@@ -15,39 +22,19 @@ const { mockCheckSession, mockCheckWriteAccess, mockDispatchSync, mockDbChain }
set: vi.fn().mockReturnThis(),
}
return {
mockCheckSession: vi.fn(),
mockCheckWriteAccess: vi.fn(),
mockDispatchSync: vi.fn().mockResolvedValue(undefined),
mockDbChain: chain,
}
})
const mockCheckWriteAccess = knowledgeApiUtilsMockFns.mockCheckKnowledgeBaseWriteAccess
vi.mock('@sim/db', () => ({ db: mockDbChain }))
vi.mock('@sim/db/schema', () => ({
knowledgeConnector: {
id: 'id',
knowledgeBaseId: 'knowledgeBaseId',
deletedAt: 'deletedAt',
status: 'status',
},
}))
vi.mock('@/app/api/knowledge/utils', () => ({
checkKnowledgeBaseWriteAccess: mockCheckWriteAccess,
}))
vi.mock('@/lib/auth/hybrid', () => ({
checkSessionOrInternalAuth: mockCheckSession,
}))
vi.mock('@/lib/core/utils/request', () => ({
generateRequestId: vi.fn().mockReturnValue('test-req-id'),
}))
vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock)
vi.mock('@/lib/knowledge/connectors/sync-engine', () => ({
dispatchSync: mockDispatchSync,
}))
vi.mock('@/lib/audit/log', () => ({
recordAudit: vi.fn(),
AuditAction: {},
AuditResourceType: {},
}))
vi.mock('@/lib/audit/log', () => auditMock)
import { POST } from '@/app/api/knowledge/[id]/connectors/[connectorId]/sync/route'
@@ -56,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()
@@ -66,7 +54,10 @@ describe('Connector Manual Sync API Route', () => {
})
it('returns 401 when unauthenticated', async () => {
mockCheckSession.mockResolvedValue({ success: false, userId: null })
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: false,
userId: null,
})
const req = createMockRequest('POST')
const response = await POST(req as never, { params: mockParams })
@@ -75,7 +66,10 @@ describe('Connector Manual Sync API Route', () => {
})
it('returns 404 when connector not found', async () => {
mockCheckSession.mockResolvedValue({ success: true, userId: 'user-1' })
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true,
userId: 'user-1',
})
mockCheckWriteAccess.mockResolvedValue({ hasAccess: true })
mockDbChain.limit.mockResolvedValueOnce([])
@@ -86,7 +80,10 @@ describe('Connector Manual Sync API Route', () => {
})
it('returns 409 when connector is syncing', async () => {
mockCheckSession.mockResolvedValue({ success: true, userId: 'user-1' })
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true,
userId: 'user-1',
})
mockCheckWriteAccess.mockResolvedValue({ hasAccess: true })
mockDbChain.limit.mockResolvedValueOnce([{ id: 'conn-456', status: 'syncing' }])
@@ -97,7 +94,7 @@ describe('Connector Manual Sync API Route', () => {
})
it('dispatches sync on valid request', async () => {
mockCheckSession.mockResolvedValue({
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValue({
success: true,
userId: 'user-1',
userName: 'Test',

View File

@@ -1,6 +1,7 @@
import { db } from '@sim/db'
import { knowledgeBase, knowledgeBaseTagDefinitions, knowledgeConnector } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, desc, eq, isNull, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
@@ -9,7 +10,6 @@ import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { hasLiveSyncAccess } from '@/lib/billing/core/subscription'
import { generateRequestId } from '@/lib/core/utils/request'
import { generateId } from '@/lib/core/utils/uuid'
import { dispatchSync } from '@/lib/knowledge/connectors/sync-engine'
import { allocateTagSlots } from '@/lib/knowledge/constants'
import { createTagDefinition } from '@/lib/knowledge/tags/service'

View File

@@ -1,8 +1,8 @@
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { generateId } from '@/lib/core/utils/uuid'
import { deleteChunk, updateChunk } from '@/lib/knowledge/chunks/service'
import { checkChunkAccess } from '@/app/api/knowledge/utils'

View File

@@ -3,11 +3,10 @@
*
* @vitest-environment node
*/
import { auditMock, createMockRequest } from '@sim/testing'
import { auditMock, authMockFns, createMockRequest, knowledgeApiUtilsMock } from '@sim/testing'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { mockGetSession, mockDbChain } = vi.hoisted(() => {
const mockGetSession = vi.fn()
const { mockDbChain } = vi.hoisted(() => {
const mockDbChain = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
@@ -18,95 +17,14 @@ const { mockGetSession, mockDbChain } = vi.hoisted(() => {
delete: vi.fn().mockReturnThis(),
transaction: vi.fn(),
}
return { mockGetSession, mockDbChain }
return { mockDbChain }
})
vi.mock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
vi.mock('@sim/db', () => ({
db: mockDbChain,
}))
vi.mock('@sim/db/schema', () => ({
knowledgeBase: {
id: 'kb_id',
userId: 'user_id',
name: 'kb_name',
description: 'description',
tokenCount: 'token_count',
embeddingModel: 'embedding_model',
embeddingDimension: 'embedding_dimension',
chunkingConfig: 'chunking_config',
workspaceId: 'workspace_id',
createdAt: 'created_at',
updatedAt: 'updated_at',
deletedAt: 'deleted_at',
},
document: {
id: 'doc_id',
knowledgeBaseId: 'kb_id',
filename: 'filename',
fileUrl: 'file_url',
fileSize: 'file_size',
mimeType: 'mime_type',
chunkCount: 'chunk_count',
tokenCount: 'token_count',
characterCount: 'character_count',
processingStatus: 'processing_status',
processingStartedAt: 'processing_started_at',
processingCompletedAt: 'processing_completed_at',
processingError: 'processing_error',
enabled: 'enabled',
tag1: 'tag1',
tag2: 'tag2',
tag3: 'tag3',
tag4: 'tag4',
tag5: 'tag5',
tag6: 'tag6',
tag7: 'tag7',
uploadedAt: 'uploaded_at',
deletedAt: 'deleted_at',
},
embedding: {
id: 'embedding_id',
documentId: 'doc_id',
knowledgeBaseId: 'kb_id',
chunkIndex: 'chunk_index',
content: 'content',
embedding: 'embedding',
tokenCount: 'token_count',
characterCount: 'character_count',
tag1: 'tag1',
tag2: 'tag2',
tag3: 'tag3',
tag4: 'tag4',
tag5: 'tag5',
tag6: 'tag6',
tag7: 'tag7',
createdAt: 'created_at',
},
permissions: {
id: 'permission_id',
userId: 'user_id',
entityType: 'entity_type',
entityId: 'entity_id',
permissionType: 'permission_type',
createdAt: 'created_at',
updatedAt: 'updated_at',
},
}))
vi.mock('@/app/api/knowledge/utils', () => ({
checkKnowledgeBaseAccess: vi.fn(),
checkKnowledgeBaseWriteAccess: vi.fn(),
checkDocumentAccess: vi.fn(),
checkDocumentWriteAccess: vi.fn(),
checkChunkAccess: vi.fn(),
generateEmbeddings: vi.fn(),
processDocumentAsync: vi.fn(),
}))
vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock)
vi.mock('@/lib/knowledge/documents/service', () => ({
updateDocument: vi.fn(),
@@ -192,7 +110,9 @@ describe('Document By ID API Route', () => {
const mockParams = Promise.resolve({ id: 'kb-123', documentId: 'doc-123' })
it('should retrieve document successfully for authenticated user', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkDocumentAccess).mockResolvedValue({
hasAccess: true,
document: mockDocument,
@@ -211,7 +131,7 @@ describe('Document By ID API Route', () => {
})
it('should return unauthorized for unauthenticated user', async () => {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
const req = createMockRequest('GET')
const response = await GET(req, { params: mockParams })
@@ -222,7 +142,9 @@ describe('Document By ID API Route', () => {
})
it('should return not found for non-existent document', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkDocumentAccess).mockResolvedValue({
hasAccess: false,
notFound: true,
@@ -238,7 +160,9 @@ describe('Document By ID API Route', () => {
})
it('should return unauthorized for document without access', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkDocumentAccess).mockResolvedValue({
hasAccess: false,
reason: 'Access denied',
@@ -253,7 +177,9 @@ describe('Document By ID API Route', () => {
})
it('should handle database errors', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkDocumentAccess).mockRejectedValue(new Error('Database error'))
const req = createMockRequest('GET')
@@ -275,7 +201,9 @@ describe('Document By ID API Route', () => {
}
it('should update document successfully', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkDocumentWriteAccess).mockResolvedValue({
hasAccess: true,
document: mockDocument,
@@ -305,7 +233,9 @@ describe('Document By ID API Route', () => {
})
it('should validate update data', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkDocumentWriteAccess).mockResolvedValue({
hasAccess: true,
document: mockDocument,
@@ -338,7 +268,9 @@ describe('Document By ID API Route', () => {
processingStartedAt: new Date(Date.now() - 200000), // 200 seconds ago
}
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkDocumentWriteAccess).mockResolvedValue({
hasAccess: true,
document: processingDocument,
@@ -367,7 +299,9 @@ describe('Document By ID API Route', () => {
})
it('should reject marking failed for non-processing document', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkDocumentWriteAccess).mockResolvedValue({
hasAccess: true,
document: { ...mockDocument, processingStatus: 'completed' },
@@ -389,7 +323,9 @@ describe('Document By ID API Route', () => {
processingStartedAt: new Date(Date.now() - 60000), // 60 seconds ago
}
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkDocumentWriteAccess).mockResolvedValue({
hasAccess: true,
document: recentProcessingDocument,
@@ -419,7 +355,9 @@ describe('Document By ID API Route', () => {
processingError: 'Previous processing failed',
}
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkDocumentWriteAccess).mockResolvedValue({
hasAccess: true,
document: failedDocument,
@@ -454,7 +392,9 @@ describe('Document By ID API Route', () => {
})
it('should reject retry for non-failed document', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkDocumentWriteAccess).mockResolvedValue({
hasAccess: true,
document: { ...mockDocument, processingStatus: 'completed' },
@@ -475,7 +415,7 @@ describe('Document By ID API Route', () => {
const validUpdateData = { filename: 'updated-document.pdf' }
it('should return unauthorized for unauthenticated user', async () => {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
const req = createMockRequest('PUT', validUpdateData)
const response = await PUT(req, { params: mockParams })
@@ -486,7 +426,9 @@ describe('Document By ID API Route', () => {
})
it('should return not found for non-existent document', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkDocumentWriteAccess).mockResolvedValue({
hasAccess: false,
notFound: true,
@@ -502,7 +444,9 @@ describe('Document By ID API Route', () => {
})
it('should handle database errors during update', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkDocumentWriteAccess).mockResolvedValue({
hasAccess: true,
document: mockDocument,
@@ -524,7 +468,9 @@ describe('Document By ID API Route', () => {
const mockParams = Promise.resolve({ id: 'kb-123', documentId: 'doc-123' })
it('should delete document successfully', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkDocumentWriteAccess).mockResolvedValue({
hasAccess: true,
document: mockDocument,
@@ -547,7 +493,7 @@ describe('Document By ID API Route', () => {
})
it('should return unauthorized for unauthenticated user', async () => {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
const req = createMockRequest('DELETE')
const response = await DELETE(req, { params: mockParams })
@@ -558,7 +504,9 @@ describe('Document By ID API Route', () => {
})
it('should return not found for non-existent document', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkDocumentWriteAccess).mockResolvedValue({
hasAccess: false,
notFound: true,
@@ -574,7 +522,9 @@ describe('Document By ID API Route', () => {
})
it('should return unauthorized for document without access', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkDocumentWriteAccess).mockResolvedValue({
hasAccess: false,
reason: 'Access denied',
@@ -589,7 +539,9 @@ describe('Document By ID API Route', () => {
})
it('should handle database errors during deletion', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkDocumentWriteAccess).mockResolvedValue({
hasAccess: true,
document: mockDocument,

View File

@@ -1,8 +1,8 @@
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { getSession } from '@/lib/auth'
import { generateId } from '@/lib/core/utils/uuid'
import { SUPPORTED_FIELD_TYPES } from '@/lib/knowledge/constants'
import {
cleanupUnusedTagDefinitions,

View File

@@ -3,11 +3,10 @@
*
* @vitest-environment node
*/
import { auditMock, createMockRequest } from '@sim/testing'
import { auditMock, authMockFns, createMockRequest, knowledgeApiUtilsMock } from '@sim/testing'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { mockGetSession, mockDbChain } = vi.hoisted(() => {
const mockGetSession = vi.fn()
const { mockDbChain } = vi.hoisted(() => {
const mockDbChain = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
@@ -21,95 +20,14 @@ const { mockGetSession, mockDbChain } = vi.hoisted(() => {
set: vi.fn().mockReturnThis(),
transaction: vi.fn(),
}
return { mockGetSession, mockDbChain }
return { mockDbChain }
})
vi.mock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
vi.mock('@sim/db', () => ({
db: mockDbChain,
}))
vi.mock('@sim/db/schema', () => ({
knowledgeBase: {
id: 'kb_id',
userId: 'user_id',
name: 'kb_name',
description: 'description',
tokenCount: 'token_count',
embeddingModel: 'embedding_model',
embeddingDimension: 'embedding_dimension',
chunkingConfig: 'chunking_config',
workspaceId: 'workspace_id',
createdAt: 'created_at',
updatedAt: 'updated_at',
deletedAt: 'deleted_at',
},
document: {
id: 'doc_id',
knowledgeBaseId: 'kb_id',
filename: 'filename',
fileUrl: 'file_url',
fileSize: 'file_size',
mimeType: 'mime_type',
chunkCount: 'chunk_count',
tokenCount: 'token_count',
characterCount: 'character_count',
processingStatus: 'processing_status',
processingStartedAt: 'processing_started_at',
processingCompletedAt: 'processing_completed_at',
processingError: 'processing_error',
enabled: 'enabled',
tag1: 'tag1',
tag2: 'tag2',
tag3: 'tag3',
tag4: 'tag4',
tag5: 'tag5',
tag6: 'tag6',
tag7: 'tag7',
uploadedAt: 'uploaded_at',
deletedAt: 'deleted_at',
},
embedding: {
id: 'embedding_id',
documentId: 'doc_id',
knowledgeBaseId: 'kb_id',
chunkIndex: 'chunk_index',
content: 'content',
embedding: 'embedding',
tokenCount: 'token_count',
characterCount: 'character_count',
tag1: 'tag1',
tag2: 'tag2',
tag3: 'tag3',
tag4: 'tag4',
tag5: 'tag5',
tag6: 'tag6',
tag7: 'tag7',
createdAt: 'created_at',
},
permissions: {
id: 'permission_id',
userId: 'user_id',
entityType: 'entity_type',
entityId: 'entity_id',
permissionType: 'permission_type',
createdAt: 'created_at',
updatedAt: 'updated_at',
},
}))
vi.mock('@/app/api/knowledge/utils', () => ({
checkKnowledgeBaseAccess: vi.fn(),
checkKnowledgeBaseWriteAccess: vi.fn(),
checkDocumentAccess: vi.fn(),
checkDocumentWriteAccess: vi.fn(),
checkChunkAccess: vi.fn(),
generateEmbeddings: vi.fn(),
processDocumentAsync: vi.fn(),
}))
vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock)
vi.mock('@/lib/knowledge/documents/service', () => ({
getDocuments: vi.fn(),
@@ -201,7 +119,9 @@ describe('Knowledge Base Documents API Route', () => {
const mockParams = Promise.resolve({ id: 'kb-123' })
it('should retrieve documents successfully for authenticated user', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkKnowledgeBaseAccess).mockResolvedValue({
hasAccess: true,
knowledgeBase: { id: 'kb-123', userId: 'user-123' },
@@ -239,7 +159,9 @@ describe('Knowledge Base Documents API Route', () => {
})
it('should return documents with default filter', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkKnowledgeBaseAccess).mockResolvedValue({
hasAccess: true,
knowledgeBase: { id: 'kb-123', userId: 'user-123' },
@@ -272,7 +194,9 @@ describe('Knowledge Base Documents API Route', () => {
})
it('should filter documents by enabled status when requested', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkKnowledgeBaseAccess).mockResolvedValue({
hasAccess: true,
knowledgeBase: { id: 'kb-123', userId: 'user-123' },
@@ -307,7 +231,7 @@ describe('Knowledge Base Documents API Route', () => {
})
it('should return unauthorized for unauthenticated user', async () => {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
const req = createMockRequest('GET')
const response = await GET(req, { params: mockParams })
@@ -318,7 +242,9 @@ describe('Knowledge Base Documents API Route', () => {
})
it('should return not found for non-existent knowledge base', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkKnowledgeBaseAccess).mockResolvedValue({
hasAccess: false,
notFound: true,
@@ -333,7 +259,9 @@ describe('Knowledge Base Documents API Route', () => {
})
it('should return unauthorized for knowledge base without access', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkKnowledgeBaseAccess).mockResolvedValue({ hasAccess: false })
const req = createMockRequest('GET')
@@ -345,7 +273,9 @@ describe('Knowledge Base Documents API Route', () => {
})
it('should handle database errors', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkKnowledgeBaseAccess).mockResolvedValue({
hasAccess: true,
knowledgeBase: { id: 'kb-123', userId: 'user-123' },
@@ -371,7 +301,9 @@ describe('Knowledge Base Documents API Route', () => {
}
it('should create single document successfully', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkKnowledgeBaseWriteAccess).mockResolvedValue({
hasAccess: true,
knowledgeBase: { id: 'kb-123', userId: 'user-123' },
@@ -415,7 +347,9 @@ describe('Knowledge Base Documents API Route', () => {
})
it('should validate single document data', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkKnowledgeBaseWriteAccess).mockResolvedValue({
hasAccess: true,
knowledgeBase: { id: 'kb-123', userId: 'user-123' },
@@ -463,7 +397,9 @@ describe('Knowledge Base Documents API Route', () => {
}
it('should create bulk documents successfully', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkKnowledgeBaseWriteAccess).mockResolvedValue({
hasAccess: true,
knowledgeBase: { id: 'kb-123', userId: 'user-123' },
@@ -513,7 +449,9 @@ describe('Knowledge Base Documents API Route', () => {
})
it('should validate bulk document data', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkKnowledgeBaseWriteAccess).mockResolvedValue({
hasAccess: true,
knowledgeBase: { id: 'kb-123', userId: 'user-123' },
@@ -545,7 +483,9 @@ describe('Knowledge Base Documents API Route', () => {
})
it('should handle processing errors gracefully', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkKnowledgeBaseWriteAccess).mockResolvedValue({
hasAccess: true,
knowledgeBase: { id: 'kb-123', userId: 'user-123' },
@@ -589,7 +529,7 @@ describe('Knowledge Base Documents API Route', () => {
}
it('should return unauthorized for unauthenticated user', async () => {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
const req = createMockRequest('POST', validDocumentData)
const response = await POST(req, { params: mockParams })
@@ -600,7 +540,9 @@ describe('Knowledge Base Documents API Route', () => {
})
it('should return not found for non-existent knowledge base', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkKnowledgeBaseWriteAccess).mockResolvedValue({
hasAccess: false,
notFound: true,
@@ -615,7 +557,9 @@ describe('Knowledge Base Documents API Route', () => {
})
it('should return unauthorized for knowledge base without access', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkKnowledgeBaseWriteAccess).mockResolvedValue({ hasAccess: false })
const req = createMockRequest('POST', validDocumentData)
@@ -627,7 +571,9 @@ describe('Knowledge Base Documents API Route', () => {
})
it('should handle database errors during creation', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkKnowledgeBaseWriteAccess).mockResolvedValue({
hasAccess: true,
knowledgeBase: { id: 'kb-123', userId: 'user-123' },

View File

@@ -1,10 +1,10 @@
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { getSession } from '@/lib/auth'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import {
bulkDocumentOperation,
bulkDocumentOperationByFilter,

View File

@@ -1,12 +1,12 @@
import { db } from '@sim/db'
import { document } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { and, eq, isNull } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import {
createDocumentRecords,
deleteDocument,

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { generateId } from '@/lib/core/utils/uuid'
import { getNextAvailableSlot, getTagDefinitions } from '@/lib/knowledge/tags/service'
import { checkKnowledgeBaseAccess } from '@/app/api/knowledge/utils'

View File

@@ -3,11 +3,10 @@
*
* @vitest-environment node
*/
import { auditMock, createMockRequest } from '@sim/testing'
import { auditMock, authMockFns, createMockRequest, knowledgeApiUtilsMock } from '@sim/testing'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { mockGetSession, mockDbChain } = vi.hoisted(() => {
const mockGetSession = vi.fn()
const { mockDbChain } = vi.hoisted(() => {
const mockDbChain = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
@@ -16,86 +15,13 @@ const { mockGetSession, mockDbChain } = vi.hoisted(() => {
update: vi.fn().mockReturnThis(),
set: vi.fn().mockReturnThis(),
}
return { mockGetSession, mockDbChain }
return { mockDbChain }
})
vi.mock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
vi.mock('@sim/db', () => ({
db: mockDbChain,
}))
vi.mock('@sim/db/schema', () => ({
knowledgeBase: {
id: 'kb_id',
userId: 'user_id',
name: 'kb_name',
description: 'description',
tokenCount: 'token_count',
embeddingModel: 'embedding_model',
embeddingDimension: 'embedding_dimension',
chunkingConfig: 'chunking_config',
workspaceId: 'workspace_id',
createdAt: 'created_at',
updatedAt: 'updated_at',
deletedAt: 'deleted_at',
},
document: {
id: 'doc_id',
knowledgeBaseId: 'kb_id',
filename: 'filename',
fileUrl: 'file_url',
fileSize: 'file_size',
mimeType: 'mime_type',
chunkCount: 'chunk_count',
tokenCount: 'token_count',
characterCount: 'character_count',
processingStatus: 'processing_status',
processingStartedAt: 'processing_started_at',
processingCompletedAt: 'processing_completed_at',
processingError: 'processing_error',
enabled: 'enabled',
tag1: 'tag1',
tag2: 'tag2',
tag3: 'tag3',
tag4: 'tag4',
tag5: 'tag5',
tag6: 'tag6',
tag7: 'tag7',
uploadedAt: 'uploaded_at',
deletedAt: 'deleted_at',
},
embedding: {
id: 'embedding_id',
documentId: 'doc_id',
knowledgeBaseId: 'kb_id',
chunkIndex: 'chunk_index',
content: 'content',
embedding: 'embedding',
tokenCount: 'token_count',
characterCount: 'character_count',
tag1: 'tag1',
tag2: 'tag2',
tag3: 'tag3',
tag4: 'tag4',
tag5: 'tag5',
tag6: 'tag6',
tag7: 'tag7',
createdAt: 'created_at',
},
permissions: {
id: 'permission_id',
userId: 'user_id',
entityType: 'entity_type',
entityId: 'entity_id',
permissionType: 'permission_type',
createdAt: 'created_at',
updatedAt: 'updated_at',
},
}))
vi.mock('@/lib/audit/log', () => auditMock)
vi.mock('@/lib/knowledge/service', async (importOriginal) => {
@@ -108,10 +34,7 @@ vi.mock('@/lib/knowledge/service', async (importOriginal) => {
}
})
vi.mock('@/app/api/knowledge/utils', () => ({
checkKnowledgeBaseAccess: vi.fn(),
checkKnowledgeBaseWriteAccess: vi.fn(),
}))
vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock)
import {
deleteKnowledgeBase,
@@ -162,7 +85,9 @@ describe('Knowledge Base By ID API Route', () => {
const mockParams = Promise.resolve({ id: 'kb-123' })
it('should retrieve knowledge base successfully for authenticated user', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkKnowledgeBaseAccess).mockResolvedValueOnce({
hasAccess: true,
@@ -184,7 +109,7 @@ describe('Knowledge Base By ID API Route', () => {
})
it('should return unauthorized for unauthenticated user', async () => {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
const req = createMockRequest('GET')
const response = await GET(req, { params: mockParams })
@@ -195,7 +120,9 @@ describe('Knowledge Base By ID API Route', () => {
})
it('should return not found for non-existent knowledge base', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkKnowledgeBaseAccess).mockResolvedValueOnce({
hasAccess: false,
@@ -211,7 +138,9 @@ describe('Knowledge Base By ID API Route', () => {
})
it('should return unauthorized for knowledge base owned by different user', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkKnowledgeBaseAccess).mockResolvedValueOnce({
hasAccess: false,
@@ -227,7 +156,9 @@ describe('Knowledge Base By ID API Route', () => {
})
it('should return not found when service returns null', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkKnowledgeBaseAccess).mockResolvedValueOnce({
hasAccess: true,
@@ -245,7 +176,9 @@ describe('Knowledge Base By ID API Route', () => {
})
it('should handle database errors', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkKnowledgeBaseAccess).mockRejectedValueOnce(new Error('Database error'))
@@ -266,7 +199,9 @@ describe('Knowledge Base By ID API Route', () => {
}
it('should update knowledge base successfully', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
resetMocks()
@@ -299,7 +234,7 @@ describe('Knowledge Base By ID API Route', () => {
})
it('should return unauthorized for unauthenticated user', async () => {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
const req = createMockRequest('PUT', validUpdateData)
const response = await PUT(req, { params: mockParams })
@@ -310,7 +245,9 @@ describe('Knowledge Base By ID API Route', () => {
})
it('should return not found for non-existent knowledge base', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
resetMocks()
@@ -328,7 +265,9 @@ describe('Knowledge Base By ID API Route', () => {
})
it('should validate update data', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
resetMocks()
@@ -351,7 +290,9 @@ describe('Knowledge Base By ID API Route', () => {
})
it('should handle database errors during update', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkKnowledgeBaseWriteAccess).mockResolvedValueOnce({
hasAccess: true,
@@ -373,7 +314,9 @@ describe('Knowledge Base By ID API Route', () => {
const mockParams = Promise.resolve({ id: 'kb-123' })
it('should delete knowledge base successfully', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
resetMocks()
@@ -396,7 +339,7 @@ describe('Knowledge Base By ID API Route', () => {
})
it('should return unauthorized for unauthenticated user', async () => {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
const req = createMockRequest('DELETE')
const response = await DELETE(req, { params: mockParams })
@@ -407,7 +350,9 @@ describe('Knowledge Base By ID API Route', () => {
})
it('should return not found for non-existent knowledge base', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
resetMocks()
@@ -425,7 +370,9 @@ describe('Knowledge Base By ID API Route', () => {
})
it('should return unauthorized for knowledge base owned by different user', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
resetMocks()
@@ -443,7 +390,9 @@ describe('Knowledge Base By ID API Route', () => {
})
it('should handle database errors during delete', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
vi.mocked(checkKnowledgeBaseWriteAccess).mockResolvedValueOnce({
hasAccess: true,

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import { deleteTagDefinition } from '@/lib/knowledge/tags/service'
import { checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'

View File

@@ -1,8 +1,8 @@
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { AuthType, checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateId } from '@/lib/core/utils/uuid'
import { SUPPORTED_FIELD_TYPES } from '@/lib/knowledge/constants'
import { createTagDefinition, getTagDefinitions } from '@/lib/knowledge/tags/service'
import { checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'

View File

@@ -1,7 +1,7 @@
import { createLogger } from '@sim/logger'
import { generateId } from '@sim/utils/id'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { generateId } from '@/lib/core/utils/uuid'
import { getTagUsage } from '@/lib/knowledge/tags/service'
import { checkKnowledgeBaseAccess } from '@/app/api/knowledge/utils'

View File

@@ -3,11 +3,16 @@
*
* @vitest-environment node
*/
import { auditMock, createMockRequest } from '@sim/testing'
import {
auditMock,
authMockFns,
createMockRequest,
permissionsMock,
permissionsMockFns,
} from '@sim/testing'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { mockGetSession, mockDbChain } = vi.hoisted(() => {
const mockGetSession = vi.fn()
const { mockDbChain } = vi.hoisted(() => {
const mockDbChain = {
select: vi.fn().mockReturnThis(),
from: vi.fn().mockReturnThis(),
@@ -19,91 +24,16 @@ const { mockGetSession, mockDbChain } = vi.hoisted(() => {
insert: vi.fn().mockReturnThis(),
values: vi.fn().mockResolvedValue(undefined),
}
return { mockGetSession, mockDbChain }
return { mockDbChain }
})
vi.mock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
vi.mock('@sim/db', () => ({
db: mockDbChain,
}))
vi.mock('@sim/db/schema', () => ({
knowledgeBase: {
id: 'kb_id',
userId: 'user_id',
name: 'kb_name',
description: 'description',
tokenCount: 'token_count',
embeddingModel: 'embedding_model',
embeddingDimension: 'embedding_dimension',
chunkingConfig: 'chunking_config',
workspaceId: 'workspace_id',
createdAt: 'created_at',
updatedAt: 'updated_at',
deletedAt: 'deleted_at',
},
document: {
id: 'doc_id',
knowledgeBaseId: 'kb_id',
filename: 'filename',
fileUrl: 'file_url',
fileSize: 'file_size',
mimeType: 'mime_type',
chunkCount: 'chunk_count',
tokenCount: 'token_count',
characterCount: 'character_count',
processingStatus: 'processing_status',
processingStartedAt: 'processing_started_at',
processingCompletedAt: 'processing_completed_at',
processingError: 'processing_error',
enabled: 'enabled',
tag1: 'tag1',
tag2: 'tag2',
tag3: 'tag3',
tag4: 'tag4',
tag5: 'tag5',
tag6: 'tag6',
tag7: 'tag7',
uploadedAt: 'uploaded_at',
deletedAt: 'deleted_at',
},
embedding: {
id: 'embedding_id',
documentId: 'doc_id',
knowledgeBaseId: 'kb_id',
chunkIndex: 'chunk_index',
content: 'content',
embedding: 'embedding',
tokenCount: 'token_count',
characterCount: 'character_count',
tag1: 'tag1',
tag2: 'tag2',
tag3: 'tag3',
tag4: 'tag4',
tag5: 'tag5',
tag6: 'tag6',
tag7: 'tag7',
createdAt: 'created_at',
},
permissions: {
id: 'permission_id',
userId: 'user_id',
entityType: 'entity_type',
entityId: 'entity_id',
permissionType: 'permission_type',
createdAt: 'created_at',
updatedAt: 'updated_at',
},
}))
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'
@@ -120,6 +50,8 @@ describe('Knowledge Base API Route', () => {
}
})
permissionsMockFns.mockGetUserEntityPermissions.mockResolvedValue('admin')
vi.stubGlobal('crypto', {
randomUUID: vi.fn().mockReturnValue('mock-uuid-1234-5678'),
})
@@ -131,7 +63,7 @@ describe('Knowledge Base API Route', () => {
describe('GET /api/knowledge', () => {
it('should return unauthorized for unauthenticated user', async () => {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
const req = createMockRequest('GET')
const response = await GET(req)
@@ -142,7 +74,9 @@ describe('Knowledge Base API Route', () => {
})
it('should handle database errors', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
mockDbChain.orderBy.mockRejectedValue(new Error('Database error'))
const req = createMockRequest('GET')
@@ -167,7 +101,9 @@ describe('Knowledge Base API Route', () => {
}
it('should create knowledge base successfully', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
const req = createMockRequest('POST', validKnowledgeBaseData)
const response = await POST(req)
@@ -181,7 +117,7 @@ describe('Knowledge Base API Route', () => {
})
it('should return unauthorized for unauthenticated user', async () => {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
const req = createMockRequest('POST', validKnowledgeBaseData)
const response = await POST(req)
@@ -192,7 +128,9 @@ describe('Knowledge Base API Route', () => {
})
it('should validate required fields', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
const req = createMockRequest('POST', { description: 'Missing name' })
const response = await POST(req)
@@ -204,7 +142,9 @@ describe('Knowledge Base API Route', () => {
})
it('should require workspaceId', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
const req = createMockRequest('POST', { name: 'Test KB' })
const response = await POST(req)
@@ -216,7 +156,9 @@ describe('Knowledge Base API Route', () => {
})
it('should validate chunking config constraints', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
const invalidData = {
name: 'Test KB',
@@ -237,7 +179,9 @@ describe('Knowledge Base API Route', () => {
})
it('should use default values for optional fields', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
const minimalData = { name: 'Test KB', workspaceId: 'test-workspace-id' }
const req = createMockRequest('POST', minimalData)
@@ -255,7 +199,9 @@ describe('Knowledge Base API Route', () => {
})
it('should handle database errors during creation', async () => {
mockGetSession.mockResolvedValue({ user: { id: 'user-123', email: 'test@example.com' } })
authMockFns.mockGetSession.mockResolvedValue({
user: { id: 'user-123', email: 'test@example.com' },
})
mockDbChain.values.mockRejectedValue(new Error('Database error'))
const req = createMockRequest('POST', validKnowledgeBaseData)

View File

@@ -5,14 +5,19 @@
*
* @vitest-environment node
*/
import { createEnvMock, createMockRequest, requestUtilsMock } from '@sim/testing'
import {
createEnvMock,
createMockRequest,
hybridAuthMockFns,
knowledgeApiUtilsMock,
knowledgeApiUtilsMockFns,
workflowsUtilsMock,
workflowsUtilsMockFns,
} from '@sim/testing'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const {
mockDbChain,
mockCheckSessionOrInternalAuth,
mockAuthorizeWorkflowByWorkspacePermission,
mockCheckKnowledgeBaseAccess,
mockGetDocumentTagDefinitions,
mockHandleTagOnlySearch,
mockHandleVectorOnlySearch,
@@ -32,9 +37,6 @@ const {
groupBy: vi.fn().mockReturnThis(),
having: vi.fn().mockReturnThis(),
},
mockCheckSessionOrInternalAuth: vi.fn(),
mockAuthorizeWorkflowByWorkspacePermission: vi.fn(),
mockCheckKnowledgeBaseAccess: vi.fn(),
mockGetDocumentTagDefinitions: vi.fn(),
mockHandleTagOnlySearch: vi.fn(),
mockHandleVectorOnlySearch: vi.fn(),
@@ -44,6 +46,8 @@ const {
mockGetDocumentNamesByIds: vi.fn(),
}))
const mockCheckKnowledgeBaseAccess = knowledgeApiUtilsMockFns.mockCheckKnowledgeBaseAccess
vi.mock('drizzle-orm', () => ({
and: vi.fn().mockImplementation((...args) => ({ and: args })),
eq: vi.fn().mockImplementation((a, b) => ({ eq: [a, b] })),
@@ -56,92 +60,14 @@ vi.mock('drizzle-orm', () => ({
})),
}))
vi.mock('@sim/db/schema', () => ({
knowledgeBase: {
id: 'kb_id',
userId: 'user_id',
name: 'kb_name',
description: 'description',
tokenCount: 'token_count',
embeddingModel: 'embedding_model',
embeddingDimension: 'embedding_dimension',
chunkingConfig: 'chunking_config',
workspaceId: 'workspace_id',
createdAt: 'created_at',
updatedAt: 'updated_at',
deletedAt: 'deleted_at',
},
document: {
id: 'doc_id',
knowledgeBaseId: 'kb_id',
filename: 'filename',
fileUrl: 'file_url',
fileSize: 'file_size',
mimeType: 'mime_type',
chunkCount: 'chunk_count',
tokenCount: 'token_count',
characterCount: 'character_count',
processingStatus: 'processing_status',
processingStartedAt: 'processing_started_at',
processingCompletedAt: 'processing_completed_at',
processingError: 'processing_error',
enabled: 'enabled',
tag1: 'tag1',
tag2: 'tag2',
tag3: 'tag3',
tag4: 'tag4',
tag5: 'tag5',
tag6: 'tag6',
tag7: 'tag7',
uploadedAt: 'uploaded_at',
deletedAt: 'deleted_at',
},
embedding: {
id: 'embedding_id',
documentId: 'doc_id',
knowledgeBaseId: 'kb_id',
chunkIndex: 'chunk_index',
content: 'content',
embedding: 'embedding',
tokenCount: 'token_count',
characterCount: 'character_count',
tag1: 'tag1',
tag2: 'tag2',
tag3: 'tag3',
tag4: 'tag4',
tag5: 'tag5',
tag6: 'tag6',
tag7: 'tag7',
createdAt: 'created_at',
},
permissions: {
id: 'permission_id',
userId: 'user_id',
entityType: 'entity_type',
entityId: 'entity_id',
permissionType: 'permission_type',
createdAt: 'created_at',
updatedAt: 'updated_at',
},
}))
vi.mock('@sim/db', () => ({
db: mockDbChain,
}))
vi.mock('@/lib/auth/hybrid', () => ({
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
}))
vi.mock('@/lib/workflows/utils', () => ({
authorizeWorkflowByWorkspacePermission: mockAuthorizeWorkflowByWorkspacePermission,
}))
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()),
}))
@@ -163,9 +89,7 @@ vi.mock('@/providers/utils', () => ({
}),
}))
vi.mock('@/app/api/knowledge/utils', () => ({
checkKnowledgeBaseAccess: mockCheckKnowledgeBaseAccess,
}))
vi.mock('@/app/api/knowledge/utils', () => knowledgeApiUtilsMock)
vi.mock('@/lib/knowledge/tags/service', () => ({
getDocumentTagDefinitions: mockGetDocumentTagDefinitions,
@@ -240,12 +164,12 @@ describe('Knowledge Search API Route', () => {
doc2: 'Document 2',
})
mockGetDocumentTagDefinitions.mockClear()
mockCheckSessionOrInternalAuth.mockClear().mockResolvedValue({
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockClear().mockResolvedValue({
success: true,
userId: 'user-123',
authType: 'session',
})
mockAuthorizeWorkflowByWorkspacePermission.mockClear().mockResolvedValue({
workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockClear().mockResolvedValue({
allowed: true,
status: 200,
})
@@ -400,15 +324,17 @@ describe('Knowledge Search API Route', () => {
expect(response.status).toBe(200)
expect(data.success).toBe(true)
expect(mockAuthorizeWorkflowByWorkspacePermission).toHaveBeenCalledWith({
workflowId: 'workflow-123',
userId: 'user-123',
action: 'read',
})
expect(workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission).toHaveBeenCalledWith(
{
workflowId: 'workflow-123',
userId: 'user-123',
action: 'read',
}
)
})
it.concurrent('should return unauthorized for unauthenticated request', async () => {
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
hybridAuthMockFns.mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
success: false,
error: 'Unauthorized',
})
@@ -427,7 +353,7 @@ describe('Knowledge Search API Route', () => {
workflowId: 'nonexistent-workflow',
}
mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({
workflowsUtilsMockFns.mockAuthorizeWorkflowByWorkspacePermission.mockResolvedValueOnce({
allowed: false,
status: 404,
message: 'Workflow not found',

View File

@@ -4,13 +4,11 @@
*
* @vitest-environment node
*/
import { createEnvMock, databaseMock, loggerMock } from '@sim/testing'
import { createEnvMock } from '@sim/testing'
import { mockNextFetchResponse } from '@sim/testing/mocks'
import { beforeEach, describe, expect, it, vi } from 'vitest'
vi.mock('drizzle-orm')
vi.mock('@sim/logger', () => loggerMock)
vi.mock('@sim/db', () => databaseMock)
vi.mock('@/lib/knowledge/documents/utils', () => ({
retryWithExponentialBackoff: (fn: any) => fn(),
}))

View File

@@ -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)

View File

@@ -13,6 +13,8 @@ import {
import { db } from '@sim/db'
import { userStats } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { generateId } from '@sim/utils/id'
import { eq, sql } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { validateOAuthAccessToken } from '@/lib/auth/oauth-token'
@@ -26,9 +28,7 @@ import { prepareExecutionContext } from '@/lib/copilot/tools/handlers/context'
import { DIRECT_TOOL_DEFS, SUBAGENT_TOOL_DEFS } from '@/lib/copilot/tools/mcp/definitions'
import { env } from '@/lib/core/config/env'
import { RateLimiter } from '@/lib/core/rate-limiter'
import { toError } from '@/lib/core/utils/helpers'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { generateId } from '@/lib/core/utils/uuid'
import {
authorizeWorkflowByWorkspacePermission,
resolveWorkflowIdForUser,

View File

@@ -3,26 +3,17 @@
*
* @vitest-environment node
*/
import { createMockRequest } from '@sim/testing'
import { authMockFns, createMockRequest, permissionsMock, permissionsMockFns } from '@sim/testing'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const { mockGetSession, mockGetUserEntityPermissions } = vi.hoisted(() => ({
mockGetSession: vi.fn(),
mockGetUserEntityPermissions: vi.fn(),
}))
const mockGetUserEntityPermissions = permissionsMockFns.mockGetUserEntityPermissions
vi.mock('@/lib/auth', () => ({
getSession: mockGetSession,
}))
vi.mock('@/lib/workspaces/permissions/utils', () => ({
getUserEntityPermissions: mockGetUserEntityPermissions,
}))
vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock)
vi.mock('@/lib/events/sse-endpoint', () => ({
createWorkspaceSSE: (_config: any) => {
return async (request: any) => {
const session = await mockGetSession()
const session = await authMockFns.mockGetSession()
if (!session?.user?.id) {
return new Response('Unauthorized', { status: 401 })
}
@@ -72,7 +63,7 @@ describe('MCP Events SSE Endpoint', () => {
})
it('returns 401 when session is missing', async () => {
mockGetSession.mockResolvedValue(null)
authMockFns.mockGetSession.mockResolvedValue(null)
const request = createMockRequest(
'GET',
@@ -89,7 +80,7 @@ describe('MCP Events SSE Endpoint', () => {
})
it('returns 400 when workspaceId is missing', async () => {
mockGetSession.mockResolvedValue({ user: defaultMockUser })
authMockFns.mockGetSession.mockResolvedValue({ user: defaultMockUser })
const request = createMockRequest('GET', undefined, {}, 'http://localhost:3000/api/mcp/events')
@@ -101,7 +92,7 @@ describe('MCP Events SSE Endpoint', () => {
})
it('returns 403 when user lacks workspace access', async () => {
mockGetSession.mockResolvedValue({ user: defaultMockUser })
authMockFns.mockGetSession.mockResolvedValue({ user: defaultMockUser })
mockGetUserEntityPermissions.mockResolvedValue(null)
const request = createMockRequest(
@@ -120,7 +111,7 @@ describe('MCP Events SSE Endpoint', () => {
})
it('returns SSE stream when authorized', async () => {
mockGetSession.mockResolvedValue({ user: defaultMockUser })
authMockFns.mockGetSession.mockResolvedValue({ user: defaultMockUser })
mockGetUserEntityPermissions.mockResolvedValue({ read: true })
const request = createMockRequest(

View File

@@ -3,81 +3,32 @@
*
* @vitest-environment node
*/
import {
dbChainMock,
dbChainMockFns,
hybridAuthMockFns,
permissionsMock,
permissionsMockFns,
resetDbChainMock,
} from '@sim/testing'
import { NextRequest } from 'next/server'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const {
mockCheckHybridAuth,
mockGetUserEntityPermissions,
mockGenerateInternalToken,
mockDbSelect,
mockDbFrom,
mockDbInnerJoin,
mockDbWhere,
mockDbLimit,
fetchMock,
} = vi.hoisted(() => ({
mockCheckHybridAuth: vi.fn(),
mockGetUserEntityPermissions: vi.fn(),
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', () => ({
workflowMcpServer: {
id: 'id',
name: 'name',
workspaceId: 'workspaceId',
isPublic: 'isPublic',
createdBy: 'createdBy',
deletedAt: 'deletedAt',
},
workflowMcpTool: {
serverId: 'serverId',
toolName: 'toolName',
toolDescription: 'toolDescription',
parameterSchema: 'parameterSchema',
workflowId: 'workflowId',
archivedAt: 'archivedAt',
},
workflow: {
id: 'id',
isDeployed: 'isDeployed',
archivedAt: 'archivedAt',
},
workspace: {
id: 'id',
archivedAt: 'archivedAt',
},
}))
vi.mock('@/lib/auth/hybrid', () => ({
AuthType: { SESSION: 'session', API_KEY: 'api_key', INTERNAL_JWT: 'internal_jwt' },
checkHybridAuth: mockCheckHybridAuth,
checkSessionOrInternalAuth: vi.fn(),
checkInternalAuth: vi.fn(),
}))
vi.mock('@/lib/workspaces/permissions/utils', () => ({
getUserEntityPermissions: mockGetUserEntityPermissions,
}))
vi.mock('@/lib/workspaces/permissions/utils', () => permissionsMock)
vi.mock('@/lib/auth/internal', () => ({
generateInternalToken: mockGenerateInternalToken,
@@ -97,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)
})
@@ -111,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',
@@ -120,7 +66,10 @@ describe('MCP Serve Route', () => {
createdBy: 'owner-1',
},
])
mockCheckHybridAuth.mockResolvedValueOnce({ success: false, error: 'Unauthorized' })
hybridAuthMockFns.mockCheckHybridAuth.mockResolvedValueOnce({
success: false,
error: 'Unauthorized',
})
const req = new NextRequest('http://localhost:3000/api/mcp/serve/server-1', {
method: 'POST',
@@ -132,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',
@@ -141,7 +90,10 @@ describe('MCP Serve Route', () => {
createdBy: 'owner-1',
},
])
mockCheckHybridAuth.mockResolvedValueOnce({ success: false, error: 'Unauthorized' })
hybridAuthMockFns.mockCheckHybridAuth.mockResolvedValueOnce({
success: false,
error: 'Unauthorized',
})
const req = new NextRequest('http://localhost:3000/api/mcp/serve/server-1')
const response = await GET(req, { params: Promise.resolve({ serverId: 'server-1' }) })
@@ -150,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',
@@ -163,7 +115,7 @@ describe('MCP Serve Route', () => {
.mockResolvedValueOnce([{ toolName: 'tool_a', workflowId: 'wf-1' }])
.mockResolvedValueOnce([{ isDeployed: true }])
mockCheckHybridAuth.mockResolvedValueOnce({
hybridAuthMockFns.mockCheckHybridAuth.mockResolvedValueOnce({
success: true,
userId: 'user-1',
authType: 'api_key',
@@ -199,7 +151,7 @@ describe('MCP Serve Route', () => {
})
it('forwards internal token for private server session auth', async () => {
mockDbLimit
dbChainMockFns.limit
.mockResolvedValueOnce([
{
id: 'server-1',
@@ -212,7 +164,7 @@ describe('MCP Serve Route', () => {
.mockResolvedValueOnce([{ toolName: 'tool_a', workflowId: 'wf-1' }])
.mockResolvedValueOnce([{ isDeployed: true }])
mockCheckHybridAuth.mockResolvedValueOnce({
hybridAuthMockFns.mockCheckHybridAuth.mockResolvedValueOnce({
success: true,
userId: 'user-1',
authType: 'session',

Some files were not shown because too many files have changed in this diff Show More