From 7a6f3b058742dc064e53dc5fc565ec07f515e0d1 Mon Sep 17 00:00:00 2001 From: Cascade Bot Date: Mon, 16 Mar 2026 18:50:23 +0000 Subject: [PATCH 1/2] test(config): add integration tests for config provider round-trip --- tests/integration/config-provider.test.ts | 549 ++++++++++++++++++++++ 1 file changed, 549 insertions(+) create mode 100644 tests/integration/config-provider.test.ts diff --git a/tests/integration/config-provider.test.ts b/tests/integration/config-provider.test.ts new file mode 100644 index 00000000..6a47c8ca --- /dev/null +++ b/tests/integration/config-provider.test.ts @@ -0,0 +1,549 @@ +/** + * Integration tests for configRepository.ts lookup functions. + * + * Tests the full JSONB sub-query round-trip and WithConfig lookups against a + * real PostgreSQL database: + * - findProjectByBoardIdFromDb — JSONB sub-query on project_integrations.config->>'boardId' + * - findProjectByJiraProjectKeyFromDb — JSONB sub-query on project_integrations.config->>'projectKey' + * - findProjectByRepoFromDb — simple column lookup on projects.repo + * - findProjectByIdFromDb — primary key lookup + * - findProjectWithConfigByBoardId — { project, config } pair + * - findProjectWithConfigByRepo — { project, config } pair + * - findProjectWithConfigById — { project, config } pair + * - findProjectWithConfigByJiraProjectKey — { project, config } pair + * - loadConfigFromDb — full config load, validated via validateConfig() + */ + +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { invalidateConfigCache } from '../../src/config/provider.js'; +import { CascadeConfigSchema, validateConfig } from '../../src/config/schema.js'; +import { + findProjectByBoardIdFromDb, + findProjectByIdFromDb, + findProjectByJiraProjectKeyFromDb, + findProjectByRepoFromDb, + findProjectWithConfigByBoardId, + findProjectWithConfigById, + findProjectWithConfigByJiraProjectKey, + findProjectWithConfigByRepo, + loadConfigFromDb, +} from '../../src/db/repositories/configRepository.js'; +import { truncateAll } from './helpers/db.js'; +import { seedIntegration, seedOrg, seedProject } from './helpers/seed.js'; + +beforeAll(async () => { + await truncateAll(); +}); + +describe('Config Provider (integration)', () => { + beforeEach(async () => { + await truncateAll(); + invalidateConfigCache(); + await seedOrg(); + }); + + // ========================================================================= + // findProjectByBoardIdFromDb — JSONB sub-query + // ========================================================================= + + describe('findProjectByBoardIdFromDb', () => { + it('returns the project for a known boardId', async () => { + await seedProject({ id: 'proj-trello', repo: 'owner/trello-repo' }); + await seedIntegration({ + projectId: 'proj-trello', + category: 'pm', + provider: 'trello', + config: { boardId: 'board-abc', lists: {}, labels: {} }, + }); + + const project = await findProjectByBoardIdFromDb('board-abc'); + + expect(project).toBeDefined(); + expect(project?.id).toBe('proj-trello'); + }); + + it('returns undefined for an unknown boardId', async () => { + await seedProject({ id: 'proj-trello', repo: 'owner/trello-repo' }); + await seedIntegration({ + projectId: 'proj-trello', + category: 'pm', + provider: 'trello', + config: { boardId: 'board-abc', lists: {}, labels: {} }, + }); + + const project = await findProjectByBoardIdFromDb('board-nonexistent'); + + expect(project).toBeUndefined(); + }); + + it('returns correct project when multiple projects exist', async () => { + await seedProject({ id: 'proj-a', repo: 'owner/repo-a' }); + await seedProject({ id: 'proj-b', repo: 'owner/repo-b' }); + + await seedIntegration({ + projectId: 'proj-a', + category: 'pm', + provider: 'trello', + config: { boardId: 'board-111', lists: {}, labels: {} }, + }); + await seedIntegration({ + projectId: 'proj-b', + category: 'pm', + provider: 'trello', + config: { boardId: 'board-222', lists: {}, labels: {} }, + }); + + const projectA = await findProjectByBoardIdFromDb('board-111'); + const projectB = await findProjectByBoardIdFromDb('board-222'); + + expect(projectA?.id).toBe('proj-a'); + expect(projectB?.id).toBe('proj-b'); + }); + }); + + // ========================================================================= + // findProjectByJiraProjectKeyFromDb — JSONB sub-query + // ========================================================================= + + describe('findProjectByJiraProjectKeyFromDb', () => { + it('returns the project for a known JIRA projectKey', async () => { + await seedProject({ id: 'proj-jira', repo: 'owner/jira-repo' }); + await seedIntegration({ + projectId: 'proj-jira', + category: 'pm', + provider: 'jira', + config: { + baseUrl: 'https://test.atlassian.net', + projectKey: 'MYPROJ', + statuses: {}, + }, + }); + + const project = await findProjectByJiraProjectKeyFromDb('MYPROJ'); + + expect(project).toBeDefined(); + expect(project?.id).toBe('proj-jira'); + }); + + it('returns undefined for an unknown JIRA projectKey', async () => { + await seedProject({ id: 'proj-jira', repo: 'owner/jira-repo' }); + await seedIntegration({ + projectId: 'proj-jira', + category: 'pm', + provider: 'jira', + config: { + baseUrl: 'https://test.atlassian.net', + projectKey: 'MYPROJ', + statuses: {}, + }, + }); + + const project = await findProjectByJiraProjectKeyFromDb('UNKNOWN'); + + expect(project).toBeUndefined(); + }); + + it('returns correct project when multiple projects exist with different JIRA keys', async () => { + await seedProject({ id: 'proj-jira-1', repo: 'owner/repo-j1' }); + await seedProject({ id: 'proj-jira-2', repo: 'owner/repo-j2' }); + + await seedIntegration({ + projectId: 'proj-jira-1', + category: 'pm', + provider: 'jira', + config: { + baseUrl: 'https://test.atlassian.net', + projectKey: 'ALPHA', + statuses: {}, + }, + }); + await seedIntegration({ + projectId: 'proj-jira-2', + category: 'pm', + provider: 'jira', + config: { + baseUrl: 'https://test.atlassian.net', + projectKey: 'BETA', + statuses: {}, + }, + }); + + const proj1 = await findProjectByJiraProjectKeyFromDb('ALPHA'); + const proj2 = await findProjectByJiraProjectKeyFromDb('BETA'); + + expect(proj1?.id).toBe('proj-jira-1'); + expect(proj2?.id).toBe('proj-jira-2'); + }); + }); + + // ========================================================================= + // findProjectByRepoFromDb — simple column lookup + // ========================================================================= + + describe('findProjectByRepoFromDb', () => { + it('returns the project for a known repo', async () => { + await seedProject({ id: 'proj-repo', repo: 'myorg/myrepo' }); + + const project = await findProjectByRepoFromDb('myorg/myrepo'); + + expect(project).toBeDefined(); + expect(project?.id).toBe('proj-repo'); + }); + + it('returns undefined for an unknown repo', async () => { + await seedProject({ id: 'proj-repo', repo: 'myorg/myrepo' }); + + const project = await findProjectByRepoFromDb('myorg/nonexistent'); + + expect(project).toBeUndefined(); + }); + + it('returns correct project when multiple projects exist with different repos', async () => { + await seedProject({ id: 'proj-x', repo: 'org/repo-x' }); + await seedProject({ id: 'proj-y', repo: 'org/repo-y' }); + + const projX = await findProjectByRepoFromDb('org/repo-x'); + const projY = await findProjectByRepoFromDb('org/repo-y'); + + expect(projX?.id).toBe('proj-x'); + expect(projY?.id).toBe('proj-y'); + }); + }); + + // ========================================================================= + // findProjectByIdFromDb — primary key lookup + // ========================================================================= + + describe('findProjectByIdFromDb', () => { + it('returns the project for a known id', async () => { + await seedProject({ id: 'proj-known', repo: 'owner/repo-known' }); + + const project = await findProjectByIdFromDb('proj-known'); + + expect(project).toBeDefined(); + expect(project?.id).toBe('proj-known'); + }); + + it('returns undefined for an unknown id', async () => { + await seedProject({ id: 'proj-known', repo: 'owner/repo-known' }); + + const project = await findProjectByIdFromDb('proj-nonexistent'); + + expect(project).toBeUndefined(); + }); + }); + + // ========================================================================= + // WithConfig variants — return { project, config } pair + // ========================================================================= + + describe('findProjectWithConfigByBoardId', () => { + it('returns { project, config } pair for a known boardId', async () => { + await seedProject({ id: 'proj-wc-trello', repo: 'owner/wc-trello' }); + await seedIntegration({ + projectId: 'proj-wc-trello', + category: 'pm', + provider: 'trello', + config: { boardId: 'board-wc', lists: {}, labels: {} }, + }); + + const result = await findProjectWithConfigByBoardId('board-wc'); + + expect(result).toBeDefined(); + expect(result?.project.id).toBe('proj-wc-trello'); + expect(result?.config.projects).toHaveLength(1); + expect(result?.config.projects[0].id).toBe('proj-wc-trello'); + }); + + it('returns undefined for an unknown boardId', async () => { + await seedProject({ id: 'proj-wc-trello', repo: 'owner/wc-trello' }); + + const result = await findProjectWithConfigByBoardId('board-missing'); + + expect(result).toBeUndefined(); + }); + + it('project is a valid ProjectConfig', async () => { + await seedProject({ id: 'proj-valid', repo: 'owner/valid-repo' }); + await seedIntegration({ + projectId: 'proj-valid', + category: 'pm', + provider: 'trello', + config: { boardId: 'board-valid', lists: {}, labels: {} }, + }); + + const result = await findProjectWithConfigByBoardId('board-valid'); + + expect(result).toBeDefined(); + // project must have required fields + expect(result?.project.id).toBe('proj-valid'); + expect(result?.project.orgId).toBe('test-org'); + expect(result?.project.name).toBe('Test Project'); + expect(result?.project.repo).toBe('owner/valid-repo'); + expect(result?.project.baseBranch).toBeDefined(); + }); + + it('config is a valid CascadeConfig containing the project', async () => { + await seedProject({ id: 'proj-cfg', repo: 'owner/cfg-repo' }); + await seedIntegration({ + projectId: 'proj-cfg', + category: 'pm', + provider: 'trello', + config: { boardId: 'board-cfg', lists: {}, labels: {} }, + }); + + const result = await findProjectWithConfigByBoardId('board-cfg'); + + expect(result).toBeDefined(); + // config must pass schema validation + const parsed = CascadeConfigSchema.safeParse(result?.config); + expect(parsed.success).toBe(true); + }); + }); + + describe('findProjectWithConfigByRepo', () => { + it('returns { project, config } pair for a known repo', async () => { + await seedProject({ id: 'proj-wc-repo', repo: 'owner/wc-repo' }); + + const result = await findProjectWithConfigByRepo('owner/wc-repo'); + + expect(result).toBeDefined(); + expect(result?.project.id).toBe('proj-wc-repo'); + expect(result?.config.projects).toHaveLength(1); + }); + + it('returns undefined for an unknown repo', async () => { + const result = await findProjectWithConfigByRepo('owner/nonexistent'); + + expect(result).toBeUndefined(); + }); + }); + + describe('findProjectWithConfigById', () => { + it('returns { project, config } pair for a known id', async () => { + await seedProject({ id: 'proj-wc-id', repo: 'owner/wc-id-repo' }); + + const result = await findProjectWithConfigById('proj-wc-id'); + + expect(result).toBeDefined(); + expect(result?.project.id).toBe('proj-wc-id'); + expect(result?.config.projects).toHaveLength(1); + }); + + it('returns undefined for an unknown id', async () => { + const result = await findProjectWithConfigById('proj-missing'); + + expect(result).toBeUndefined(); + }); + }); + + describe('findProjectWithConfigByJiraProjectKey', () => { + it('returns { project, config } pair for a known JIRA projectKey', async () => { + await seedProject({ id: 'proj-wc-jira', repo: 'owner/wc-jira-repo' }); + await seedIntegration({ + projectId: 'proj-wc-jira', + category: 'pm', + provider: 'jira', + config: { + baseUrl: 'https://test.atlassian.net', + projectKey: 'WCJIRA', + statuses: {}, + }, + }); + + const result = await findProjectWithConfigByJiraProjectKey('WCJIRA'); + + expect(result).toBeDefined(); + expect(result?.project.id).toBe('proj-wc-jira'); + expect(result?.config.projects).toHaveLength(1); + }); + + it('returns undefined for an unknown JIRA projectKey', async () => { + const result = await findProjectWithConfigByJiraProjectKey('NOTFOUND'); + + expect(result).toBeUndefined(); + }); + + it('project is a valid ProjectConfig and config is a valid CascadeConfig', async () => { + await seedProject({ id: 'proj-jira-valid', repo: 'owner/jira-valid-repo' }); + await seedIntegration({ + projectId: 'proj-jira-valid', + category: 'pm', + provider: 'jira', + config: { + baseUrl: 'https://test.atlassian.net', + projectKey: 'VALID', + statuses: {}, + }, + }); + + const result = await findProjectWithConfigByJiraProjectKey('VALID'); + + expect(result).toBeDefined(); + expect(result?.project.id).toBe('proj-jira-valid'); + + const parsed = CascadeConfigSchema.safeParse(result?.config); + expect(parsed.success).toBe(true); + }); + }); + + // ========================================================================= + // Multi-project correctness + // ========================================================================= + + describe('Multi-project correctness', () => { + it('returns the correct project when Trello and JIRA projects coexist', async () => { + await seedProject({ id: 'proj-multi-trello', repo: 'owner/multi-trello' }); + await seedProject({ id: 'proj-multi-jira', repo: 'owner/multi-jira' }); + + await seedIntegration({ + projectId: 'proj-multi-trello', + category: 'pm', + provider: 'trello', + config: { boardId: 'board-multi', lists: {}, labels: {} }, + }); + await seedIntegration({ + projectId: 'proj-multi-jira', + category: 'pm', + provider: 'jira', + config: { + baseUrl: 'https://test.atlassian.net', + projectKey: 'MULTI', + statuses: {}, + }, + }); + + const trelloProject = await findProjectByBoardIdFromDb('board-multi'); + const jiraProject = await findProjectByJiraProjectKeyFromDb('MULTI'); + + expect(trelloProject?.id).toBe('proj-multi-trello'); + expect(jiraProject?.id).toBe('proj-multi-jira'); + }); + + it('boardId lookup does not match JIRA project with same value in config', async () => { + // Ensures provider filter in the sub-query is correct + await seedProject({ id: 'proj-jira-only', repo: 'owner/jira-only' }); + await seedIntegration({ + projectId: 'proj-jira-only', + category: 'pm', + provider: 'jira', + config: { + baseUrl: 'https://test.atlassian.net', + projectKey: 'BOARD123', // same value as what we'd search as boardId + statuses: {}, + }, + }); + + // Searching as boardId should not find the JIRA project + const result = await findProjectByBoardIdFromDb('BOARD123'); + expect(result).toBeUndefined(); + }); + }); + + // ========================================================================= + // loadConfigFromDb — full config load + // ========================================================================= + + describe('loadConfigFromDb', () => { + it('loads and validates config for a single project', async () => { + await seedProject({ id: 'proj-load', repo: 'owner/load-repo' }); + await seedIntegration({ + projectId: 'proj-load', + category: 'pm', + provider: 'trello', + config: { boardId: 'board-load', lists: {}, labels: {} }, + }); + + const config = await loadConfigFromDb(); + + expect(config.projects).toHaveLength(1); + expect(config.projects[0].id).toBe('proj-load'); + }); + + it('passes validateConfig() schema validation', async () => { + await seedProject({ id: 'proj-validate', repo: 'owner/validate-repo' }); + + const config = await loadConfigFromDb(); + + // Must not throw + expect(() => validateConfig(config)).not.toThrow(); + + // Must also pass safeParse + const parsed = CascadeConfigSchema.safeParse(config); + expect(parsed.success).toBe(true); + }); + + it('loads all projects when multiple exist', async () => { + await seedProject({ id: 'proj-load-1', repo: 'owner/load-repo-1' }); + await seedProject({ id: 'proj-load-2', repo: 'owner/load-repo-2' }); + await seedProject({ id: 'proj-load-3', repo: 'owner/load-repo-3' }); + + const config = await loadConfigFromDb(); + + expect(config.projects).toHaveLength(3); + const ids = config.projects.map((p) => p.id).sort(); + expect(ids).toEqual(['proj-load-1', 'proj-load-2', 'proj-load-3']); + }); + + it('includes trello config in project when Trello integration exists', async () => { + await seedProject({ id: 'proj-with-trello', repo: 'owner/trello-full' }); + await seedIntegration({ + projectId: 'proj-with-trello', + category: 'pm', + provider: 'trello', + config: { boardId: 'board-full', lists: { todo: 'list-1' }, labels: { bug: 'label-1' } }, + }); + + const config = await loadConfigFromDb(); + const project = config.projects.find((p) => p.id === 'proj-with-trello'); + + expect(project).toBeDefined(); + expect(project?.trello?.boardId).toBe('board-full'); + }); + + it('includes jira config in project when JIRA integration exists', async () => { + await seedProject({ id: 'proj-with-jira', repo: 'owner/jira-full' }); + await seedIntegration({ + projectId: 'proj-with-jira', + category: 'pm', + provider: 'jira', + config: { + baseUrl: 'https://test.atlassian.net', + projectKey: 'FULL', + statuses: { todo: 'To Do' }, + }, + }); + + const config = await loadConfigFromDb(); + const project = config.projects.find((p) => p.id === 'proj-with-jira'); + + expect(project).toBeDefined(); + expect(project?.jira?.projectKey).toBe('FULL'); + }); + }); + + // ========================================================================= + // Cache invalidation + // ========================================================================= + + describe('invalidateConfigCache (via provider layer)', () => { + it('invalidateConfigCache() clears the cache so fresh DB reads happen', async () => { + await seedProject({ id: 'proj-cache-test', repo: 'owner/cache-repo' }); + await seedIntegration({ + projectId: 'proj-cache-test', + category: 'pm', + provider: 'trello', + config: { boardId: 'board-cache', lists: {}, labels: {} }, + }); + + // First lookup via DB + const first = await findProjectByBoardIdFromDb('board-cache'); + expect(first?.id).toBe('proj-cache-test'); + + // Invalidate and re-lookup — should still work + invalidateConfigCache(); + const second = await findProjectByBoardIdFromDb('board-cache'); + expect(second?.id).toBe('proj-cache-test'); + }); + }); +}); From 34455f0764ab3640922a14281577546578dfcaeb Mon Sep 17 00:00:00 2001 From: Cascade Bot Date: Mon, 16 Mar 2026 19:03:51 +0000 Subject: [PATCH 2/2] refactor(tests): fix config provider tests per review feedback - Rewrite config-provider.test.ts to focus on the actual provider layer (cached findProjectByBoardId, findProjectByRepo, findProjectByJiraProjectKey, loadConfig from src/config/provider.ts) rather than duplicating direct DB repository tests - Fix cache invalidation test to use cached provider functions, mutate the DB between calls, and verify invalidateConfigCache() causes a fresh read - Move genuinely new test coverage into configRepository.test.ts: WithConfig variants for repo/id/jira, CascadeConfigSchema.safeParse validation, cross-provider isolation, Trello+JIRA coexistence, and JIRA config loading Co-Authored-By: Claude Opus 4.6 --- tests/integration/config-provider.test.ts | 531 +++++------------- tests/integration/db/configRepository.test.ts | 185 ++++++ 2 files changed, 311 insertions(+), 405 deletions(-) diff --git a/tests/integration/config-provider.test.ts b/tests/integration/config-provider.test.ts index 6a47c8ca..a753825a 100644 --- a/tests/integration/config-provider.test.ts +++ b/tests/integration/config-provider.test.ts @@ -1,33 +1,26 @@ /** - * Integration tests for configRepository.ts lookup functions. + * Integration tests for the config provider layer (src/config/provider.ts). * - * Tests the full JSONB sub-query round-trip and WithConfig lookups against a - * real PostgreSQL database: - * - findProjectByBoardIdFromDb — JSONB sub-query on project_integrations.config->>'boardId' - * - findProjectByJiraProjectKeyFromDb — JSONB sub-query on project_integrations.config->>'projectKey' - * - findProjectByRepoFromDb — simple column lookup on projects.repo - * - findProjectByIdFromDb — primary key lookup - * - findProjectWithConfigByBoardId — { project, config } pair - * - findProjectWithConfigByRepo — { project, config } pair - * - findProjectWithConfigById — { project, config } pair - * - findProjectWithConfigByJiraProjectKey — { project, config } pair - * - loadConfigFromDb — full config load, validated via validateConfig() + * Tests the cached lookup functions against a real PostgreSQL database, + * verifying that: + * - Cached provider functions (findProjectByBoardId, findProjectByRepo, + * findProjectByJiraProjectKey, loadConfig) serve results from the cache + * on subsequent calls. + * - invalidateConfigCache() forces a fresh DB read on the next call. + * - After cache invalidation + DB mutation, the provider returns the + * updated result rather than the stale cached value. */ import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; -import { invalidateConfigCache } from '../../src/config/provider.js'; -import { CascadeConfigSchema, validateConfig } from '../../src/config/schema.js'; import { - findProjectByBoardIdFromDb, - findProjectByIdFromDb, - findProjectByJiraProjectKeyFromDb, - findProjectByRepoFromDb, - findProjectWithConfigByBoardId, - findProjectWithConfigById, - findProjectWithConfigByJiraProjectKey, - findProjectWithConfigByRepo, - loadConfigFromDb, -} from '../../src/db/repositories/configRepository.js'; + findProjectByBoardId, + findProjectByJiraProjectKey, + findProjectByRepo, + invalidateConfigCache, + loadConfig, +} from '../../src/config/provider.js'; +import { getDb } from '../../src/db/client.js'; +import { projectIntegrations } from '../../src/db/schema/index.js'; import { truncateAll } from './helpers/db.js'; import { seedIntegration, seedOrg, seedProject } from './helpers/seed.js'; @@ -35,7 +28,7 @@ beforeAll(async () => { await truncateAll(); }); -describe('Config Provider (integration)', () => { +describe('Config Provider — cached lookups (integration)', () => { beforeEach(async () => { await truncateAll(); invalidateConfigCache(); @@ -43,20 +36,20 @@ describe('Config Provider (integration)', () => { }); // ========================================================================= - // findProjectByBoardIdFromDb — JSONB sub-query + // findProjectByBoardId — cached provider function // ========================================================================= - describe('findProjectByBoardIdFromDb', () => { + describe('findProjectByBoardId', () => { it('returns the project for a known boardId', async () => { await seedProject({ id: 'proj-trello', repo: 'owner/trello-repo' }); await seedIntegration({ projectId: 'proj-trello', category: 'pm', provider: 'trello', - config: { boardId: 'board-abc', lists: {}, labels: {} }, + config: { boardId: 'board-cached', lists: {}, labels: {} }, }); - const project = await findProjectByBoardIdFromDb('board-abc'); + const project = await findProjectByBoardId('board-cached'); expect(project).toBeDefined(); expect(project?.id).toBe('proj-trello'); @@ -68,482 +61,210 @@ describe('Config Provider (integration)', () => { projectId: 'proj-trello', category: 'pm', provider: 'trello', - config: { boardId: 'board-abc', lists: {}, labels: {} }, + config: { boardId: 'board-cached', lists: {}, labels: {} }, }); - const project = await findProjectByBoardIdFromDb('board-nonexistent'); + const project = await findProjectByBoardId('board-nonexistent'); expect(project).toBeUndefined(); }); - it('returns correct project when multiple projects exist', async () => { - await seedProject({ id: 'proj-a', repo: 'owner/repo-a' }); - await seedProject({ id: 'proj-b', repo: 'owner/repo-b' }); - + it('returns a cached result on second call without invalidation', async () => { + await seedProject({ id: 'proj-cache-hit', repo: 'owner/cache-repo' }); await seedIntegration({ - projectId: 'proj-a', + projectId: 'proj-cache-hit', category: 'pm', provider: 'trello', - config: { boardId: 'board-111', lists: {}, labels: {} }, - }); - await seedIntegration({ - projectId: 'proj-b', - category: 'pm', - provider: 'trello', - config: { boardId: 'board-222', lists: {}, labels: {} }, + config: { boardId: 'board-for-cache', lists: {}, labels: {} }, }); - const projectA = await findProjectByBoardIdFromDb('board-111'); - const projectB = await findProjectByBoardIdFromDb('board-222'); + // First call — populates cache + const first = await findProjectByBoardId('board-for-cache'); + expect(first?.id).toBe('proj-cache-hit'); - expect(projectA?.id).toBe('proj-a'); - expect(projectB?.id).toBe('proj-b'); - }); - }); + // Mutate DB directly — delete the integration + const db = getDb(); + await db.delete(projectIntegrations); - // ========================================================================= - // findProjectByJiraProjectKeyFromDb — JSONB sub-query - // ========================================================================= - - describe('findProjectByJiraProjectKeyFromDb', () => { - it('returns the project for a known JIRA projectKey', async () => { - await seedProject({ id: 'proj-jira', repo: 'owner/jira-repo' }); - await seedIntegration({ - projectId: 'proj-jira', - category: 'pm', - provider: 'jira', - config: { - baseUrl: 'https://test.atlassian.net', - projectKey: 'MYPROJ', - statuses: {}, - }, - }); - - const project = await findProjectByJiraProjectKeyFromDb('MYPROJ'); - - expect(project).toBeDefined(); - expect(project?.id).toBe('proj-jira'); + // Second call — should still return cached result, not hit DB + const second = await findProjectByBoardId('board-for-cache'); + expect(second?.id).toBe('proj-cache-hit'); }); - it('returns undefined for an unknown JIRA projectKey', async () => { - await seedProject({ id: 'proj-jira', repo: 'owner/jira-repo' }); + it('returns fresh DB result after invalidateConfigCache()', async () => { + await seedProject({ id: 'proj-invalidate', repo: 'owner/invalidate-repo' }); await seedIntegration({ - projectId: 'proj-jira', + projectId: 'proj-invalidate', category: 'pm', - provider: 'jira', - config: { - baseUrl: 'https://test.atlassian.net', - projectKey: 'MYPROJ', - statuses: {}, - }, + provider: 'trello', + config: { boardId: 'board-invalidate', lists: {}, labels: {} }, }); - const project = await findProjectByJiraProjectKeyFromDb('UNKNOWN'); - - expect(project).toBeUndefined(); - }); - - it('returns correct project when multiple projects exist with different JIRA keys', async () => { - await seedProject({ id: 'proj-jira-1', repo: 'owner/repo-j1' }); - await seedProject({ id: 'proj-jira-2', repo: 'owner/repo-j2' }); - - await seedIntegration({ - projectId: 'proj-jira-1', - category: 'pm', - provider: 'jira', - config: { - baseUrl: 'https://test.atlassian.net', - projectKey: 'ALPHA', - statuses: {}, - }, - }); - await seedIntegration({ - projectId: 'proj-jira-2', - category: 'pm', - provider: 'jira', - config: { - baseUrl: 'https://test.atlassian.net', - projectKey: 'BETA', - statuses: {}, - }, - }); + // First call — populates cache + const before = await findProjectByBoardId('board-invalidate'); + expect(before?.id).toBe('proj-invalidate'); - const proj1 = await findProjectByJiraProjectKeyFromDb('ALPHA'); - const proj2 = await findProjectByJiraProjectKeyFromDb('BETA'); + // Mutate DB: remove the integration so the boardId no longer exists + const db = getDb(); + await db.delete(projectIntegrations); - expect(proj1?.id).toBe('proj-jira-1'); - expect(proj2?.id).toBe('proj-jira-2'); + // Invalidate cache then re-query — must reflect the DB mutation + invalidateConfigCache(); + const after = await findProjectByBoardId('board-invalidate'); + expect(after).toBeUndefined(); }); }); // ========================================================================= - // findProjectByRepoFromDb — simple column lookup + // findProjectByRepo — cached provider function // ========================================================================= - describe('findProjectByRepoFromDb', () => { + describe('findProjectByRepo', () => { it('returns the project for a known repo', async () => { await seedProject({ id: 'proj-repo', repo: 'myorg/myrepo' }); - const project = await findProjectByRepoFromDb('myorg/myrepo'); + const project = await findProjectByRepo('myorg/myrepo'); expect(project).toBeDefined(); expect(project?.id).toBe('proj-repo'); }); it('returns undefined for an unknown repo', async () => { - await seedProject({ id: 'proj-repo', repo: 'myorg/myrepo' }); - - const project = await findProjectByRepoFromDb('myorg/nonexistent'); + const project = await findProjectByRepo('myorg/nonexistent'); expect(project).toBeUndefined(); }); - it('returns correct project when multiple projects exist with different repos', async () => { - await seedProject({ id: 'proj-x', repo: 'org/repo-x' }); - await seedProject({ id: 'proj-y', repo: 'org/repo-y' }); + it('returns fresh DB result after invalidateConfigCache()', async () => { + await seedProject({ id: 'proj-repo-invalidate', repo: 'org/repo-to-delete' }); - const projX = await findProjectByRepoFromDb('org/repo-x'); - const projY = await findProjectByRepoFromDb('org/repo-y'); + // Populate cache + const before = await findProjectByRepo('org/repo-to-delete'); + expect(before?.id).toBe('proj-repo-invalidate'); - expect(projX?.id).toBe('proj-x'); - expect(projY?.id).toBe('proj-y'); - }); - }); + // Delete the project from the DB + const db = getDb(); + await db.execute(`DELETE FROM projects WHERE id = 'proj-repo-invalidate'`); - // ========================================================================= - // findProjectByIdFromDb — primary key lookup - // ========================================================================= - - describe('findProjectByIdFromDb', () => { - it('returns the project for a known id', async () => { - await seedProject({ id: 'proj-known', repo: 'owner/repo-known' }); - - const project = await findProjectByIdFromDb('proj-known'); - - expect(project).toBeDefined(); - expect(project?.id).toBe('proj-known'); - }); - - it('returns undefined for an unknown id', async () => { - await seedProject({ id: 'proj-known', repo: 'owner/repo-known' }); - - const project = await findProjectByIdFromDb('proj-nonexistent'); + // Without invalidation, cache still serves the old result + const stale = await findProjectByRepo('org/repo-to-delete'); + expect(stale?.id).toBe('proj-repo-invalidate'); - expect(project).toBeUndefined(); + // After invalidation, fresh DB read reflects the deletion + invalidateConfigCache(); + const fresh = await findProjectByRepo('org/repo-to-delete'); + expect(fresh).toBeUndefined(); }); }); // ========================================================================= - // WithConfig variants — return { project, config } pair + // findProjectByJiraProjectKey — cached provider function // ========================================================================= - describe('findProjectWithConfigByBoardId', () => { - it('returns { project, config } pair for a known boardId', async () => { - await seedProject({ id: 'proj-wc-trello', repo: 'owner/wc-trello' }); - await seedIntegration({ - projectId: 'proj-wc-trello', - category: 'pm', - provider: 'trello', - config: { boardId: 'board-wc', lists: {}, labels: {} }, - }); - - const result = await findProjectWithConfigByBoardId('board-wc'); - - expect(result).toBeDefined(); - expect(result?.project.id).toBe('proj-wc-trello'); - expect(result?.config.projects).toHaveLength(1); - expect(result?.config.projects[0].id).toBe('proj-wc-trello'); - }); - - it('returns undefined for an unknown boardId', async () => { - await seedProject({ id: 'proj-wc-trello', repo: 'owner/wc-trello' }); - - const result = await findProjectWithConfigByBoardId('board-missing'); - - expect(result).toBeUndefined(); - }); - - it('project is a valid ProjectConfig', async () => { - await seedProject({ id: 'proj-valid', repo: 'owner/valid-repo' }); - await seedIntegration({ - projectId: 'proj-valid', - category: 'pm', - provider: 'trello', - config: { boardId: 'board-valid', lists: {}, labels: {} }, - }); - - const result = await findProjectWithConfigByBoardId('board-valid'); - - expect(result).toBeDefined(); - // project must have required fields - expect(result?.project.id).toBe('proj-valid'); - expect(result?.project.orgId).toBe('test-org'); - expect(result?.project.name).toBe('Test Project'); - expect(result?.project.repo).toBe('owner/valid-repo'); - expect(result?.project.baseBranch).toBeDefined(); - }); - - it('config is a valid CascadeConfig containing the project', async () => { - await seedProject({ id: 'proj-cfg', repo: 'owner/cfg-repo' }); - await seedIntegration({ - projectId: 'proj-cfg', - category: 'pm', - provider: 'trello', - config: { boardId: 'board-cfg', lists: {}, labels: {} }, - }); - - const result = await findProjectWithConfigByBoardId('board-cfg'); - - expect(result).toBeDefined(); - // config must pass schema validation - const parsed = CascadeConfigSchema.safeParse(result?.config); - expect(parsed.success).toBe(true); - }); - }); - - describe('findProjectWithConfigByRepo', () => { - it('returns { project, config } pair for a known repo', async () => { - await seedProject({ id: 'proj-wc-repo', repo: 'owner/wc-repo' }); - - const result = await findProjectWithConfigByRepo('owner/wc-repo'); - - expect(result).toBeDefined(); - expect(result?.project.id).toBe('proj-wc-repo'); - expect(result?.config.projects).toHaveLength(1); - }); - - it('returns undefined for an unknown repo', async () => { - const result = await findProjectWithConfigByRepo('owner/nonexistent'); - - expect(result).toBeUndefined(); - }); - }); - - describe('findProjectWithConfigById', () => { - it('returns { project, config } pair for a known id', async () => { - await seedProject({ id: 'proj-wc-id', repo: 'owner/wc-id-repo' }); - - const result = await findProjectWithConfigById('proj-wc-id'); - - expect(result).toBeDefined(); - expect(result?.project.id).toBe('proj-wc-id'); - expect(result?.config.projects).toHaveLength(1); - }); - - it('returns undefined for an unknown id', async () => { - const result = await findProjectWithConfigById('proj-missing'); - - expect(result).toBeUndefined(); - }); - }); - - describe('findProjectWithConfigByJiraProjectKey', () => { - it('returns { project, config } pair for a known JIRA projectKey', async () => { - await seedProject({ id: 'proj-wc-jira', repo: 'owner/wc-jira-repo' }); + describe('findProjectByJiraProjectKey', () => { + it('returns the project for a known JIRA projectKey', async () => { + await seedProject({ id: 'proj-jira', repo: 'owner/jira-repo' }); await seedIntegration({ - projectId: 'proj-wc-jira', + projectId: 'proj-jira', category: 'pm', provider: 'jira', config: { baseUrl: 'https://test.atlassian.net', - projectKey: 'WCJIRA', + projectKey: 'MYPROJ', statuses: {}, }, }); - const result = await findProjectWithConfigByJiraProjectKey('WCJIRA'); + const project = await findProjectByJiraProjectKey('MYPROJ'); - expect(result).toBeDefined(); - expect(result?.project.id).toBe('proj-wc-jira'); - expect(result?.config.projects).toHaveLength(1); + expect(project).toBeDefined(); + expect(project?.id).toBe('proj-jira'); }); it('returns undefined for an unknown JIRA projectKey', async () => { - const result = await findProjectWithConfigByJiraProjectKey('NOTFOUND'); + const project = await findProjectByJiraProjectKey('UNKNOWN'); - expect(result).toBeUndefined(); + expect(project).toBeUndefined(); }); - it('project is a valid ProjectConfig and config is a valid CascadeConfig', async () => { - await seedProject({ id: 'proj-jira-valid', repo: 'owner/jira-valid-repo' }); + it('returns fresh DB result after invalidateConfigCache()', async () => { + await seedProject({ id: 'proj-jira-invalidate', repo: 'owner/jira-invalidate' }); await seedIntegration({ - projectId: 'proj-jira-valid', + projectId: 'proj-jira-invalidate', category: 'pm', provider: 'jira', config: { baseUrl: 'https://test.atlassian.net', - projectKey: 'VALID', + projectKey: 'INVAL', statuses: {}, }, }); - const result = await findProjectWithConfigByJiraProjectKey('VALID'); - - expect(result).toBeDefined(); - expect(result?.project.id).toBe('proj-jira-valid'); - - const parsed = CascadeConfigSchema.safeParse(result?.config); - expect(parsed.success).toBe(true); - }); - }); - - // ========================================================================= - // Multi-project correctness - // ========================================================================= - - describe('Multi-project correctness', () => { - it('returns the correct project when Trello and JIRA projects coexist', async () => { - await seedProject({ id: 'proj-multi-trello', repo: 'owner/multi-trello' }); - await seedProject({ id: 'proj-multi-jira', repo: 'owner/multi-jira' }); + // Populate cache + const before = await findProjectByJiraProjectKey('INVAL'); + expect(before?.id).toBe('proj-jira-invalidate'); - await seedIntegration({ - projectId: 'proj-multi-trello', - category: 'pm', - provider: 'trello', - config: { boardId: 'board-multi', lists: {}, labels: {} }, - }); - await seedIntegration({ - projectId: 'proj-multi-jira', - category: 'pm', - provider: 'jira', - config: { - baseUrl: 'https://test.atlassian.net', - projectKey: 'MULTI', - statuses: {}, - }, - }); + // Remove the integration from the DB + const db = getDb(); + await db.delete(projectIntegrations); - const trelloProject = await findProjectByBoardIdFromDb('board-multi'); - const jiraProject = await findProjectByJiraProjectKeyFromDb('MULTI'); - - expect(trelloProject?.id).toBe('proj-multi-trello'); - expect(jiraProject?.id).toBe('proj-multi-jira'); - }); - - it('boardId lookup does not match JIRA project with same value in config', async () => { - // Ensures provider filter in the sub-query is correct - await seedProject({ id: 'proj-jira-only', repo: 'owner/jira-only' }); - await seedIntegration({ - projectId: 'proj-jira-only', - category: 'pm', - provider: 'jira', - config: { - baseUrl: 'https://test.atlassian.net', - projectKey: 'BOARD123', // same value as what we'd search as boardId - statuses: {}, - }, - }); - - // Searching as boardId should not find the JIRA project - const result = await findProjectByBoardIdFromDb('BOARD123'); - expect(result).toBeUndefined(); + // After invalidation, fresh DB read shows the integration is gone + invalidateConfigCache(); + const fresh = await findProjectByJiraProjectKey('INVAL'); + expect(fresh).toBeUndefined(); }); }); // ========================================================================= - // loadConfigFromDb — full config load + // loadConfig — cached provider function // ========================================================================= - describe('loadConfigFromDb', () => { - it('loads and validates config for a single project', async () => { + describe('loadConfig', () => { + it('returns a valid CascadeConfig with all seeded projects', async () => { await seedProject({ id: 'proj-load', repo: 'owner/load-repo' }); - await seedIntegration({ - projectId: 'proj-load', - category: 'pm', - provider: 'trello', - config: { boardId: 'board-load', lists: {}, labels: {} }, - }); - const config = await loadConfigFromDb(); + const config = await loadConfig(); + expect(config).toBeDefined(); expect(config.projects).toHaveLength(1); expect(config.projects[0].id).toBe('proj-load'); }); - it('passes validateConfig() schema validation', async () => { - await seedProject({ id: 'proj-validate', repo: 'owner/validate-repo' }); - - const config = await loadConfigFromDb(); - - // Must not throw - expect(() => validateConfig(config)).not.toThrow(); - - // Must also pass safeParse - const parsed = CascadeConfigSchema.safeParse(config); - expect(parsed.success).toBe(true); - }); - - it('loads all projects when multiple exist', async () => { - await seedProject({ id: 'proj-load-1', repo: 'owner/load-repo-1' }); - await seedProject({ id: 'proj-load-2', repo: 'owner/load-repo-2' }); - await seedProject({ id: 'proj-load-3', repo: 'owner/load-repo-3' }); - - const config = await loadConfigFromDb(); - - expect(config.projects).toHaveLength(3); - const ids = config.projects.map((p) => p.id).sort(); - expect(ids).toEqual(['proj-load-1', 'proj-load-2', 'proj-load-3']); - }); + it('serves cached result on second call without invalidation', async () => { + await seedProject({ id: 'proj-load-cache', repo: 'owner/load-cache-repo' }); - it('includes trello config in project when Trello integration exists', async () => { - await seedProject({ id: 'proj-with-trello', repo: 'owner/trello-full' }); - await seedIntegration({ - projectId: 'proj-with-trello', - category: 'pm', - provider: 'trello', - config: { boardId: 'board-full', lists: { todo: 'list-1' }, labels: { bug: 'label-1' } }, - }); + // First call — populates cache + const first = await loadConfig(); + expect(first.projects).toHaveLength(1); - const config = await loadConfigFromDb(); - const project = config.projects.find((p) => p.id === 'proj-with-trello'); + // Seed another project directly into DB — bypasses cache + await seedProject({ id: 'proj-load-cache-2', repo: 'owner/load-cache-repo-2' }); - expect(project).toBeDefined(); - expect(project?.trello?.boardId).toBe('board-full'); + // Second call — should return cached result (1 project, not 2) + const second = await loadConfig(); + expect(second.projects).toHaveLength(1); }); - it('includes jira config in project when JIRA integration exists', async () => { - await seedProject({ id: 'proj-with-jira', repo: 'owner/jira-full' }); - await seedIntegration({ - projectId: 'proj-with-jira', - category: 'pm', - provider: 'jira', - config: { - baseUrl: 'https://test.atlassian.net', - projectKey: 'FULL', - statuses: { todo: 'To Do' }, - }, - }); - - const config = await loadConfigFromDb(); - const project = config.projects.find((p) => p.id === 'proj-with-jira'); - - expect(project).toBeDefined(); - expect(project?.jira?.projectKey).toBe('FULL'); - }); - }); + it('returns fresh DB result after invalidateConfigCache()', async () => { + await seedProject({ id: 'proj-load-inv', repo: 'owner/load-inv-repo' }); - // ========================================================================= - // Cache invalidation - // ========================================================================= + // Populate cache + const before = await loadConfig(); + expect(before.projects).toHaveLength(1); - describe('invalidateConfigCache (via provider layer)', () => { - it('invalidateConfigCache() clears the cache so fresh DB reads happen', async () => { - await seedProject({ id: 'proj-cache-test', repo: 'owner/cache-repo' }); - await seedIntegration({ - projectId: 'proj-cache-test', - category: 'pm', - provider: 'trello', - config: { boardId: 'board-cache', lists: {}, labels: {} }, - }); + // Seed a second project directly in DB + await seedProject({ id: 'proj-load-inv-2', repo: 'owner/load-inv-repo-2' }); - // First lookup via DB - const first = await findProjectByBoardIdFromDb('board-cache'); - expect(first?.id).toBe('proj-cache-test'); + // Without invalidation, cache still returns 1 project + const cached = await loadConfig(); + expect(cached.projects).toHaveLength(1); - // Invalidate and re-lookup — should still work + // After invalidation, fresh DB read sees both projects invalidateConfigCache(); - const second = await findProjectByBoardIdFromDb('board-cache'); - expect(second?.id).toBe('proj-cache-test'); + const fresh = await loadConfig(); + expect(fresh.projects).toHaveLength(2); + const ids = fresh.projects.map((p) => p.id).sort(); + expect(ids).toEqual(['proj-load-inv', 'proj-load-inv-2']); }); }); }); diff --git a/tests/integration/db/configRepository.test.ts b/tests/integration/db/configRepository.test.ts index 425d6886..bc256f8b 100644 --- a/tests/integration/db/configRepository.test.ts +++ b/tests/integration/db/configRepository.test.ts @@ -1,10 +1,14 @@ import { beforeEach, describe, expect, it } from 'vitest'; +import { CascadeConfigSchema, validateConfig } from '../../../src/config/schema.js'; import { findProjectByBoardIdFromDb, findProjectByIdFromDb, findProjectByJiraProjectKeyFromDb, findProjectByRepoFromDb, findProjectWithConfigByBoardId, + findProjectWithConfigById, + findProjectWithConfigByJiraProjectKey, + findProjectWithConfigByRepo, loadConfigFromDb, } from '../../../src/db/repositories/configRepository.js'; import { truncateAll } from '../helpers/db.js'; @@ -199,5 +203,186 @@ describe('configRepository (integration)', () => { expect(p1?.trello?.boardId).toBe('board-project-1'); expect(p2?.trello?.boardId).toBe('board-project-2'); }); + + it('returns the correct project when Trello and JIRA projects coexist', async () => { + await seedProject({ id: 'project-jira', name: 'JIRA Project', repo: 'owner/jira-repo' }); + await seedIntegration({ + category: 'pm', + provider: 'trello', + config: { boardId: 'board-mixed', lists: {}, labels: {} }, + }); + await seedIntegration({ + projectId: 'project-jira', + category: 'pm', + provider: 'jira', + config: { + baseUrl: 'https://test.atlassian.net', + projectKey: 'MIXED', + statuses: {}, + }, + }); + + const trelloProject = await findProjectByBoardIdFromDb('board-mixed'); + const jiraProject = await findProjectByJiraProjectKeyFromDb('MIXED'); + + expect(trelloProject?.id).toBe('test-project'); + expect(jiraProject?.id).toBe('project-jira'); + }); + + it('boardId lookup does not match JIRA project with same value in config', async () => { + // Ensures the provider filter in the JSONB sub-query is correct + await seedIntegration({ + category: 'pm', + provider: 'jira', + config: { + baseUrl: 'https://test.atlassian.net', + projectKey: 'BOARD123', + statuses: {}, + }, + }); + + // Searching as boardId should not find the JIRA project + const result = await findProjectByBoardIdFromDb('BOARD123'); + expect(result).toBeUndefined(); + }); + }); + + // ========================================================================= + // loadConfigFromDb — schema validation and JIRA config + // ========================================================================= + + describe('loadConfigFromDb — validation and JIRA', () => { + it('passes validateConfig() schema validation', async () => { + const config = await loadConfigFromDb(); + + // Must not throw + expect(() => validateConfig(config)).not.toThrow(); + + // Must also pass safeParse + const parsed = CascadeConfigSchema.safeParse(config); + expect(parsed.success).toBe(true); + }); + + it('includes jira config in project when JIRA integration exists', async () => { + await seedIntegration({ + category: 'pm', + provider: 'jira', + config: { + baseUrl: 'https://test.atlassian.net', + projectKey: 'FULL', + statuses: { todo: 'To Do' }, + }, + }); + + const config = await loadConfigFromDb(); + const project = config.projects[0]; + + expect(project).toBeDefined(); + expect(project?.jira?.projectKey).toBe('FULL'); + }); + }); + + // ========================================================================= + // findProjectWithConfigByRepo — { project, config } pair + // ========================================================================= + + describe('findProjectWithConfigByRepo', () => { + it('returns { project, config } pair for a known repo', async () => { + const result = await findProjectWithConfigByRepo('owner/repo'); + + expect(result).toBeDefined(); + expect(result?.project.id).toBe('test-project'); + expect(result?.config.projects).toHaveLength(1); + }); + + it('returns undefined for an unknown repo', async () => { + const result = await findProjectWithConfigByRepo('owner/nonexistent'); + + expect(result).toBeUndefined(); + }); + + it('config passes CascadeConfigSchema.safeParse()', async () => { + const result = await findProjectWithConfigByRepo('owner/repo'); + + expect(result).toBeDefined(); + const parsed = CascadeConfigSchema.safeParse(result?.config); + expect(parsed.success).toBe(true); + }); + }); + + // ========================================================================= + // findProjectWithConfigById — { project, config } pair + // ========================================================================= + + describe('findProjectWithConfigById', () => { + it('returns { project, config } pair for a known id', async () => { + const result = await findProjectWithConfigById('test-project'); + + expect(result).toBeDefined(); + expect(result?.project.id).toBe('test-project'); + expect(result?.config.projects).toHaveLength(1); + }); + + it('returns undefined for an unknown id', async () => { + const result = await findProjectWithConfigById('proj-missing'); + + expect(result).toBeUndefined(); + }); + + it('config passes CascadeConfigSchema.safeParse()', async () => { + const result = await findProjectWithConfigById('test-project'); + + expect(result).toBeDefined(); + const parsed = CascadeConfigSchema.safeParse(result?.config); + expect(parsed.success).toBe(true); + }); + }); + + // ========================================================================= + // findProjectWithConfigByJiraProjectKey — { project, config } pair + // ========================================================================= + + describe('findProjectWithConfigByJiraProjectKey', () => { + it('returns { project, config } pair for a known JIRA projectKey', async () => { + await seedIntegration({ + category: 'pm', + provider: 'jira', + config: { + baseUrl: 'https://test.atlassian.net', + projectKey: 'WCJIRA', + statuses: {}, + }, + }); + + const result = await findProjectWithConfigByJiraProjectKey('WCJIRA'); + + expect(result).toBeDefined(); + expect(result?.project.id).toBe('test-project'); + expect(result?.config.projects).toHaveLength(1); + }); + + it('returns undefined for an unknown JIRA projectKey', async () => { + const result = await findProjectWithConfigByJiraProjectKey('NOTFOUND'); + + expect(result).toBeUndefined(); + }); + + it('config passes CascadeConfigSchema.safeParse()', async () => { + await seedIntegration({ + category: 'pm', + provider: 'jira', + config: { + baseUrl: 'https://test.atlassian.net', + projectKey: 'VALID', + statuses: {}, + }, + }); + + const result = await findProjectWithConfigByJiraProjectKey('VALID'); + + expect(result).toBeDefined(); + const parsed = CascadeConfigSchema.safeParse(result?.config); + expect(parsed.success).toBe(true); + }); }); });