mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
v0.6.51: tables improvements, billing fixes, 404 pages, code hygiene
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
28
apps/sim/app/(landing)/integrations/not-found.tsx
Normal file
28
apps/sim/app/(landing)/integrations/not-found.tsx
Normal 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're looking for doesn'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>
|
||||
)
|
||||
}
|
||||
28
apps/sim/app/(landing)/models/not-found.tsx
Normal file
28
apps/sim/app/(landing)/models/not-found.tsx
Normal 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're looking for doesn'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>
|
||||
)
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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),
|
||||
})),
|
||||
}
|
||||
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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' },
|
||||
})
|
||||
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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([])
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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' })
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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(),
|
||||
}))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user