fix all of the bugs

This commit is contained in:
Nate Kelley 2025-07-12 22:01:25 -06:00
parent 4a1c4f73f0
commit 963bf6b2f2
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
36 changed files with 301 additions and 215 deletions

View File

@ -1,6 +1,9 @@
{ {
"extends": "@buster/typescript-config/base.json", "extends": "@buster/typescript-config/base.json",
"compilerOptions": {}, "compilerOptions": {},
"include": ["scripts/**/*"], "include": ["scripts*"],
"exclude": ["node_modules"] "exclude": ["node_modules"],
} "references": [
{ "path": "../../packages/database" }
]
}

View File

@ -66,6 +66,8 @@ const mockMessage: Message = {
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
deletedAt: null, deletedAt: null,
feedback: null, feedback: null,
postProcessingMessage: null,
triggerRunId: null,
}; };
describe('buildChatWithMessages', () => { describe('buildChatWithMessages', () => {

View File

@ -36,9 +36,9 @@ describe('Chat Message Redo Integration Tests', () => {
await db.insert(organizations).values({ await db.insert(organizations).values({
id: testOrgId, id: testOrgId,
name: 'Test Organization for Redo', name: 'Test Organization for Redo',
slug: 'test-org-redo', restrictNewUserInvitations: false,
createdBy: testUserId, defaultRole: 'workspace_admin',
updatedBy: testUserId, domains: [],
}); });
// Create test user // Create test user
@ -49,7 +49,6 @@ describe('Chat Message Redo Integration Tests', () => {
email: 'test-redo@example.com', email: 'test-redo@example.com',
name: 'Test User Redo', name: 'Test User Redo',
avatarUrl: null, avatarUrl: null,
metadata: {},
}) })
.returning(); .returning();

View File

@ -51,6 +51,8 @@ const mockMessage: Message = {
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
deletedAt: null, deletedAt: null,
feedback: null, feedback: null,
postProcessingMessage: null,
triggerRunId: null,
}; };
// Mock database functions // Mock database functions

View File

@ -1,4 +1,5 @@
import type { Organization, User } from '@buster/database'; import type { User } from '@buster/database';
import type { Organization, OrganizationRole } from '@buster/server-shared/organization';
import { HTTPException } from 'hono/http-exception'; import { HTTPException } from 'hono/http-exception';
import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { addApprovedDomainsHandler } from './add-approved-domains'; import { addApprovedDomainsHandler } from './add-approved-domains';

View File

@ -1,5 +1,6 @@
import { db, eq, organizations } from '@buster/database'; import { db, eq, organizations } from '@buster/database';
import type { Organization, User } from '@buster/database'; import type { User } from '@buster/database';
import type { Organization, OrganizationRole } from '@buster/server-shared/organization';
import { HTTPException } from 'hono/http-exception'; import { HTTPException } from 'hono/http-exception';
import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { getApprovedDomainsHandler } from './get-approved-domains'; import { getApprovedDomainsHandler } from './get-approved-domains';
@ -68,7 +69,7 @@ describe('getApprovedDomainsHandler (integration)', () => {
}); });
it('should work for users with different roles', async () => { it('should work for users with different roles', async () => {
const roles = ['querier', 'data_admin', 'workspace_admin']; const roles: OrganizationRole[] = ['querier', 'data_admin', 'workspace_admin'];
for (const role of roles) { for (const role of roles) {
let roleUser: User | undefined; let roleUser: User | undefined;
@ -153,7 +154,7 @@ describe('getApprovedDomainsHandler (integration)', () => {
.limit(1); .limit(1);
expect(orgAfter[0]?.domains).toEqual(originalOrg.domains); expect(orgAfter[0]?.domains).toEqual(originalOrg.domains);
expect(new Date(orgAfter[0]?.updatedAt).toISOString()).toEqual( expect(new Date(orgAfter[0]!.updatedAt).toISOString()).toEqual(
new Date(originalOrg.updatedAt).toISOString() new Date(originalOrg.updatedAt).toISOString()
); );
}); });

View File

@ -1,5 +1,7 @@
import { db, eq, organizations } from '@buster/database'; import { db, eq, organizations } from '@buster/database';
import type { Organization, User } from '@buster/database'; import type { User } from '@buster/database';
import type { OrganizationRole } from '@buster/server-shared/organization';
import type { InferSelectModel } from 'drizzle-orm';
import { HTTPException } from 'hono/http-exception'; import { HTTPException } from 'hono/http-exception';
import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { getWorkspaceSettingsHandler } from './get-workspace-settings'; import { getWorkspaceSettingsHandler } from './get-workspace-settings';
@ -12,6 +14,8 @@ import {
createUserWithoutOrganization, createUserWithoutOrganization,
} from './test-db-utils'; } from './test-db-utils';
type Organization = InferSelectModel<typeof organizations>;
describe('getWorkspaceSettingsHandler (integration)', () => { describe('getWorkspaceSettingsHandler (integration)', () => {
let testUser: User; let testUser: User;
let testOrg: Organization; let testOrg: Organization;
@ -61,7 +65,12 @@ describe('getWorkspaceSettingsHandler (integration)', () => {
}); });
it('should work for users with different roles', async () => { it('should work for users with different roles', async () => {
const roles = ['querier', 'restricted_querier', 'data_admin', 'workspace_admin']; const roles: OrganizationRole[] = [
'querier',
'restricted_querier',
'data_admin',
'workspace_admin',
];
for (const role of roles) { for (const role of roles) {
const roleUser = await createTestUserInDb(); const roleUser = await createTestUserInDb();
@ -169,7 +178,7 @@ describe('getWorkspaceSettingsHandler (integration)', () => {
originalOrg.restrictNewUserInvitations originalOrg.restrictNewUserInvitations
); );
expect(orgAfter[0]?.defaultRole).toEqual(originalOrg.defaultRole); expect(orgAfter[0]?.defaultRole).toEqual(originalOrg.defaultRole);
expect(new Date(orgAfter[0]?.updatedAt).toISOString()).toEqual(originalOrg.updatedAt); expect(new Date(orgAfter[0]!.updatedAt).toISOString()).toEqual(originalOrg.updatedAt);
}); });
it('should always return empty default_datasets array', async () => { it('should always return empty default_datasets array', async () => {

View File

@ -67,7 +67,7 @@ describe('getWorkspaceSettingsHandler', () => {
const orgWithDifferentSettings = { const orgWithDifferentSettings = {
...mockOrg, ...mockOrg,
restrictNewUserInvitations: false, restrictNewUserInvitations: false,
defaultRole: 'data_admin', defaultRole: 'data_admin' as const,
}; };
vi.mocked(securityUtils.fetchOrganization).mockResolvedValue(orgWithDifferentSettings); vi.mocked(securityUtils.fetchOrganization).mockResolvedValue(orgWithDifferentSettings);

View File

@ -1,4 +1,5 @@
import type { Organization, User } from '@buster/database'; import type { User, organizations } from '@buster/database';
import type { InferSelectModel } from 'drizzle-orm';
import { HTTPException } from 'hono/http-exception'; import { HTTPException } from 'hono/http-exception';
import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { removeApprovedDomainsHandler } from './remove-approved-domains'; import { removeApprovedDomainsHandler } from './remove-approved-domains';
@ -12,6 +13,8 @@ import {
getOrganizationFromDb, getOrganizationFromDb,
} from './test-db-utils'; } from './test-db-utils';
type Organization = InferSelectModel<typeof organizations>;
describe('removeApprovedDomainsHandler (integration)', () => { describe('removeApprovedDomainsHandler (integration)', () => {
let testUser: User; let testUser: User;
let testOrg: Organization; let testOrg: Organization;

View File

@ -1,6 +1,6 @@
import type { Organization, User } from '@buster/database'; import type { User, organizations } from '@buster/database';
import { db, usersToOrganizations } from '@buster/database'; import type { OrganizationRole } from '@buster/server-shared/organization';
import { eq } from 'drizzle-orm'; import type { InferSelectModel } from 'drizzle-orm';
import { HTTPException } from 'hono/http-exception'; import { HTTPException } from 'hono/http-exception';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { import {
@ -15,6 +15,8 @@ import {
} from './test-db-utils'; } from './test-db-utils';
import { updateWorkspaceSettingsHandler } from './update-workspace-settings'; import { updateWorkspaceSettingsHandler } from './update-workspace-settings';
type Organization = InferSelectModel<typeof organizations>;
describe('updateWorkspaceSettingsHandler (integration)', () => { describe('updateWorkspaceSettingsHandler (integration)', () => {
let testUser: User; let testUser: User;
let testOrg: Organization; let testOrg: Organization;
@ -45,7 +47,7 @@ describe('updateWorkspaceSettingsHandler (integration)', () => {
const request = { const request = {
restrict_new_user_invitations: true, restrict_new_user_invitations: true,
default_role: 'data_admin', default_role: 'data_admin' as const,
}; };
const result = await updateWorkspaceSettingsHandler(request, testUser); const result = await updateWorkspaceSettingsHandler(request, testUser);
@ -72,7 +74,7 @@ describe('updateWorkspaceSettingsHandler (integration)', () => {
expect(result1.default_role).toBe('querier'); // Should remain unchanged expect(result1.default_role).toBe('querier'); // Should remain unchanged
// Update only default_role // Update only default_role
const request2 = { default_role: 'data_admin' }; const request2 = { default_role: 'data_admin' as const };
const result2 = await updateWorkspaceSettingsHandler(request2, testUser); const result2 = await updateWorkspaceSettingsHandler(request2, testUser);
expect(result2.restrict_new_user_invitations).toBe(true); // Should keep previous update expect(result2.restrict_new_user_invitations).toBe(true); // Should keep previous update
@ -88,7 +90,7 @@ describe('updateWorkspaceSettingsHandler (integration)', () => {
const request = { const request = {
restrict_new_user_invitations: true, restrict_new_user_invitations: true,
default_role: 'viewer', default_role: 'viewer' as const,
}; };
const result = await updateWorkspaceSettingsHandler(request, testUser); const result = await updateWorkspaceSettingsHandler(request, testUser);
@ -122,7 +124,7 @@ describe('updateWorkspaceSettingsHandler (integration)', () => {
describe('Error Cases', () => { describe('Error Cases', () => {
it('should return 403 for non-workspace-admin users', async () => { it('should return 403 for non-workspace-admin users', async () => {
const roles = ['querier', 'restricted_querier', 'data_admin', 'viewer']; const roles: OrganizationRole[] = ['querier', 'restricted_querier', 'data_admin', 'viewer'];
for (const role of roles) { for (const role of roles) {
// Create a fresh organization for each role test to avoid conflicts // Create a fresh organization for each role test to avoid conflicts
@ -193,7 +195,7 @@ describe('updateWorkspaceSettingsHandler (integration)', () => {
const request = { const request = {
restrict_new_user_invitations: true, restrict_new_user_invitations: true,
default_role: 'viewer', default_role: 'viewer' as const,
}; };
await updateWorkspaceSettingsHandler(request, testUser); await updateWorkspaceSettingsHandler(request, testUser);
@ -209,13 +211,13 @@ describe('updateWorkspaceSettingsHandler (integration)', () => {
const originalName = testOrg.name; const originalName = testOrg.name;
const originalCreatedAt = testOrg.createdAt; const originalCreatedAt = testOrg.createdAt;
const request = { default_role: 'data_admin' }; const request = { default_role: 'data_admin' as const };
await updateWorkspaceSettingsHandler(request, testUser); await updateWorkspaceSettingsHandler(request, testUser);
const updatedOrg = await getOrganizationFromDb(testOrg.id); const updatedOrg = await getOrganizationFromDb(testOrg.id);
expect(updatedOrg?.domains).toEqual(originalDomains); expect(updatedOrg?.domains).toEqual(originalDomains);
expect(updatedOrg?.name).toBe(originalName); expect(updatedOrg?.name).toBe(originalName);
expect(new Date(updatedOrg?.createdAt).toISOString()).toBe(originalCreatedAt); expect(new Date(updatedOrg!.createdAt).toISOString()).toBe(originalCreatedAt);
}); });
it("should update organization's updatedAt timestamp", async () => { it("should update organization's updatedAt timestamp", async () => {

View File

@ -1,3 +1,4 @@
import type { OrganizationRole } from '@buster/server-shared/organization';
import type { UpdateWorkspaceSettingsRequest } from '@buster/server-shared/security'; import type { UpdateWorkspaceSettingsRequest } from '@buster/server-shared/security';
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { WorkspaceSettingsService } from './workspace-settings-service'; import { WorkspaceSettingsService } from './workspace-settings-service';
@ -9,8 +10,8 @@ describe('WorkspaceSettingsService', () => {
it('should map snake_case to camelCase fields', () => { it('should map snake_case to camelCase fields', () => {
const settings = { const settings = {
restrictNewUserInvitations: true, restrictNewUserInvitations: true,
defaultRole: 'member', defaultRole: 'workspace_admin',
}; } as Parameters<typeof service.formatWorkspaceSettingsResponse>[0];
const result = service.formatWorkspaceSettingsResponse(settings); const result = service.formatWorkspaceSettingsResponse(settings);
expect(result).toEqual({ expect(result).toEqual({
restrict_new_user_invitations: true, restrict_new_user_invitations: true,
@ -22,8 +23,8 @@ describe('WorkspaceSettingsService', () => {
it('should include empty default_datasets array', () => { it('should include empty default_datasets array', () => {
const settings = { const settings = {
restrictNewUserInvitations: false, restrictNewUserInvitations: false,
defaultRole: 'admin', defaultRole: 'admin' as OrganizationRole,
}; } as Parameters<typeof service.formatWorkspaceSettingsResponse>[0];
const result = service.formatWorkspaceSettingsResponse(settings); const result = service.formatWorkspaceSettingsResponse(settings);
expect(result.default_datasets).toEqual([]); expect(result.default_datasets).toEqual([]);
}); });
@ -31,12 +32,12 @@ describe('WorkspaceSettingsService', () => {
it('should handle all boolean values correctly', () => { it('should handle all boolean values correctly', () => {
const settingsTrue = { const settingsTrue = {
restrictNewUserInvitations: true, restrictNewUserInvitations: true,
defaultRole: 'member', defaultRole: 'member' as OrganizationRole,
}; } as Parameters<typeof service.formatWorkspaceSettingsResponse>[0];
const settingsFalse = { const settingsFalse = {
restrictNewUserInvitations: false, restrictNewUserInvitations: false,
defaultRole: 'member', defaultRole: 'member' as OrganizationRole,
}; } as Parameters<typeof service.formatWorkspaceSettingsResponse>[0];
expect( expect(
service.formatWorkspaceSettingsResponse(settingsTrue).restrict_new_user_invitations service.formatWorkspaceSettingsResponse(settingsTrue).restrict_new_user_invitations
@ -57,9 +58,9 @@ describe('WorkspaceSettingsService', () => {
]; ];
for (const role of roles) { for (const role of roles) {
const settings = { const settings: Parameters<typeof service.formatWorkspaceSettingsResponse>[0] = {
restrictNewUserInvitations: false, restrictNewUserInvitations: false,
defaultRole: role, defaultRole: role as OrganizationRole,
}; };
const result = service.formatWorkspaceSettingsResponse(settings); const result = service.formatWorkspaceSettingsResponse(settings);
expect(result.default_role).toBe(role); expect(result.default_role).toBe(role);
@ -88,8 +89,8 @@ describe('WorkspaceSettingsService', () => {
it('should handle partial updates (only default_role)', () => { it('should handle partial updates (only default_role)', () => {
const request: UpdateWorkspaceSettingsRequest = { const request: UpdateWorkspaceSettingsRequest = {
default_role: 'admin', default_role: 'admin' as OrganizationRole,
}; } as Parameters<typeof service.buildUpdateData>[0];
const result = service.buildUpdateData(request); const result = service.buildUpdateData(request);
expect(result).toHaveProperty('defaultRole', 'admin'); expect(result).toHaveProperty('defaultRole', 'admin');
@ -99,8 +100,8 @@ describe('WorkspaceSettingsService', () => {
it('should handle full updates', () => { it('should handle full updates', () => {
const request: UpdateWorkspaceSettingsRequest = { const request: UpdateWorkspaceSettingsRequest = {
restrict_new_user_invitations: false, restrict_new_user_invitations: false,
default_role: 'member', default_role: 'member' as OrganizationRole,
}; } as Parameters<typeof service.buildUpdateData>[0];
const result = service.buildUpdateData(request); const result = service.buildUpdateData(request);
expect(result).toHaveProperty('restrictNewUserInvitations', false); expect(result).toHaveProperty('restrictNewUserInvitations', false);

View File

@ -1,4 +1,5 @@
import { db, slackIntegrations } from '@buster/database'; import { db, slackIntegrations } from '@buster/database';
import type { GetChannelsResponse, SlackErrorResponse } from '@buster/server-shared/slack';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { Hono } from 'hono'; import { Hono } from 'hono';
import type { Context } from 'hono'; import type { Context } from 'hono';
@ -90,7 +91,12 @@ describe.skipIf(skipIfNoEnv)('Slack Channels Integration Tests', () => {
app.use('*', async (c, next) => { app.use('*', async (c, next) => {
const path = c.req.path; const path = c.req.path;
if (path.includes('/channels')) { if (path.includes('/channels')) {
(c as Context).set('busterUser', { id: testUserId }); (c as Context).set('busterUser', {
id: testUserId,
name: 'Test User',
email: 'test@example.com',
avatarUrl: 'https://example.com/avatar.png',
});
(c as Context).set('organizationId', testOrganizationId); (c as Context).set('organizationId', testOrganizationId);
} }
await next(); await next();
@ -115,7 +121,7 @@ describe.skipIf(skipIfNoEnv)('Slack Channels Integration Tests', () => {
}); });
expect(response.status).toBe(404); expect(response.status).toBe(404);
const data = await response.json(); const data = (await response.json()) as SlackErrorResponse;
expect(data.error).toBe('No active Slack integration found'); expect(data.error).toBe('No active Slack integration found');
expect(data.code).toBe('INTEGRATION_NOT_FOUND'); expect(data.code).toBe('INTEGRATION_NOT_FOUND');
}); });
@ -150,14 +156,14 @@ describe.skipIf(skipIfNoEnv)('Slack Channels Integration Tests', () => {
}) })
.returning(); .returning();
createdIntegrationIds.push(integration.id); createdIntegrationIds.push(integration!.id);
const response = await app.request('/api/v2/slack/channels', { const response = await app.request('/api/v2/slack/channels', {
method: 'GET', method: 'GET',
}); });
expect(response.status).toBe(200); expect(response.status).toBe(200);
const data = await response.json(); const data = (await response.json()) as GetChannelsResponse;
expect(data.channels).toBeDefined(); expect(data.channels).toBeDefined();
expect(Array.isArray(data.channels)).toBe(true); expect(Array.isArray(data.channels)).toBe(true);
@ -166,7 +172,7 @@ describe.skipIf(skipIfNoEnv)('Slack Channels Integration Tests', () => {
expect(data.channels[0]).toHaveProperty('id'); expect(data.channels[0]).toHaveProperty('id');
expect(data.channels[0]).toHaveProperty('name'); expect(data.channels[0]).toHaveProperty('name');
// Should not have other properties (only id and name as requested) // Should not have other properties (only id and name as requested)
expect(Object.keys(data.channels[0])).toEqual(['id', 'name']); expect(Object.keys(data.channels[0]!)).toEqual(['id', 'name']);
} }
// Clean up the secret if we created one // Clean up the secret if we created one
@ -200,7 +206,7 @@ describe.skipIf(skipIfNoEnv)('Slack Channels Integration Tests', () => {
}) })
.returning(); .returning();
createdIntegrationIds.push(integration.id); createdIntegrationIds.push(integration!.id);
await app.request('/api/v2/slack/channels', { await app.request('/api/v2/slack/channels', {
method: 'GET', method: 'GET',
@ -210,11 +216,11 @@ describe.skipIf(skipIfNoEnv)('Slack Channels Integration Tests', () => {
const [updated] = await db const [updated] = await db
.select() .select()
.from(slackIntegrations) .from(slackIntegrations)
.where(eq(slackIntegrations.id, integration.id)); .where(eq(slackIntegrations.id, integration!.id));
expect(updated.lastUsedAt).toBeTruthy(); expect(updated!.lastUsedAt).toBeTruthy();
expect(new Date(updated.lastUsedAt!).getTime()).toBeGreaterThan( expect(new Date(updated!.lastUsedAt!).getTime()).toBeGreaterThan(
new Date(integration.createdAt).getTime() new Date(integration!.createdAt).getTime()
); );
}); });
@ -231,7 +237,12 @@ describe.skipIf(skipIfNoEnv)('Slack Channels Integration Tests', () => {
const testApp = new Hono(); const testApp = new Hono();
testApp.use('*', async (c, next) => { testApp.use('*', async (c, next) => {
if (c.req.path.includes('/channels')) { if (c.req.path.includes('/channels')) {
(c as Context).set('busterUser', { id: testUserId }); (c as Context).set('busterUser', {
id: testUserId,
name: 'Test User',
email: 'test@example.com',
avatarUrl: 'https://example.com/avatar.png',
});
(c as Context).set('organizationId', testOrganizationId); (c as Context).set('organizationId', testOrganizationId);
} }
await next(); await next();
@ -243,7 +254,7 @@ describe.skipIf(skipIfNoEnv)('Slack Channels Integration Tests', () => {
}); });
expect(response.status).toBe(503); expect(response.status).toBe(503);
const data = await response.json(); const data = (await response.json()) as SlackErrorResponse;
expect(data.error).toBe('Slack integration is not configured'); expect(data.error).toBe('Slack integration is not configured');
expect(data.code).toBe('INTEGRATION_NOT_CONFIGURED'); expect(data.code).toBe('INTEGRATION_NOT_CONFIGURED');
@ -270,14 +281,14 @@ describe.skipIf(skipIfNoEnv)('Slack Channels Integration Tests', () => {
}) })
.returning(); .returning();
createdIntegrationIds.push(integration.id); createdIntegrationIds.push(integration!.id);
const response = await app.request('/api/v2/slack/channels', { const response = await app.request('/api/v2/slack/channels', {
method: 'GET', method: 'GET',
}); });
expect(response.status).toBe(500); expect(response.status).toBe(500);
const data = await response.json(); const data = (await response.json()) as SlackErrorResponse;
expect(data.error).toBe('Failed to retrieve authentication token'); expect(data.error).toBe('Failed to retrieve authentication token');
expect(data.code).toBe('TOKEN_RETRIEVAL_ERROR'); expect(data.code).toBe('TOKEN_RETRIEVAL_ERROR');
}); });

View File

@ -1,4 +1,10 @@
import { db, slackIntegrations } from '@buster/database'; import { db, slackIntegrations } from '@buster/database';
import type {
GetIntegrationResponse,
InitiateOAuthResponse,
RemoveIntegrationResponse,
SlackErrorResponse,
} from '@buster/server-shared/slack';
import { and, eq } from 'drizzle-orm'; import { and, eq } from 'drizzle-orm';
import { Hono } from 'hono'; import { Hono } from 'hono';
import type { Context } from 'hono'; import type { Context } from 'hono';
@ -110,7 +116,12 @@ describe.skipIf(skipIfNoEnv)('SlackHandler Integration Tests', () => {
// Set auth context for protected routes // Set auth context for protected routes
const path = c.req.path; const path = c.req.path;
if (path.includes('/auth/init') || path.includes('/integration')) { if (path.includes('/auth/init') || path.includes('/integration')) {
(c as Context).set('busterUser', { id: testUserId }); (c as Context).set('busterUser', {
id: testUserId,
name: 'Test User',
email: 'test@example.com',
avatarUrl: 'https://example.com/avatar.png',
});
(c as Context).set('organizationId', testOrganizationId); (c as Context).set('organizationId', testOrganizationId);
} }
await next(); await next();
@ -145,7 +156,12 @@ describe.skipIf(skipIfNoEnv)('SlackHandler Integration Tests', () => {
const testApp = new Hono(); const testApp = new Hono();
testApp.use('*', async (c, next) => { testApp.use('*', async (c, next) => {
if (c.req.path.includes('/auth/init')) { if (c.req.path.includes('/auth/init')) {
(c as Context).set('busterUser', { id: testUserId }); (c as Context).set('busterUser', {
id: testUserId,
name: 'Test User',
email: 'test@example.com',
avatarUrl: 'https://example.com/avatar.png',
});
(c as Context).set('organizationId', testOrganizationId); (c as Context).set('organizationId', testOrganizationId);
} }
await next(); await next();
@ -161,7 +177,7 @@ describe.skipIf(skipIfNoEnv)('SlackHandler Integration Tests', () => {
}); });
expect(response.status).toBe(503); expect(response.status).toBe(503);
const data = await response.json(); const data = (await response.json()) as SlackErrorResponse;
expect(data.error).toBe('Slack integration is not enabled'); expect(data.error).toBe('Slack integration is not enabled');
expect(data.code).toBe('INTEGRATION_DISABLED'); expect(data.code).toBe('INTEGRATION_DISABLED');
@ -221,8 +237,8 @@ describe.skipIf(skipIfNoEnv)('SlackHandler Integration Tests', () => {
}); });
expect(response.status).toBe(200); expect(response.status).toBe(200);
const data = await response.json(); const data = (await response.json()) as InitiateOAuthResponse;
expect(data.authUrl).toContain('https://slack.com/oauth'); expect(data.auth_url).toContain('https://slack.com/oauth');
expect(data.state).toBeTruthy(); expect(data.state).toBeTruthy();
// Verify pending integration was created in database // Verify pending integration was created in database
@ -232,12 +248,12 @@ describe.skipIf(skipIfNoEnv)('SlackHandler Integration Tests', () => {
.where(eq(slackIntegrations.organizationId, testOrganizationId)); .where(eq(slackIntegrations.organizationId, testOrganizationId));
expect(pending).toBeTruthy(); expect(pending).toBeTruthy();
createdIntegrationIds.push(pending.id); createdIntegrationIds.push(pending!.id);
expect(pending.status).toBe('pending'); expect(pending!.status).toBe('pending');
expect(pending.userId).toBe(testUserId); expect(pending!.userId).toBe(testUserId);
expect(pending.oauthState).toBeTruthy(); expect(pending!.oauthState).toBeTruthy();
expect(pending.oauthMetadata).toMatchObject({ expect(pending!.oauthMetadata).toMatchObject({
returnUrl: '/dashboard', returnUrl: '/dashboard',
source: 'settings', source: 'settings',
projectId: '550e8400-e29b-41d4-a716-446655440000', projectId: '550e8400-e29b-41d4-a716-446655440000',
@ -261,7 +277,7 @@ describe.skipIf(skipIfNoEnv)('SlackHandler Integration Tests', () => {
}) })
.returning(); .returning();
createdIntegrationIds.push(existing.id); createdIntegrationIds.push(existing!.id);
const response = await app.request('/api/v2/slack/auth/init', { const response = await app.request('/api/v2/slack/auth/init', {
method: 'POST', method: 'POST',
@ -272,7 +288,7 @@ describe.skipIf(skipIfNoEnv)('SlackHandler Integration Tests', () => {
}); });
expect(response.status).toBe(409); expect(response.status).toBe(409);
const data = await response.json(); const data = (await response.json()) as SlackErrorResponse;
expect(data.error).toBe('Organization already has an active Slack integration'); expect(data.error).toBe('Organization already has an active Slack integration');
expect(data.code).toBe('INTEGRATION_EXISTS'); expect(data.code).toBe('INTEGRATION_EXISTS');
}); });
@ -317,7 +333,7 @@ describe.skipIf(skipIfNoEnv)('SlackHandler Integration Tests', () => {
}) })
.returning(); .returning();
createdIntegrationIds.push(pending.id); createdIntegrationIds.push(pending!.id);
const response = await app.request( const response = await app.request(
`/api/v2/slack/auth/callback?code=test-code&state=${testState}`, `/api/v2/slack/auth/callback?code=test-code&state=${testState}`,
@ -333,14 +349,14 @@ describe.skipIf(skipIfNoEnv)('SlackHandler Integration Tests', () => {
const [activated] = await db const [activated] = await db
.select() .select()
.from(slackIntegrations) .from(slackIntegrations)
.where(eq(slackIntegrations.id, pending.id)); .where(eq(slackIntegrations.id, pending!.id));
expect(activated.status).toBe('active'); expect(activated!.status).toBe('active');
expect(activated.teamName).toBe('Test Workspace'); expect(activated!.teamName).toBe('Test Workspace');
expect(activated.tokenVaultKey).toBe(`slack-token-${pending.id}`); expect(activated!.tokenVaultKey).toBe(`slack-token-${pending!.id}`);
expect(activated.installedAt).toBeTruthy(); expect(activated!.installedAt).toBeTruthy();
expect(activated.oauthState).toBeNull(); expect(activated!.oauthState).toBeNull();
expect(activated.oauthExpiresAt).toBeNull(); expect(activated!.oauthExpiresAt).toBeNull();
}); });
it('should handle invalid state', async () => { it('should handle invalid state', async () => {
@ -390,19 +406,19 @@ describe.skipIf(skipIfNoEnv)('SlackHandler Integration Tests', () => {
}) })
.returning(); .returning();
createdIntegrationIds.push(integration.id); createdIntegrationIds.push(integration!.id);
const response = await app.request('/api/v2/slack/integration', { const response = await app.request('/api/v2/slack/integration', {
method: 'GET', method: 'GET',
}); });
expect(response.status).toBe(200); expect(response.status).toBe(200);
const data = await response.json(); const data = (await response.json()) as GetIntegrationResponse;
expect(data.connected).toBe(true); expect(data.connected).toBe(true);
expect(data.integration).toBeDefined(); expect(data.integration).toBeDefined();
expect(data.integration.id).toBe(integration.id); expect(data.integration!.id).toBe(integration!.id);
expect(data.integration.teamName).toBe('Status Test Workspace'); expect(data.integration!.team_name).toBe('Status Test Workspace');
expect(data.integration.teamDomain).toBe('status-test'); expect(data.integration!.team_domain).toBe('status-test');
}); });
it('should return not connected when no integration exists', async () => { it('should return not connected when no integration exists', async () => {
@ -416,7 +432,7 @@ describe.skipIf(skipIfNoEnv)('SlackHandler Integration Tests', () => {
}); });
expect(response.status).toBe(200); expect(response.status).toBe(200);
const data = await response.json(); const data = (await response.json()) as GetIntegrationResponse;
expect(data.connected).toBe(false); expect(data.connected).toBe(false);
expect(data.integration).toBeUndefined(); expect(data.integration).toBeUndefined();
}); });
@ -459,14 +475,14 @@ describe.skipIf(skipIfNoEnv)('SlackHandler Integration Tests', () => {
}) })
.returning(); .returning();
createdIntegrationIds.push(integration.id); createdIntegrationIds.push(integration!.id);
const response = await app.request('/api/v2/slack/integration', { const response = await app.request('/api/v2/slack/integration', {
method: 'DELETE', method: 'DELETE',
}); });
expect(response.status).toBe(200); expect(response.status).toBe(200);
const data = await response.json(); const data = (await response.json()) as RemoveIntegrationResponse;
expect(data.message).toBe('Slack integration removed successfully'); expect(data.message).toBe('Slack integration removed successfully');
// Wait a moment for the database to update // Wait a moment for the database to update
@ -476,13 +492,13 @@ describe.skipIf(skipIfNoEnv)('SlackHandler Integration Tests', () => {
const removedList = await db const removedList = await db
.select() .select()
.from(slackIntegrations) .from(slackIntegrations)
.where(eq(slackIntegrations.id, integration.id)); .where(eq(slackIntegrations.id, integration!.id));
expect(removedList.length).toBe(1); expect(removedList.length).toBe(1);
const removed = removedList[0]; const removed = removedList[0];
expect(removed).toBeDefined(); expect(removed).toBeDefined();
expect(removed.status).toBe('revoked'); expect(removed!.status).toBe('revoked');
expect(removed.deletedAt).toBeTruthy(); expect(removed!.deletedAt).toBeTruthy();
}); });
it('should handle non-existent integration', async () => { it('should handle non-existent integration', async () => {
@ -499,7 +515,7 @@ describe.skipIf(skipIfNoEnv)('SlackHandler Integration Tests', () => {
}); });
expect(response.status).toBe(404); expect(response.status).toBe(404);
const data = await response.json(); const data = (await response.json()) as SlackErrorResponse;
expect(data.error).toBe('No active Slack integration found'); expect(data.error).toBe('No active Slack integration found');
expect(data.code).toBe('INTEGRATION_NOT_FOUND'); expect(data.code).toBe('INTEGRATION_NOT_FOUND');
}); });

View File

@ -1,7 +1,8 @@
import { getUserOrganizationId } from '@buster/database'; import { getUserOrganizationId } from '@buster/database';
import { SlackError } from '@buster/server-shared/slack';
import { HTTPException } from 'hono/http-exception'; import { HTTPException } from 'hono/http-exception';
import { beforeEach, describe, expect, it, vi } from 'vitest'; import { beforeEach, describe, expect, it, vi } from 'vitest';
import { SlackError, SlackHandler } from './handler'; import { SlackHandler } from './handler';
// Mock dependencies // Mock dependencies
const mockSlackOAuthService = { const mockSlackOAuthService = {
@ -82,7 +83,7 @@ describe('SlackHandler', () => {
// Mock getUserOrganizationId to return org info // Mock getUserOrganizationId to return org info
vi.mocked(getUserOrganizationId).mockResolvedValue({ vi.mocked(getUserOrganizationId).mockResolvedValue({
organizationId: 'org-123', organizationId: 'org-123',
userId: 'user-123', role: 'owner',
}); });
const mockResult = { const mockResult = {
@ -115,7 +116,7 @@ describe('SlackHandler', () => {
// Mock getUserOrganizationId to return org info // Mock getUserOrganizationId to return org info
vi.mocked(getUserOrganizationId).mockResolvedValue({ vi.mocked(getUserOrganizationId).mockResolvedValue({
organizationId: 'org-123', organizationId: 'org-123',
userId: 'user-123', role: 'owner',
}); });
vi.mocked(mockSlackOAuthService.initiateOAuth).mockRejectedValue( vi.mocked(mockSlackOAuthService.initiateOAuth).mockRejectedValue(
@ -209,7 +210,7 @@ describe('SlackHandler', () => {
// Mock getUserOrganizationId to return org info // Mock getUserOrganizationId to return org info
vi.mocked(getUserOrganizationId).mockResolvedValue({ vi.mocked(getUserOrganizationId).mockResolvedValue({
organizationId: 'org-123', organizationId: 'org-123',
userId: 'user-123', role: 'owner',
}); });
const mockStatus = { const mockStatus = {
@ -249,7 +250,7 @@ describe('SlackHandler', () => {
// Mock getUserOrganizationId to return org info // Mock getUserOrganizationId to return org info
vi.mocked(getUserOrganizationId).mockResolvedValue({ vi.mocked(getUserOrganizationId).mockResolvedValue({
organizationId: 'org-123', organizationId: 'org-123',
userId: 'user-123', role: 'owner',
}); });
vi.mocked(mockSlackOAuthService.removeIntegration).mockResolvedValue({ vi.mocked(mockSlackOAuthService.removeIntegration).mockResolvedValue({
@ -273,7 +274,7 @@ describe('SlackHandler', () => {
// Mock getUserOrganizationId to return org info // Mock getUserOrganizationId to return org info
vi.mocked(getUserOrganizationId).mockResolvedValue({ vi.mocked(getUserOrganizationId).mockResolvedValue({
organizationId: 'org-123', organizationId: 'org-123',
userId: 'user-123', role: 'owner',
}); });
vi.mocked(mockSlackOAuthService.removeIntegration).mockResolvedValue({ vi.mocked(mockSlackOAuthService.removeIntegration).mockResolvedValue({

View File

@ -136,12 +136,12 @@ describe.skipIf(skipIfNoDatabase)('Slack Helpers Database Integration Tests', ()
}) })
.returning(); .returning();
createdIntegrationIds.push(integration.id); createdIntegrationIds.push(integration!.id);
const result = await slackHelpers.getActiveIntegration(testOrganizationId); const result = await slackHelpers.getActiveIntegration(testOrganizationId);
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(result?.id).toBe(integration.id); expect(result?.id).toBe(integration!.id);
expect(result?.status).toBe('active'); expect(result?.status).toBe('active');
expect(result?.teamName).toBe('Test Workspace'); expect(result?.teamName).toBe('Test Workspace');
expect(result?.organizationId).toBe(testOrganizationId); expect(result?.organizationId).toBe(testOrganizationId);
@ -163,7 +163,7 @@ describe.skipIf(skipIfNoDatabase)('Slack Helpers Database Integration Tests', ()
}) })
.returning(); .returning();
createdIntegrationIds.push(integration.id); createdIntegrationIds.push(integration!.id);
const result = await slackHelpers.getActiveIntegration(testOrganizationId); const result = await slackHelpers.getActiveIntegration(testOrganizationId);
expect(result).toBeNull(); expect(result).toBeNull();
@ -192,14 +192,14 @@ describe.skipIf(skipIfNoDatabase)('Slack Helpers Database Integration Tests', ()
.where(eq(slackIntegrations.id, integrationId)); .where(eq(slackIntegrations.id, integrationId));
expect(created).toBeTruthy(); expect(created).toBeTruthy();
expect(created.organizationId).toBe(testOrganizationId); expect(created!.organizationId).toBe(testOrganizationId);
expect(created.userId).toBe(testUserId); expect(created!.userId).toBe(testUserId);
expect(created.status).toBe('pending'); expect(created!.status).toBe('pending');
expect(created.oauthState).toBe(testIds.oauthState); expect(created!.oauthState).toBe(testIds.oauthState);
expect(created.oauthMetadata).toEqual(metadata); expect(created!.oauthMetadata).toEqual(metadata);
// Check expiry is set to ~15 minutes // Check expiry is set to ~15 minutes
const expiryTime = new Date(created.oauthExpiresAt!).getTime(); const expiryTime = new Date(created!.oauthExpiresAt!).getTime();
const expectedExpiry = Date.now() + 15 * 60 * 1000; const expectedExpiry = Date.now() + 15 * 60 * 1000;
expect(Math.abs(expiryTime - expectedExpiry)).toBeLessThan(30000); // Within 30 seconds expect(Math.abs(expiryTime - expectedExpiry)).toBeLessThan(30000); // Within 30 seconds
}); });
@ -219,7 +219,7 @@ describe.skipIf(skipIfNoDatabase)('Slack Helpers Database Integration Tests', ()
}) })
.returning(); .returning();
createdIntegrationIds.push(activeIntegration.id); createdIntegrationIds.push(activeIntegration!.id);
// Try to create a new pending integration // Try to create a new pending integration
await expect( await expect(
@ -248,17 +248,17 @@ describe.skipIf(skipIfNoDatabase)('Slack Helpers Database Integration Tests', ()
}) })
.returning(); .returning();
createdIntegrationIds.push(pending.id); createdIntegrationIds.push(pending!.id);
// Update it with OAuth response data // Update it with OAuth response data
await slackHelpers.updateIntegrationAfterOAuth(pending.id, { await slackHelpers.updateIntegrationAfterOAuth(pending!.id, {
teamId: testIds.teamId, teamId: testIds.teamId,
teamName: 'OAuth Workspace', teamName: 'OAuth Workspace',
teamDomain: 'oauth-workspace', teamDomain: 'oauth-workspace',
enterpriseId: testIds.enterpriseId, enterpriseId: testIds.enterpriseId,
botUserId: testIds.botUserId, botUserId: testIds.botUserId,
scope: 'channels:read chat:write channels:join', scope: 'channels:read chat:write channels:join',
tokenVaultKey: `slack-token-${pending.id}`, tokenVaultKey: `slack-token-${pending!.id}`,
installedBySlackUserId: 'U11111', installedBySlackUserId: 'U11111',
}); });
@ -266,20 +266,20 @@ describe.skipIf(skipIfNoDatabase)('Slack Helpers Database Integration Tests', ()
const [updated] = await db const [updated] = await db
.select() .select()
.from(slackIntegrations) .from(slackIntegrations)
.where(eq(slackIntegrations.id, pending.id)); .where(eq(slackIntegrations.id, pending!.id));
expect(updated.status).toBe('active'); expect(updated!.status).toBe('active');
expect(updated.teamId).toBe(testIds.teamId); expect(updated!.teamId).toBe(testIds.teamId);
expect(updated.teamName).toBe('OAuth Workspace'); expect(updated!.teamName).toBe('OAuth Workspace');
expect(updated.teamDomain).toBe('oauth-workspace'); expect(updated!.teamDomain).toBe('oauth-workspace');
expect(updated.enterpriseId).toBe(testIds.enterpriseId); expect(updated!.enterpriseId).toBe(testIds.enterpriseId);
expect(updated.botUserId).toBe(testIds.botUserId); expect(updated!.botUserId).toBe(testIds.botUserId);
expect(updated.scope).toBe('channels:read chat:write channels:join'); expect(updated!.scope).toBe('channels:read chat:write channels:join');
expect(updated.tokenVaultKey).toBe(`slack-token-${pending.id}`); expect(updated!.tokenVaultKey).toBe(`slack-token-${pending!.id}`);
expect(updated.installedBySlackUserId).toBe('U11111'); expect(updated!.installedBySlackUserId).toBe('U11111');
expect(updated.installedAt).toBeTruthy(); expect(updated!.installedAt).toBeTruthy();
expect(updated.oauthState).toBeNull(); expect(updated!.oauthState).toBeNull();
expect(updated.oauthExpiresAt).toBeNull(); expect(updated!.oauthExpiresAt).toBeNull();
}); });
}); });
@ -302,7 +302,7 @@ describe.skipIf(skipIfNoDatabase)('Slack Helpers Database Integration Tests', ()
}) })
.returning(); .returning();
createdIntegrationIds.push(integration.id); createdIntegrationIds.push(integration!.id);
// Update default channel // Update default channel
const defaultChannel = { const defaultChannel = {
@ -310,17 +310,17 @@ describe.skipIf(skipIfNoDatabase)('Slack Helpers Database Integration Tests', ()
id: 'C12345', id: 'C12345',
}; };
await slackHelpers.updateDefaultChannel(integration.id, defaultChannel); await slackHelpers.updateDefaultChannel(integration!.id, defaultChannel);
// Verify the update // Verify the update
const [updated] = await db const [updated] = await db
.select() .select()
.from(slackIntegrations) .from(slackIntegrations)
.where(eq(slackIntegrations.id, integration.id)); .where(eq(slackIntegrations.id, integration!.id));
expect(updated.defaultChannel).toEqual(defaultChannel); expect(updated!.defaultChannel).toEqual(defaultChannel);
expect(new Date(updated.updatedAt).getTime()).toBeGreaterThan( expect(new Date(updated!.updatedAt).getTime()).toBeGreaterThan(
new Date(updated.createdAt).getTime() new Date(updated!.createdAt).getTime()
); );
}); });
}); });
@ -344,21 +344,21 @@ describe.skipIf(skipIfNoDatabase)('Slack Helpers Database Integration Tests', ()
}) })
.returning(); .returning();
createdIntegrationIds.push(integration.id); createdIntegrationIds.push(integration!.id);
// Soft delete it // Soft delete it
await slackHelpers.softDeleteIntegration(integration.id); await slackHelpers.softDeleteIntegration(integration!.id);
// Verify it was soft deleted // Verify it was soft deleted
const [deleted] = await db const [deleted] = await db
.select() .select()
.from(slackIntegrations) .from(slackIntegrations)
.where(eq(slackIntegrations.id, integration.id)); .where(eq(slackIntegrations.id, integration!.id));
expect(deleted.status).toBe('revoked'); expect(deleted!.status).toBe('revoked');
expect(deleted.deletedAt).toBeTruthy(); expect(deleted!.deletedAt).toBeTruthy();
expect(new Date(deleted.updatedAt).getTime()).toBeGreaterThan( expect(new Date(deleted!.updatedAt).getTime()).toBeGreaterThan(
new Date(deleted.createdAt).getTime() new Date(deleted!.createdAt).getTime()
); );
}); });
}); });
@ -382,23 +382,23 @@ describe.skipIf(skipIfNoDatabase)('Slack Helpers Database Integration Tests', ()
}) })
.returning(); .returning();
createdIntegrationIds.push(integration.id); createdIntegrationIds.push(integration!.id);
// Wait a bit to ensure timestamp difference // Wait a bit to ensure timestamp difference
await new Promise((resolve) => setTimeout(resolve, 100)); await new Promise((resolve) => setTimeout(resolve, 100));
// Update last used // Update last used
await slackHelpers.updateLastUsedAt(integration.id); await slackHelpers.updateLastUsedAt(integration!.id);
// Verify the update // Verify the update
const [updated] = await db const [updated] = await db
.select() .select()
.from(slackIntegrations) .from(slackIntegrations)
.where(eq(slackIntegrations.id, integration.id)); .where(eq(slackIntegrations.id, integration!.id));
expect(updated.lastUsedAt).toBeTruthy(); expect(updated!.lastUsedAt).toBeTruthy();
expect(new Date(updated.lastUsedAt!).getTime()).toBeGreaterThan( expect(new Date(updated!.lastUsedAt!).getTime()).toBeGreaterThan(
new Date(updated.createdAt).getTime() new Date(updated!.createdAt).getTime()
); );
}); });
}); });
@ -424,7 +424,7 @@ describe.skipIf(skipIfNoDatabase)('Slack Helpers Database Integration Tests', ()
}) })
.returning(); .returning();
createdIntegrationIds.push(integration.id); createdIntegrationIds.push(integration!.id);
const result = await slackHelpers.hasActiveIntegration(testOrganizationId); const result = await slackHelpers.hasActiveIntegration(testOrganizationId);
expect(result).toBe(true); expect(result).toBe(true);
@ -446,12 +446,12 @@ describe.skipIf(skipIfNoDatabase)('Slack Helpers Database Integration Tests', ()
}) })
.returning(); .returning();
createdIntegrationIds.push(integration.id); createdIntegrationIds.push(integration!.id);
const result = await slackHelpers.getPendingIntegrationByState(testIds.oauthState); const result = await slackHelpers.getPendingIntegrationByState(testIds.oauthState);
expect(result).toBeTruthy(); expect(result).toBeTruthy();
expect(result?.id).toBe(integration.id); expect(result?.id).toBe(integration!.id);
expect(result?.oauthState).toBe(testIds.oauthState); expect(result?.oauthState).toBe(testIds.oauthState);
}); });
@ -469,7 +469,7 @@ describe.skipIf(skipIfNoDatabase)('Slack Helpers Database Integration Tests', ()
}) })
.returning(); .returning();
createdIntegrationIds.push(integration.id); createdIntegrationIds.push(integration!.id);
const result = await slackHelpers.getPendingIntegrationByState(testIds.oauthState); const result = await slackHelpers.getPendingIntegrationByState(testIds.oauthState);
expect(result).toBeNull(); expect(result).toBeNull();
@ -505,7 +505,7 @@ describe.skipIf(skipIfNoDatabase)('Slack Helpers Database Integration Tests', ()
}) })
.returning(); .returning();
createdIntegrationIds.push(valid.id); // Only track valid one since expired will be deleted createdIntegrationIds.push(valid!.id); // Only track valid one since expired will be deleted
// Run cleanup // Run cleanup
const deletedCount = await slackHelpers.cleanupExpiredPendingIntegrations(); const deletedCount = await slackHelpers.cleanupExpiredPendingIntegrations();
@ -516,7 +516,7 @@ describe.skipIf(skipIfNoDatabase)('Slack Helpers Database Integration Tests', ()
const [expiredCheck] = await db const [expiredCheck] = await db
.select() .select()
.from(slackIntegrations) .from(slackIntegrations)
.where(eq(slackIntegrations.id, expired.id)); .where(eq(slackIntegrations.id, expired!.id));
expect(expiredCheck).toBeUndefined(); expect(expiredCheck).toBeUndefined();
@ -524,7 +524,7 @@ describe.skipIf(skipIfNoDatabase)('Slack Helpers Database Integration Tests', ()
const [validCheck] = await db const [validCheck] = await db
.select() .select()
.from(slackIntegrations) .from(slackIntegrations)
.where(eq(slackIntegrations.id, valid.id)); .where(eq(slackIntegrations.id, valid!.id));
expect(validCheck).toBeTruthy(); expect(validCheck).toBeTruthy();
}); });

View File

@ -138,12 +138,12 @@ describe.skipIf(skipIfNoEnv)('SlackOAuthService Integration Tests', () => {
.where(eq(slackIntegrations.organizationId, testOrganizationId)); .where(eq(slackIntegrations.organizationId, testOrganizationId));
expect(pending).toBeTruthy(); expect(pending).toBeTruthy();
createdIntegrationIds.push(pending.id); createdIntegrationIds.push(pending!.id);
expect(pending.status).toBe('pending'); expect(pending!.status).toBe('pending');
expect(pending.userId).toBe(testUserId); expect(pending!.userId).toBe(testUserId);
expect(pending.oauthState).toBeTruthy(); expect(pending!.oauthState).toBeTruthy();
expect(pending.oauthMetadata).toMatchObject({ expect(pending!.oauthMetadata).toMatchObject({
returnUrl: '/dashboard', returnUrl: '/dashboard',
source: 'settings', source: 'settings',
projectId: '550e8400-e29b-41d4-a716-446655440000', projectId: '550e8400-e29b-41d4-a716-446655440000',
@ -168,7 +168,7 @@ describe.skipIf(skipIfNoEnv)('SlackOAuthService Integration Tests', () => {
}) })
.returning(); .returning();
createdIntegrationIds.push(existing.id); createdIntegrationIds.push(existing!.id);
// Try to initiate OAuth again // Try to initiate OAuth again
await expect( await expect(
@ -202,9 +202,9 @@ describe.skipIf(skipIfNoEnv)('SlackOAuthService Integration Tests', () => {
source: 'onboarding', source: 'onboarding',
}, },
}) })
.returning(); .returning()!;
createdIntegrationIds.push(pending.id); createdIntegrationIds.push(pending!.id);
const testIds = generateTestIds(); const testIds = generateTestIds();
// Mock the auth service to return specific state // Mock the auth service to return specific state
@ -227,7 +227,7 @@ describe.skipIf(skipIfNoEnv)('SlackOAuthService Integration Tests', () => {
}); });
expect(result.success).toBe(true); expect(result.success).toBe(true);
expect(result.integrationId).toBe(pending.id); expect(result.integrationId).toBe(pending!.id);
expect(result.teamName).toBe('Callback Workspace'); expect(result.teamName).toBe('Callback Workspace');
expect(result.metadata).toMatchObject({ expect(result.metadata).toMatchObject({
returnUrl: '/settings', returnUrl: '/settings',
@ -241,22 +241,22 @@ describe.skipIf(skipIfNoEnv)('SlackOAuthService Integration Tests', () => {
const activatedList = await db const activatedList = await db
.select() .select()
.from(slackIntegrations) .from(slackIntegrations)
.where(eq(slackIntegrations.id, pending.id)); .where(eq(slackIntegrations.id, pending!.id));
expect(activatedList.length).toBe(1); expect(activatedList.length).toBe(1);
const activated = activatedList[0]; const activated = activatedList[0];
expect(activated).toBeDefined(); expect(activated).toBeDefined();
expect(activated.status).toBe('active'); expect(activated!.status).toBe('active');
expect(activated.teamId).toBe(testIds.teamId); expect(activated!.teamId).toBe(testIds.teamId);
expect(activated.teamName).toBe('Callback Workspace'); expect(activated!.teamName).toBe('Callback Workspace');
expect(activated.teamDomain).toBe('callback-workspace'); expect(activated!.teamDomain).toBe('callback-workspace');
expect(activated.botUserId).toBe(testIds.botUserId); expect(activated!.botUserId).toBe(testIds.botUserId);
expect(activated.scope).toBe('channels:read,chat:write,channels:join'); expect(activated!.scope).toBe('channels:read,chat:write,channels:join');
expect(activated.tokenVaultKey).toBe(`slack-token-${pending.id}`); expect(activated!.tokenVaultKey).toBe(`slack-token-${pending!.id}`);
expect(activated.installedBySlackUserId).toBe('U88888'); expect(activated!.installedBySlackUserId).toBe('U88888');
expect(activated.installedAt).toBeTruthy(); expect(activated!.installedAt).toBeTruthy();
expect(activated.oauthState).toBeNull(); expect(activated!.oauthState).toBeNull();
expect(activated.oauthExpiresAt).toBeNull(); expect(activated!.oauthExpiresAt).toBeNull();
}); });
it('should handle invalid state', async () => { it('should handle invalid state', async () => {
@ -299,13 +299,13 @@ describe.skipIf(skipIfNoEnv)('SlackOAuthService Integration Tests', () => {
expect(result.success).toBe(false); expect(result.success).toBe(false);
expect(result.error).toBe('Failed to exchange code for token'); expect(result.error).toBe('Failed to exchange code for token');
expect(result.integrationId).toBe(pending.id); expect(result.integrationId).toBe(pending!.id);
// Verify the pending integration was deleted (to allow retry) // Verify the pending integration was deleted (to allow retry)
const integrations = await db const integrations = await db
.select() .select()
.from(slackIntegrations) .from(slackIntegrations)
.where(eq(slackIntegrations.id, pending.id)); .where(eq(slackIntegrations.id, pending!.id));
expect(integrations.length).toBe(0); expect(integrations.length).toBe(0);
}); });
@ -332,13 +332,13 @@ describe.skipIf(skipIfNoEnv)('SlackOAuthService Integration Tests', () => {
}) })
.returning(); .returning();
createdIntegrationIds.push(integration.id); createdIntegrationIds.push(integration!.id);
const status = await service.getIntegrationStatus(testOrganizationId); const status = await service.getIntegrationStatus(testOrganizationId);
expect(status.connected).toBe(true); expect(status.connected).toBe(true);
expect(status.integration).toBeDefined(); expect(status.integration).toBeDefined();
expect(status.integration!.id).toBe(integration.id); expect(status.integration!.id).toBe(integration!.id);
expect(status.integration!.teamName).toBe('Status Test Workspace'); expect(status.integration!.teamName).toBe('Status Test Workspace');
expect(status.integration!.teamDomain).toBe('status-test'); expect(status.integration!.teamDomain).toBe('status-test');
expect(status.integration!.installedAt).toContain('2024-01-01'); expect(status.integration!.installedAt).toContain('2024-01-01');
@ -380,7 +380,7 @@ describe.skipIf(skipIfNoEnv)('SlackOAuthService Integration Tests', () => {
.returning(); .returning();
// Store the ID immediately // Store the ID immediately
const integrationId = integration.id; const integrationId = integration!.id;
createdIntegrationIds.push(integrationId); createdIntegrationIds.push(integrationId);
// Verify it was created // Verify it was created
@ -390,7 +390,7 @@ describe.skipIf(skipIfNoEnv)('SlackOAuthService Integration Tests', () => {
.where(eq(slackIntegrations.id, integrationId)); .where(eq(slackIntegrations.id, integrationId));
expect(created).toBeDefined(); expect(created).toBeDefined();
expect(created.status).toBe('active'); expect(created!.status).toBe('active');
// The service will try to delete the token from vault // The service will try to delete the token from vault
// In integration tests, we let it run naturally // In integration tests, we let it run naturally
@ -410,8 +410,8 @@ describe.skipIf(skipIfNoEnv)('SlackOAuthService Integration Tests', () => {
expect(removedIntegrations.length).toBe(1); expect(removedIntegrations.length).toBe(1);
const removed = removedIntegrations[0]; const removed = removedIntegrations[0];
expect(removed).toBeDefined(); expect(removed).toBeDefined();
expect(removed.status).toBe('revoked'); expect(removed!.status).toBe('revoked');
expect(removed.deletedAt).toBeTruthy(); expect(removed!.deletedAt).toBeTruthy();
}); });
it('should handle non-existent integration', async () => { it('should handle non-existent integration', async () => {

View File

@ -136,7 +136,7 @@ describe.skipIf(skipIfNoEnv)('Token Storage Integration Tests', () => {
}) })
.returning(); .returning();
createdIntegrationIds.push(integration.id); createdIntegrationIds.push(integration!.id);
// Retrieve state // Retrieve state
const stateData = await storage.getState(testState); const stateData = await storage.getState(testState);
@ -167,7 +167,7 @@ describe.skipIf(skipIfNoEnv)('Token Storage Integration Tests', () => {
}) })
.returning(); .returning();
createdIntegrationIds.push(integration.id); createdIntegrationIds.push(integration!.id);
// Should return null for expired state // Should return null for expired state
const stateData = await storage.getState(testState); const stateData = await storage.getState(testState);

View File

@ -6,5 +6,14 @@
"tsBuildInfoFile": "dist/.cache/tsbuildinfo.json" "tsBuildInfoFile": "dist/.cache/tsbuildinfo.json"
}, },
"include": ["src"], "include": ["src"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts"] "exclude": ["node_modules", "dist", "**/*.test.tsx", "**/*.test.ts"],
"references": [
{ "path": "../../packages/access-controls" },
{ "path": "../../packages/ai" },
{ "path": "../../packages/database" },
{ "path": "../../packages/server-shared" },
{ "path": "../../packages/slack" },
{ "path": "../../packages/test-utils" },
{ "path": "../../packages/vitest-config" }
]
} }

View File

@ -7,5 +7,14 @@
"tsBuildInfoFile": "dist/.cache/tsbuildinfo.json" "tsBuildInfoFile": "dist/.cache/tsbuildinfo.json"
}, },
"include": ["src/**/*", "tests/**/*", "env.d.ts"], "include": ["src/**/*", "tests/**/*", "env.d.ts"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"],
"references": [
{ "path": "../../packages/access-controls" },
{ "path": "../../packages/ai" },
{ "path": "../../packages/database" },
{ "path": "../../packages/server-shared" },
{ "path": "../../packages/slack" },
{ "path": "../../packages/test-utils" },
{ "path": "../../packages/vitest-config" }
]
} }

View File

@ -7,6 +7,7 @@
"module": "ESNext", "module": "ESNext",
"moduleResolution": "bundler" "moduleResolution": "bundler"
}, },
"include": ["src/**/*", "env.d.ts"], "include": ["src*", "env.d.ts"],
"exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"] "exclude": ["node_modules", "dist", "tests", "**/*.test.tsx", "**/*.test.ts", "**/*.spec.ts"],
"references": [{ "path": "../database" }, { "path": "../vitest-config" }]
} }

View File

@ -5,6 +5,15 @@
"outDir": "dist", "outDir": "dist",
"rootDir": "src" "rootDir": "src"
}, },
"include": ["src/**/*", "env.d.ts"], "include": ["src*", "env.d.ts"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"],
"references": [
{ "path": "../access-controls" },
{ "path": "../data-source" },
{ "path": "../database" },
{ "path": "../server-shared" },
{ "path": "../stored-values" },
{ "path": "../test-utils" },
{ "path": "../vitest-config" }
]
} }

View File

@ -13,5 +13,6 @@
} }
}, },
"include": ["src/**/*", "env.d.ts"], "include": ["src/**/*", "env.d.ts"],
"exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"] "exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"],
"references": [{ "path": "../vitest-config" }]
} }

View File

@ -34,8 +34,6 @@ function validateEnvironment(): string {
} }
return connectionString; return connectionString;
return connectionString;
} }
// Initialize the database pool // Initialize the database pool

View File

@ -6,5 +6,6 @@
"rootDir": "src" "rootDir": "src"
}, },
"include": ["src/**/*", "env.d.ts"], "include": ["src/**/*", "env.d.ts"],
"exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"] "exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"],
"references": [{ "path": "../vitest-config" }]
} }

View File

@ -5,6 +5,7 @@
"outDir": "dist", "outDir": "dist",
"rootDir": "src" "rootDir": "src"
}, },
"include": ["src/**/*", "env.d.ts"], "include": ["src*", "env.d.ts"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] "exclude": ["node_modules", "dist", "**/*.test.tsx", "**/*.test.ts", "**/*.spec.ts"],
"references": [{ "path": "../vitest-config" }]
} }

View File

@ -207,12 +207,12 @@ describe('ReasoningMessageSchema', () => {
expect(result.data.type).toBe('files'); expect(result.data.type).toBe('files');
if (result.data.type === 'files') { if (result.data.type === 'files') {
expect(result.data.file_ids).toHaveLength(2); expect(result.data.file_ids).toHaveLength(2);
expect(result.data.files['file-1'].file_type).toBe('metric'); expect(result.data.files['file-1']?.file_type).toBe('metric');
expect(result.data.files['file-1'].file.modified).toEqual([ expect(result.data.files['file-1']?.file?.modified).toEqual([
[0, 10], [0, 10],
[20, 30], [20, 30],
]); ]);
expect(result.data.files['file-2'].file.modified).toBeUndefined(); expect(result.data.files['file-2']?.file?.modified).toBeUndefined();
} }
} }
}); });
@ -258,9 +258,9 @@ describe('ReasoningMessageSchema', () => {
expect(result.data.type).toBe('pills'); expect(result.data.type).toBe('pills');
if (result.data.type === 'pills') { if (result.data.type === 'pills') {
expect(result.data.pill_containers).toHaveLength(2); expect(result.data.pill_containers).toHaveLength(2);
expect(result.data.pill_containers[0].pills).toHaveLength(2); expect(result.data.pill_containers?.[0]?.pills).toHaveLength(2);
expect(result.data.pill_containers[0].pills[0].type).toBe('metric'); expect(result.data.pill_containers?.[0]?.pills?.[0]?.type).toBe('metric');
expect(result.data.pill_containers[1].pills[0].text).toBe('Sales Overview'); expect(result.data.pill_containers?.[1]?.pills?.[0]?.text).toBe('Sales Overview');
} }
} }
}); });

View File

@ -193,8 +193,8 @@ describe('chartConfigProps', () => {
}); });
// Check that defaults are applied to column settings // Check that defaults are applied to column settings
expect(result.columnSettings.revenue.lineWidth).toBe(2); expect(result.columnSettings.revenue?.lineWidth).toBe(2);
expect(result.columnSettings.profit.columnVisualization).toBe('bar'); expect(result.columnSettings.profit?.columnVisualization).toBe('bar');
expect(result.columnLabelFormats.revenue).toMatchObject({ expect(result.columnLabelFormats.revenue).toMatchObject({
prefix: '$', prefix: '$',

View File

@ -312,7 +312,10 @@ describe('Helper function edge cases', () => {
it('should handle null and undefined defaults correctly', () => { it('should handle null and undefined defaults correctly', () => {
const NullDefaultsSchema = z.object({ const NullDefaultsSchema = z.object({
nullable: z.string().nullable().default(null), nullable: z.string().nullable().default(null),
optional: z.string().optional().default(undefined), optional: z
.string()
.optional()
.default(undefined as any),
emptyString: z.string().default(''), emptyString: z.string().default(''),
zero: z.number().default(0), zero: z.number().default(0),
false: z.boolean().default(false), false: z.boolean().default(false),

View File

@ -191,8 +191,8 @@ describe('ShareConfigSchema', () => {
if (result.success) { if (result.success) {
expect(result.data.individual_permissions).toHaveLength(2); expect(result.data.individual_permissions).toHaveLength(2);
expect(result.data.individual_permissions?.[0].email).toBe('user1@example.com'); expect(result.data.individual_permissions?.[0]?.email).toBe('user1@example.com');
expect(result.data.individual_permissions?.[0].role).toBe('canEdit'); expect(result.data.individual_permissions?.[0]?.role).toBe('canEdit');
expect(result.data.publicly_accessible).toBe(true); expect(result.data.publicly_accessible).toBe(true);
expect(result.data.permission).toBe('owner'); expect(result.data.permission).toBe('owner');
} }
@ -369,11 +369,11 @@ describe('ShareConfigSchema', () => {
if (result.success) { if (result.success) {
expect(result.data.individual_permissions).toHaveLength(4); expect(result.data.individual_permissions).toHaveLength(4);
expect(result.data.individual_permissions?.[0].role).toBe('owner'); expect(result.data.individual_permissions?.[0]?.role).toBe('owner');
expect(result.data.individual_permissions?.[1].role).toBe('canEdit'); expect(result.data.individual_permissions?.[1]?.role).toBe('canEdit');
expect(result.data.individual_permissions?.[2].role).toBe('canView'); expect(result.data.individual_permissions?.[2]?.role).toBe('canView');
expect(result.data.individual_permissions?.[3].role).toBe('canFilter'); expect(result.data.individual_permissions?.[3]?.role).toBe('canFilter');
expect(result.data.individual_permissions?.[2].name).toBeUndefined(); expect(result.data.individual_permissions?.[2]?.name).toBeUndefined();
} }
}); });

View File

@ -6,8 +6,6 @@
"tsBuildInfoFile": "dist/.cache/tsbuildinfo.json" "tsBuildInfoFile": "dist/.cache/tsbuildinfo.json"
}, },
"include": ["src"], "include": ["src"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"], "exclude": ["node_modules", "dist", "**/*.test.tsx", "**/*.test.ts", "**/*.spec.ts"],
"references": [ "references": [{ "path": "../database" }, { "path": "../vitest-config" }]
{ "path": "../database" } // Reference other projects
]
} }

View File

@ -6,5 +6,6 @@
"rootDir": "src" "rootDir": "src"
}, },
"include": ["src/**/*", "env.d.ts"], "include": ["src/**/*", "env.d.ts"],
"exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"] "exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"],
"references": [{ "path": "../vitest-config" }]
} }

View File

@ -5,6 +5,7 @@
"outDir": "dist", "outDir": "dist",
"rootDir": "src" "rootDir": "src"
}, },
"include": ["src/**/*", "env.d.ts"], "include": ["src*", "env.d.ts"],
"exclude": ["node_modules", "dist", "tests", "**/*.test.ts", "**/*.spec.ts"] "exclude": ["node_modules", "dist", "tests", "**/*.test.tsx", "**/*.test.ts", "**/*.spec.ts"],
"references": [{ "path": "../database" }, { "path": "../vitest-config" }]
} }

View File

@ -1,4 +1,4 @@
{ {
"extends": "../typescript-config/base.json", "extends": "../typescript-config/base.json",
"include": ["volumes/**/*"] "include": ["volumes*"]
} }

View File

@ -4,6 +4,7 @@
"tsBuildInfoFile": "dist/.cache/tsbuildinfo.json", "tsBuildInfoFile": "dist/.cache/tsbuildinfo.json",
"outDir": "dist" "outDir": "dist"
}, },
"include": ["src/**/*", "env.d.ts"], "include": ["src*", "env.d.ts"],
"exclude": ["node_modules", "dist"] "exclude": ["node_modules", "dist"],
"references": [{ "path": "../database" }, { "path": "../vitest-config" }]
} }

View File

@ -3,8 +3,9 @@
"declaration": true, "declaration": true,
"declarationMap": true, "declarationMap": true,
"esModuleInterop": true, "esModuleInterop": true,
"incremental": false, "incremental": true,
"isolatedModules": true, "isolatedModules": true,
"composite": true,
"lib": ["es2022", "DOM", "DOM.Iterable"], "lib": ["es2022", "DOM", "DOM.Iterable"],
"module": "ESNext", "module": "ESNext",
"moduleDetection": "force", "moduleDetection": "force",

View File

@ -10,5 +10,6 @@
} }
}, },
"include": ["src/**/*"], "include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"] "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"],
"references": [{ "path": "../vitest-config" }]
} }