From d7f2f16930167ae88a9c9397e8227ce4b006ab99 Mon Sep 17 00:00:00 2001 From: dal Date: Fri, 18 Jul 2025 14:51:41 -0600 Subject: [PATCH] get rid of random pr --- slack-oauth-integration-prd.md | 1080 -------------------------------- 1 file changed, 1080 deletions(-) delete mode 100644 slack-oauth-integration-prd.md diff --git a/slack-oauth-integration-prd.md b/slack-oauth-integration-prd.md deleted file mode 100644 index b8f9e3ea0..000000000 --- a/slack-oauth-integration-prd.md +++ /dev/null @@ -1,1080 +0,0 @@ -# Slack OAuth Integration - Product Requirements Document - -## Executive Summary - -This document outlines the requirements for implementing Slack OAuth integration in the Buster application, enabling users to connect their Slack workspaces and utilize Slack messaging capabilities within the platform. - -## Overview - -### Purpose -Enable Buster users to securely authenticate with Slack workspaces, allowing the application to: -- Send messages to Slack channels -- List available channels -- Manage message threads -- Track message delivery - -### Scope -- OAuth 2.0 authentication flow -- Token management and storage -- Database schema for Slack integrations -- API endpoints for OAuth flow -- Security and compliance considerations - -## Technical Architecture - -### Components - -#### 1. **@buster/slack Package** (Existing) -- Standalone Slack integration with no database dependencies -- Provides OAuth flow management, channel operations, and messaging -- Interface-based design requiring implementation of storage interfaces - -#### 2. **@apps/server** (To be implemented) -- OAuth initiation endpoint -- OAuth callback endpoint -- Integration management endpoints - -#### 3. **@packages/database** (Schema additions required) -- Slack integration records -- Token storage (via Supabase Vault) -- OAuth state management - -## Detailed Requirements - -### OAuth Flow - -#### 1. OAuth Initiation -**Endpoint:** `POST /api/v2/slack/auth/init` - -**Request Body (Optional):** -```json -{ - "metadata": { - "returnUrl": "/settings/integrations", // Where to redirect after OAuth completes - "source": "settings_page", // Analytics tracking - "projectId": "uuid" // Optional project context - } -} -``` - -**Response:** -```json -{ - "authUrl": "https://slack.com/oauth/v2/authorize?...", - "state": "secure-random-state" -} -``` - -**Metadata Explained:** -The `metadata` object is completely optional and serves to maintain context through the OAuth flow: - -1. **returnUrl** - Where in your app to redirect after OAuth completes - - Default: `/settings/integrations` - - Example: `/projects/123/settings` to return to specific project - -2. **source** - Track where user initiated OAuth from - - Examples: `"onboarding"`, `"settings_page"`, `"project_view"` - - Useful for analytics and user flow optimization - -3. **projectId** - Associate integration with specific context - - Optional: Since one org = one Slack integration - - Useful if you want to track which project triggered the integration - -4. **Custom fields** - Any additional context your app needs - -**How it works:** -```typescript -// The metadata gets stored in the pending integration record -{ - id: "integration-id", - oauthState: "secure-random-state", - oauthMetadata: { - returnUrl: "/settings/integrations", - source: "settings_page", - projectId: "project-123", - // System adds: - initiatedAt: "2024-01-01T00:00:00Z", - ipAddress: "192.168.1.1" - }, - status: "pending" -} - -// After successful OAuth, use metadata to redirect: -const returnUrl = integration.oauthMetadata.returnUrl || '/settings'; -return c.redirect(`${returnUrl}?integration=success`); -``` - -**Requirements:** -- Metadata is entirely optional (endpoint works without it) -- Stored in `oauth_metadata` JSONB column -- Available during callback to restore user context -- Cleared after successful OAuth -- Size limit: 1KB to prevent abuse - -#### 2. OAuth Callback -**Endpoint:** `GET /api/v2/slack/auth/callback` - -**Authentication:** This endpoint should be **unauthenticated** because: -- Slack redirects here directly after OAuth approval -- The user's session might have expired during the OAuth flow -- We use the `state` parameter to map back to the user/organization - -**Query Parameters:** -- `code`: Authorization code from Slack -- `state`: State parameter that maps to our pending integration - -**Flow:** -1. Look up pending integration by `state` parameter -2. Verify state hasn't expired (15-minute window) -3. Exchange code for access token with Slack -4. Update integration record with Slack workspace info -5. Store token in Supabase Vault -6. Redirect to success page (from metadata.returnUrl) - -**Success Response (Redirect):** -``` -302 Redirect to: /settings/integrations?status=success&workspace=Acme%20Corp -``` - -**Error Response (Redirect):** -``` -302 Redirect to: /settings/integrations?status=error&error=access_denied -``` - -### Database Schema (Drizzle) - -Add the following to `@packages/database/src/schema.ts`: - -```typescript -// Enum for Slack integration status -export const slackIntegrationStatusEnum = pgEnum('slack_integration_status_enum', [ - 'pending', - 'active', - 'failed', - 'revoked' -]); - -// Slack integrations table -export const slackIntegrations = pgTable( - 'slack_integrations', - { - id: uuid().defaultRandom().primaryKey().notNull(), - organizationId: uuid('organization_id').notNull(), - userId: uuid('user_id').notNull(), - - // OAuth state fields (for pending integrations) - oauthState: varchar('oauth_state', { length: 255 }).unique(), - oauthExpiresAt: timestamp('oauth_expires_at', { withTimezone: true, mode: 'string' }), - oauthMetadata: jsonb('oauth_metadata').default({}), - - // Slack workspace info (populated after successful OAuth) - teamId: varchar('team_id', { length: 255 }), - teamName: varchar('team_name', { length: 255 }), - teamDomain: varchar('team_domain', { length: 255 }), - enterpriseId: varchar('enterprise_id', { length: 255 }), - - // Bot info - botUserId: varchar('bot_user_id', { length: 255 }), - scope: text(), - - // Token reference (actual token in Supabase Vault) - tokenVaultKey: varchar('token_vault_key', { length: 255 }).unique(), - - // Metadata - installedBySlackUserId: varchar('installed_by_slack_user_id', { length: 255 }), - installedAt: timestamp('installed_at', { withTimezone: true, mode: 'string' }), - lastUsedAt: timestamp('last_used_at', { withTimezone: true, mode: 'string' }), - status: slackIntegrationStatusEnum().default('pending').notNull(), - - // Timestamps - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }), - }, - (table) => [ - foreignKey({ - columns: [table.organizationId], - foreignColumns: [organizations.id], - name: 'slack_integrations_organization_id_fkey', - }).onDelete('cascade'), - foreignKey({ - columns: [table.userId], - foreignColumns: [users.id], - name: 'slack_integrations_user_id_fkey', - }), - unique('slack_integrations_org_team_key').on(table.organizationId, table.teamId), - index('idx_slack_integrations_org_id').using( - 'btree', - table.organizationId.asc().nullsLast().op('uuid_ops') - ), - index('idx_slack_integrations_team_id').using( - 'btree', - table.teamId.asc().nullsLast().op('text_ops') - ), - index('idx_slack_integrations_oauth_state').using( - 'btree', - table.oauthState.asc().nullsLast().op('text_ops') - ), - index('idx_slack_integrations_oauth_expires').using( - 'btree', - table.oauthExpiresAt.asc().nullsLast().op('timestamptz_ops') - ), - check( - 'slack_integrations_status_check', - sql`(status = 'pending' AND oauth_state IS NOT NULL) OR (status != 'pending' AND team_id IS NOT NULL)` - ), - ] -); - -// Slack message tracking table (optional) -export const slackMessageTracking = pgTable( - 'slack_message_tracking', - { - id: uuid().defaultRandom().primaryKey().notNull(), - integrationId: uuid('integration_id').notNull(), - - // Internal reference - internalMessageId: uuid('internal_message_id').notNull().unique(), - - // Slack references - slackChannelId: varchar('slack_channel_id', { length: 255 }).notNull(), - slackMessageTs: varchar('slack_message_ts', { length: 255 }).notNull(), - slackThreadTs: varchar('slack_thread_ts', { length: 255 }), - - // Metadata - messageType: varchar('message_type', { length: 50 }).notNull(), // 'message', 'reply', 'update' - content: text(), - senderInfo: jsonb('sender_info'), - - // Timestamps - sentAt: timestamp('sent_at', { withTimezone: true, mode: 'string' }).notNull(), - createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' }) - .defaultNow() - .notNull(), - }, - (table) => [ - foreignKey({ - columns: [table.integrationId], - foreignColumns: [slackIntegrations.id], - name: 'slack_message_tracking_integration_id_fkey', - }).onDelete('cascade'), - index('idx_message_tracking_integration').using( - 'btree', - table.integrationId.asc().nullsLast().op('uuid_ops') - ), - index('idx_message_tracking_channel').using( - 'btree', - table.slackChannelId.asc().nullsLast().op('text_ops') - ), - index('idx_message_tracking_thread').using( - 'btree', - table.slackThreadTs.asc().nullsLast().op('text_ops') - ), - ] -); -``` - -### API Implementation Structure - -#### File Organization - -``` -@apps/server/src/api/v2/slack/ -├── index.ts # Route definitions -├── index.test.ts # Route tests -├── handler.ts # Request handlers -├── handler.test.ts # Unit tests -├── handler.integration.test.ts # Integration tests -└── services/ # Business logic - ├── slack-oauth-service.ts # OAuth flow logic - ├── slack-oauth-service.test.ts # Service unit tests - ├── slack-helpers.ts # Utility functions - └── slack-helpers.test.ts # Helper tests -``` - -#### API Endpoints - -Since each organization will only have one Slack integration, we need just these endpoints: - -**1. Initiate OAuth Flow** -`POST /api/v2/slack/auth` -- Creates pending integration record -- Generates OAuth URL with state - -**2. OAuth Callback** -`GET /api/v2/slack/auth/callback` -- Handles redirect from Slack -- Exchanges code for token -- Updates integration to active - -**3. Get Current Integration** -`GET /api/v2/slack/integration` -- Returns current integration status -- Returns null if no integration exists - -**4. Remove Integration** -`DELETE /api/v2/slack/integration` -- Revokes token from Supabase Vault -- Soft deletes integration record - -#### File Responsibilities & Separation of Concerns - -**index.ts** - Pure routing, no business logic -```typescript -// ONLY route definitions and middleware -const app = new Hono() - .post('/auth', authMiddleware, handler.initiateOAuth) - .get('/auth/callback', handler.handleOAuthCallback) - .get('/integration', authMiddleware, handler.getIntegration) - .delete('/integration', authMiddleware, handler.removeIntegration); - -export default app; -``` - -**handler.ts** - Thin HTTP layer, delegates to services -```typescript -// Each handler is a thin wrapper that: -// 1. Extracts and validates input -// 2. Calls the appropriate service function -// 3. Formats the response - -export const initiateOAuth = async (c: Context) => { - const { organizationId, userId } = c.get('auth'); - - // Delegate to service - handler doesn't contain business logic - const result = await slackOAuthService.initiateOAuth({ - organizationId, - userId, - }); - - return c.json(result); -}; -``` - -**services/slack-oauth-service.ts** - Business logic in pure, testable functions -```typescript -// Pure functions that can be tested independently -export async function initiateOAuth(params: { - organizationId: string; - userId: string; -}): Promise<{ authUrl: string; state: string }> { - // Check for existing integration - const existing = await slackHelpers.getActiveIntegration(params.organizationId); - if (existing) { - throw new Error('Integration already exists'); - } - - // Generate OAuth URL - const { authUrl, state } = await generateOAuthUrl(params); - - // Store pending integration - await slackHelpers.createPendingIntegration({ - ...params, - oauthState: state, - }); - - return { authUrl, state }; -} - -// Each function does ONE thing and is independently testable -export async function generateOAuthUrl(params: { - organizationId: string; - userId: string; -}): Promise<{ authUrl: string; state: string }> { - // Pure OAuth URL generation logic -} - -export async function exchangeCodeForToken(params: { - code: string; - state: string; -}): Promise { - // Pure token exchange logic -} -``` - -**services/slack-helpers.ts** - Database operations as pure functions -```typescript -// Each helper is a focused, testable function -export async function getActiveIntegration( - organizationId: string -): Promise { - return db - .select() - .from(slackIntegrations) - .where(and( - eq(slackIntegrations.organizationId, organizationId), - eq(slackIntegrations.status, 'active'), - isNull(slackIntegrations.deletedAt) - )) - .limit(1) - .then(rows => rows[0] || null); -} - -export async function createPendingIntegration(params: { - organizationId: string; - userId: string; - oauthState: string; -}): Promise { - const [integration] = await db - .insert(slackIntegrations) - .values({ - ...params, - status: 'pending', - oauthExpiresAt: new Date(Date.now() + 15 * 60 * 1000), - }) - .returning({ id: slackIntegrations.id }); - - return integration.id; -} - -export async function storeTokenInVault( - integrationId: string, - token: string -): Promise { - // Pure function for vault storage - const vaultKey = `slack-token-${integrationId}`; - await supabaseVault.store(vaultKey, token); - return vaultKey; -} -``` - -#### Key Design Principles - -1. **Single Responsibility** - Each function does ONE thing -2. **Pure Functions** - Functions return values based on inputs, minimal side effects -3. **Dependency Injection** - Pass dependencies as parameters for easy mocking -4. **No Business Logic in Handlers** - Handlers only handle HTTP concerns -5. **Testable in Isolation** - Each function can be unit tested independently - -#### Testing Structure - -**Unit Tests (*.test.ts)** -- Mock external dependencies -- Test individual functions in isolation -- Focus on business logic correctness -- Mock database and Slack API calls - -**Integration Tests (*.integration.test.ts)** -- Use test database -- Test full request/response cycle -- Mock only external APIs (Slack) -- Verify database state changes - -**Test Utilities** -- Shared test factories for creating test data -- Mock implementations of interfaces -- Test database setup/teardown helpers - -### Security Requirements - -#### Token Storage -- Access tokens stored in Supabase Vault -- Vault key stored in database, not the token itself -- Tokens encrypted at rest -- Support for token rotation - -#### Access Control -- Integration scoped to organization -- User must have appropriate permissions -- Audit logging for all OAuth operations -- Rate limiting on OAuth endpoints - -#### OAuth Security -- State parameter validation with 15-minute expiry -- HTTPS required for all OAuth endpoints -- Redirect URI whitelist validation -- PKCE support for enhanced security (future) - -### Implementation Interfaces - -#### ISlackTokenStorage Implementation -```typescript -class DatabaseTokenStorage implements ISlackTokenStorage { - async storeToken(key: string, token: string): Promise { - // Store in Supabase Vault - // Update slack_integrations.token_vault_key - } - - async getToken(key: string): Promise { - // Retrieve from Supabase Vault - } - - async deleteToken(key: string): Promise { - // Remove from Vault - // Clear slack_integrations.token_vault_key - } - - async hasToken(key: string): Promise { - // Check if token exists in Vault - } -} -``` - -#### ISlackOAuthStateStorage Implementation -```typescript -class DatabaseOAuthStateStorage implements ISlackOAuthStateStorage { - async storeState(state: string, data: SlackOAuthStateData): Promise { - // Create a pending integration record with OAuth state - await db.insert(slackIntegrations).values({ - userId: data.metadata.userId, - organizationId: data.metadata.organizationId, - oauth_state: state, - oauth_expires_at: new Date(data.expiresAt), - oauth_metadata: data.metadata, - status: 'pending' - }); - } - - async getState(state: string): Promise { - // Query slack_integrations for pending OAuth - const integration = await db - .select() - .from(slackIntegrations) - .where(and( - eq(slackIntegrations.oauth_state, state), - eq(slackIntegrations.status, 'pending'), - gt(slackIntegrations.oauth_expires_at, new Date()) - )) - .limit(1); - - if (!integration[0]) return null; - - return { - expiresAt: integration[0].oauth_expires_at.getTime(), - metadata: integration[0].oauth_metadata - }; - } - - async deleteState(state: string): Promise { - // Clean up failed/expired OAuth attempts - await db - .delete(slackIntegrations) - .where(and( - eq(slackIntegrations.oauth_state, state), - eq(slackIntegrations.status, 'pending') - )); - } -} -``` - -### Slack App Configuration - -#### Required OAuth Scopes -- `channels:read` - List channels in workspace -- `chat:write` - Send messages as bot -- `chat:write.public` - Send to channels bot hasn't joined -- `channels:join` - Join public channels -- `users:read` - Read user information - -#### OAuth Redirect URLs -- Development: `http://localhost:3000/api/v2/slack/auth/callback` -- Staging: `https://staging.buster.so/api/v2/slack/auth/callback` -- Production: `https://app.buster.so/api/v2/slack/auth/callback` - -### Error Handling - -#### OAuth Errors -- `OAUTH_ACCESS_DENIED` - User denied authorization -- `OAUTH_INVALID_STATE` - Invalid or expired state -- `OAUTH_TOKEN_EXCHANGE_FAILED` - Failed to exchange code -- `INVALID_SCOPE` - Required scopes not granted -- `WORKSPACE_LIMIT_REACHED` - Organization workspace limit - -#### Integration Errors -- `INTEGRATION_NOT_FOUND` - Integration doesn't exist -- `INTEGRATION_INACTIVE` - Integration has been deactivated -- `INVALID_CHANNEL` - Channel not accessible -- `RATE_LIMITED` - Slack API rate limit - -### Monitoring & Analytics - -#### Metrics to Track -- OAuth flow completion rate -- Time to complete OAuth flow -- Integration usage by organization -- Message send success rate -- Channel access patterns -- Error rates by type - -#### Audit Events -- OAuth flow initiated -- OAuth flow completed/failed -- Integration created/deleted -- Token refreshed -- Message sent/failed -- Permission changes - -### User Experience - -#### Integration Flow -1. User navigates to Integrations page -2. Clicks "Connect Slack" -3. Redirected to Slack OAuth page -4. Approves permissions -5. Redirected back to Buster -6. See success message with workspace info -7. Can immediately start using integration - -#### Management Interface -- List all connected workspaces -- Show connection status -- Last used timestamp -- Quick actions (test, disconnect) -- Channel browser -- Message history viewer - -### Future Enhancements - -#### Phase 2 -- Incoming webhooks support -- Event subscriptions (message reactions, new messages) -- Slash commands -- Interactive components (buttons, modals) -- Multiple workspace per organization - -#### Phase 3 -- Scheduled messages -- Message formatting builder -- Template library -- Bulk operations -- Analytics dashboard - -### Testing Requirements - -#### Test File Coverage - -**index.test.ts** -- Route registration verification -- Middleware application checks -- Route parameter validation - -**handler.test.ts** - Test HTTP layer only -```typescript -describe('SlackHandler', () => { - // Mock the service layer - const mockSlackOAuthService = { - initiateOAuth: vi.fn(), - handleCallback: vi.fn(), - getIntegration: vi.fn(), - removeIntegration: vi.fn(), - }; - - describe('initiateOAuth', () => { - it('should extract auth context and call service'); - it('should return 200 with auth URL'); - it('should handle service errors appropriately'); - }); - - describe('handleOAuthCallback', () => { - it('should validate query parameters'); - it('should call service with code and state'); - it('should redirect on success'); - it('should handle errors with proper status codes'); - }); -}); -``` - -**services/slack-oauth-service.test.ts** - Test business logic -```typescript -describe('SlackOAuthService', () => { - // Mock only external dependencies - const mockSlackHelpers = { - getActiveIntegration: vi.fn(), - createPendingIntegration: vi.fn(), - updateIntegrationStatus: vi.fn(), - }; - - describe('initiateOAuth', () => { - it('should throw if integration already exists'); - it('should generate valid OAuth URL'); - it('should create pending integration with state'); - it('should set 15-minute expiry on state'); - }); - - describe('exchangeCodeForToken', () => { - it('should validate state exists and not expired'); - it('should call Slack API with correct parameters'); - it('should store token in vault'); - it('should update integration to active status'); - it('should clear OAuth state after success'); - }); -}); -``` - -**services/slack-helpers.test.ts** - Test data layer -```typescript -describe('SlackHelpers', () => { - describe('getActiveIntegration', () => { - it('should return active integration for org'); - it('should exclude deleted integrations'); - it('should return null if no integration'); - }); - - describe('createPendingIntegration', () => { - it('should create with pending status'); - it('should set OAuth expiry timestamp'); - it('should return integration ID'); - }); - - describe('storeTokenInVault', () => { - it('should generate correct vault key'); - it('should store encrypted token'); - it('should return vault key'); - }); -}); -``` - -**handler.integration.test.ts** -- Full OAuth flow with database -- State expiration handling -- Organization isolation -- Concurrent request handling - -**services/slack-oauth-service.test.ts** -- OAuth URL generation -- State management -- Token exchange logic -- Vault integration mocking - -**services/slack-helpers.test.ts** -- Database query functions -- Token encryption/decryption -- Status checking logic -- Data transformation utilities - -#### Testing Best Practices - -1. **Test Data Isolation** - - Each test creates its own organization/user - - Clean up after each test - - No shared state between tests - -2. **Mock Strategy** - - Mock Slack API responses - - Mock Supabase Vault operations - - Use real database in integration tests - - Mock time for expiration tests - -3. **Error Scenarios** - - Network failures - - Invalid OAuth codes - - Expired states - - Rate limiting - - Database conflicts - -4. **Security Testing** - - CSRF protection validation - - Authorization checks - - Token exposure prevention - - Cross-organization access - -### Development & Testing Workflow - -#### Local Development Setup - -1. **Environment Configuration** -```bash -# .env.local -SLACK_INTEGRATION_ENABLED=false # Set to true when ready to test -SLACK_CLIENT_ID=your-dev-client-id -SLACK_CLIENT_SECRET=your-dev-client-secret -SLACK_REDIRECT_URI=http://localhost:3000/api/v2/slack/auth/callback -SUPABASE_VAULT_KEY=your-vault-key -``` - -**Feature Flag Implementation:** -```typescript -// services/slack-oauth-service.ts -export async function initiateOAuth(params: { - organizationId: string; - userId: string; -}): Promise<{ authUrl: string; state: string }> { - // Check if Slack integration is enabled - if (!process.env.SLACK_INTEGRATION_ENABLED || process.env.SLACK_INTEGRATION_ENABLED === 'false') { - throw new Error('Slack integration is not enabled'); - } - - // Regular OAuth flow continues... -} -``` - -2. **Testing with Real Slack** - -For local development, you'll need to set up a real Slack app and use ngrok to expose your localhost: - -**Slack App Setup:** -1. Go to https://api.slack.com/apps and create a new app -2. Choose "From scratch" and select your workspace -3. Add OAuth scopes under "OAuth & Permissions": - - `channels:read` - - `chat:write` - - `chat:write.public` -4. Add redirect URL (will update with ngrok URL) - -**Local Development with ngrok:** -```bash -# Start your local server -pnpm run dev --filter=@buster-app/server - -# In another terminal, expose your localhost -ngrok http 3000 - -# Copy the HTTPS URL from ngrok (e.g., https://abc123.ngrok.io) -# Update your Slack app's redirect URL to: -# https://abc123.ngrok.io/api/v2/slack/auth/callback - -# Update your .env.local -SLACK_CLIENT_ID=your-slack-client-id -SLACK_CLIENT_SECRET=your-slack-client-secret -SLACK_REDIRECT_URI=https://abc123.ngrok.io/api/v2/slack/auth/callback -``` - -**Testing Flow:** -1. Start your server and ngrok -2. Navigate to your OAuth initiation endpoint -3. You'll be redirected to real Slack OAuth -4. Approve access in your test workspace -5. Slack redirects back to your ngrok URL -6. Your callback handler processes the real OAuth code - -3. **Database Setup** -```bash -# Run migrations -pnpm run db:migrate - -# Seed test data -pnpm run db:seed:dev -``` - -#### Testing Workflow - -**1. Unit Tests (Fast Feedback)** -```bash -# Run all unit tests (works without Slack credentials) -pnpm run test:unit --filter=@buster-app/server - -# Run specific test file -pnpm run test apps/server/src/api/v2/slack/handler.test.ts - -# Watch mode for development -pnpm run test:watch apps/server/src/api/v2/slack -``` - -**Unit Test Mocking Strategy:** -```typescript -// handler.test.ts - Tests work without real Slack -describe('SlackHandler', () => { - beforeEach(() => { - // Mock environment check - vi.stubEnv('SLACK_INTEGRATION_ENABLED', 'true'); - - // Mock Slack API calls - vi.mock('@buster/slack', () => ({ - SlackAuthService: vi.fn(() => ({ - generateAuthUrl: vi.fn().mockResolvedValue({ - authUrl: 'https://mock-url', - state: 'mock-state' - }), - handleCallback: vi.fn().mockResolvedValue({ - teamId: 'T-MOCK', - teamName: 'Mock Team' - }) - })) - })); - }); - - it('should handle disabled integration gracefully', async () => { - vi.stubEnv('SLACK_INTEGRATION_ENABLED', 'false'); - - const response = await app.request('/api/v2/slack/auth', { - method: 'POST' - }); - - expect(response.status).toBe(503); // Service Unavailable - expect(await response.json()).toEqual({ - error: 'Slack integration is not enabled' - }); - }); -}); -``` - -**2. Integration Tests (Conditional Execution)** -```bash -# Integration tests skip Slack tests when disabled -pnpm run test:integration --filter=@buster-app/server -``` - -```typescript -// handler.integration.test.ts -describe('Slack OAuth Integration', () => { - const slackEnabled = process.env.SLACK_INTEGRATION_ENABLED === 'true'; - - describe.skipIf(!slackEnabled)('with real Slack', () => { - // These tests only run when SLACK_INTEGRATION_ENABLED=true - it('should complete OAuth flow', async () => { - // Real Slack OAuth test - }); - }); - - describe('with mocked Slack', () => { - // These tests always run - it('should handle database operations', async () => { - // Test database without real Slack - }); - }); -}); -``` - -**3. Manual Testing Flow** -```bash -# Start server -pnpm run dev --filter=@buster-app/server - -# Test OAuth flow -curl -X POST http://localhost:3000/api/v2/slack/auth \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" - -# Response includes auth URL -# Visit URL in browser to complete OAuth - -# Check integration status -curl http://localhost:3000/api/v2/slack/integration \ - -H "Authorization: Bearer $TOKEN" - -# Remove integration -curl -X DELETE http://localhost:3000/api/v2/slack/integration \ - -H "Authorization: Bearer $TOKEN" -``` - -#### Verification Checklist - -**OAuth Flow Verification** -- [ ] Auth URL includes all required parameters -- [ ] State is stored in database with expiry -- [ ] Callback validates state correctly -- [ ] Token exchange succeeds -- [ ] Token stored in Supabase Vault -- [ ] Integration record updated to 'active' -- [ ] OAuth metadata cleared after success - -**Security Verification** -- [ ] State expires after 15 minutes -- [ ] Invalid state returns error -- [ ] Cross-organization access blocked -- [ ] Tokens never exposed in API responses -- [ ] Deleted integrations can't be accessed - -**Error Handling Verification** -- [ ] User denial handled gracefully -- [ ] Network errors return appropriate status -- [ ] Duplicate integration attempts blocked -- [ ] Missing integration returns 404 - -### CI/CD Considerations - -#### Environment-Based Configuration - -**Local Development (default):** -```env -SLACK_INTEGRATION_ENABLED=false -# Slack credentials optional -``` - -**CI/CD Pipeline:** -```env -SLACK_INTEGRATION_ENABLED=false -# Tests run with mocked Slack -``` - -**Staging:** -```env -SLACK_INTEGRATION_ENABLED=true -SLACK_CLIENT_ID=staging-client-id -SLACK_CLIENT_SECRET=xxx (from secrets) -SLACK_REDIRECT_URI=https://staging.buster.so/api/v2/slack/auth/callback -``` - -**Production:** -```env -SLACK_INTEGRATION_ENABLED=true -SLACK_CLIENT_ID=prod-client-id -SLACK_CLIENT_SECRET=xxx (from Supabase Vault) -SLACK_REDIRECT_URI=https://app.buster.so/api/v2/slack/auth/callback -``` - -#### API Response When Disabled - -When `SLACK_INTEGRATION_ENABLED=false`, endpoints return: -```json -{ - "error": "Slack integration is not enabled", - "code": "INTEGRATION_DISABLED", - "status": 503 -} -``` - -This allows: -- Frontend to conditionally show/hide Slack features -- Tests to run without Slack credentials -- Gradual rollout with feature flags - -### Deployment Considerations - -#### Environment Variables -```env -SLACK_INTEGRATION_ENABLED=true/false -SLACK_CLIENT_ID=xxx -SLACK_CLIENT_SECRET=xxx (store in Supabase Vault) -SLACK_REDIRECT_URI=https://app.buster.so/api/v2/slack/auth/callback -SLACK_OAUTH_SCOPES=channels:read,chat:write,chat:write.public -``` - -#### Migration Strategy -1. Deploy database schema -2. Configure Supabase Vault -3. Deploy API endpoints -4. Update Slack app configuration -5. Enable feature flag -6. Gradual rollout - -### Success Criteria - -1. **Security**: Zero OAuth-related security incidents -2. **Reliability**: 99.9% uptime for OAuth endpoints -3. **Performance**: OAuth flow completes in <3 seconds -4. **Adoption**: 50% of organizations connect Slack within 30 days -5. **Retention**: 90% of integrations remain active after 60 days - -### Dependencies - -- **@buster/slack** package (existing) -- **Supabase Vault** for token storage -- **Slack API** availability -- **Database** migrations completed -- **Frontend** integration UI - -### Timeline - -- **Week 1-2**: Database schema and migrations -- **Week 2-3**: OAuth endpoints implementation -- **Week 3-4**: Integration management APIs -- **Week 4-5**: Frontend integration -- **Week 5-6**: Testing and security review -- **Week 6-7**: Documentation and deployment - -### Appendix - -#### Slack API Reference -- [OAuth 2.0 Flow](https://api.slack.com/authentication/oauth-v2) -- [Web API Methods](https://api.slack.com/web) -- [Bot Tokens & Scopes](https://api.slack.com/authentication/token-types) - -#### Internal References -- @buster/slack package documentation -- Supabase Vault documentation -- Organization permissions model \ No newline at end of file