From ff54bc246da97a31bd9046e2fde45bebe02eea19 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 5 Feb 2026 23:51:34 +0000 Subject: [PATCH 1/2] test: add tests for resolveFromProjectSearch to increase coverage - Export resolveFromProjectSearch and ResolvedEventTarget type for testing - Add 8 new tests for resolveFromProjectSearch: - No projects found: throws ContextError with project name - Multiple projects found: throws ValidationError with org list - Single project found: returns resolved target with org/project - Use spyOn to mock findProjectsBySlug without complex module mocking - Tests cover all 3 branches of resolveFromProjectSearch (empty, multiple, single) --- src/commands/event/view.ts | 20 ++++- test/commands/event/view.test.ts | 143 ++++++++++++++++++++++++++++++- 2 files changed, 156 insertions(+), 7 deletions(-) diff --git a/src/commands/event/view.ts b/src/commands/event/view.ts index f04af33c..5c7c0e7f 100644 --- a/src/commands/event/view.ts +++ b/src/commands/event/view.ts @@ -93,8 +93,11 @@ export function parsePositionalArgs(args: string[]): { return { eventId: second, targetArg: first }; } -/** Resolved target type for internal use */ -type ResolvedEventTarget = { +/** + * Resolved target type for event commands. + * @internal Exported for testing + */ +export type ResolvedEventTarget = { org: string; project: string; orgDisplay: string; @@ -104,8 +107,19 @@ type ResolvedEventTarget = { /** * Resolve target from a project search result. + * + * Searches for a project by slug across all accessible organizations. + * Throws if no project found or if multiple projects found in different orgs. + * + * @param projectSlug - Project slug to search for + * @param eventId - Event ID (used in error messages) + * @returns Resolved target with org and project info + * @throws {ContextError} If no project found + * @throws {ValidationError} If project exists in multiple organizations + * + * @internal Exported for testing */ -async function resolveFromProjectSearch( +export async function resolveFromProjectSearch( projectSlug: string, eventId: string ): Promise { diff --git a/test/commands/event/view.test.ts b/test/commands/event/view.test.ts index d86e49c4..efef09d7 100644 --- a/test/commands/event/view.test.ts +++ b/test/commands/event/view.test.ts @@ -1,12 +1,19 @@ /** * Event View Command Tests * - * Tests for positional argument parsing in src/commands/event/view.ts + * Tests for positional argument parsing and project resolution + * in src/commands/event/view.ts */ -import { describe, expect, test } from "bun:test"; -import { parsePositionalArgs } from "../../../src/commands/event/view.js"; -import { ContextError } from "../../../src/lib/errors.js"; +import { beforeEach, describe, expect, spyOn, test } from "bun:test"; +import { + parsePositionalArgs, + resolveFromProjectSearch, +} from "../../../src/commands/event/view.js"; +import type { ProjectWithOrg } from "../../../src/lib/api-client.js"; +// biome-ignore lint/performance/noNamespaceImport: needed for spyOn mocking +import * as apiClient from "../../../src/lib/api-client.js"; +import { ContextError, ValidationError } from "../../../src/lib/errors.js"; describe("parsePositionalArgs", () => { describe("single argument (event ID only)", () => { @@ -85,3 +92,131 @@ describe("parsePositionalArgs", () => { }); }); }); + +describe("resolveFromProjectSearch", () => { + let findProjectsBySlugSpy: ReturnType; + + beforeEach(() => { + findProjectsBySlugSpy = spyOn(apiClient, "findProjectsBySlug"); + }); + + describe("no projects found", () => { + test("throws ContextError when project not found", async () => { + findProjectsBySlugSpy.mockResolvedValue([]); + + await expect( + resolveFromProjectSearch("my-project", "event-123") + ).rejects.toThrow(ContextError); + }); + + test("includes project name in error message", async () => { + findProjectsBySlugSpy.mockResolvedValue([]); + + try { + await resolveFromProjectSearch("frontend", "event-123"); + expect.unreachable("Should have thrown"); + } catch (error) { + expect(error).toBeInstanceOf(ContextError); + expect((error as ContextError).message).toContain('Project "frontend"'); + expect((error as ContextError).message).toContain( + "Check that you have access" + ); + } + }); + }); + + describe("multiple projects found", () => { + test("throws ValidationError when project exists in multiple orgs", async () => { + findProjectsBySlugSpy.mockResolvedValue([ + { slug: "frontend", orgSlug: "org-a", id: "1", name: "Frontend" }, + { slug: "frontend", orgSlug: "org-b", id: "2", name: "Frontend" }, + ] as ProjectWithOrg[]); + + await expect( + resolveFromProjectSearch("frontend", "event-123") + ).rejects.toThrow(ValidationError); + }); + + test("includes all orgs in error message", async () => { + findProjectsBySlugSpy.mockResolvedValue([ + { slug: "frontend", orgSlug: "acme-corp", id: "1", name: "Frontend" }, + { slug: "frontend", orgSlug: "beta-inc", id: "2", name: "Frontend" }, + ] as ProjectWithOrg[]); + + try { + await resolveFromProjectSearch("frontend", "event-456"); + expect.unreachable("Should have thrown"); + } catch (error) { + expect(error).toBeInstanceOf(ValidationError); + const message = (error as ValidationError).message; + expect(message).toContain("exists in multiple organizations"); + expect(message).toContain("acme-corp/frontend"); + expect(message).toContain("beta-inc/frontend"); + expect(message).toContain("event-456"); // Event ID in example + } + }); + + test("includes usage example in error message", async () => { + findProjectsBySlugSpy.mockResolvedValue([ + { slug: "api", orgSlug: "org-1", id: "1", name: "API" }, + { slug: "api", orgSlug: "org-2", id: "2", name: "API" }, + { slug: "api", orgSlug: "org-3", id: "3", name: "API" }, + ] as ProjectWithOrg[]); + + try { + await resolveFromProjectSearch("api", "abc123"); + expect.unreachable("Should have thrown"); + } catch (error) { + expect(error).toBeInstanceOf(ValidationError); + const message = (error as ValidationError).message; + expect(message).toContain( + "Example: sentry event view /api abc123" + ); + } + }); + }); + + describe("single project found", () => { + test("returns resolved target for single match", async () => { + findProjectsBySlugSpy.mockResolvedValue([ + { slug: "backend", orgSlug: "my-company", id: "42", name: "Backend" }, + ] as ProjectWithOrg[]); + + const result = await resolveFromProjectSearch("backend", "event-xyz"); + + expect(result).toEqual({ + org: "my-company", + project: "backend", + orgDisplay: "my-company", + projectDisplay: "backend", + }); + }); + + test("uses orgSlug from project result", async () => { + findProjectsBySlugSpy.mockResolvedValue([ + { + slug: "mobile-app", + orgSlug: "acme-industries", + id: "100", + name: "Mobile App", + }, + ] as ProjectWithOrg[]); + + const result = await resolveFromProjectSearch("mobile-app", "evt-001"); + + expect(result.org).toBe("acme-industries"); + expect(result.orgDisplay).toBe("acme-industries"); + }); + + test("preserves project slug in result", async () => { + findProjectsBySlugSpy.mockResolvedValue([ + { slug: "web-frontend", orgSlug: "org", id: "1", name: "Web Frontend" }, + ] as ProjectWithOrg[]); + + const result = await resolveFromProjectSearch("web-frontend", "e123"); + + expect(result.project).toBe("web-frontend"); + expect(result.projectDisplay).toBe("web-frontend"); + }); + }); +}); From c8b0f3d7e303a88b0cbd9ae042c13d8db88ee4c8 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 5 Feb 2026 23:59:52 +0000 Subject: [PATCH 2/2] fix: add afterEach cleanup to restore spyOn mock Prevents test isolation issues where the spy on findProjectsBySlug could persist and affect other test files running in the same process. --- test/commands/event/view.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/commands/event/view.test.ts b/test/commands/event/view.test.ts index efef09d7..2ce6a362 100644 --- a/test/commands/event/view.test.ts +++ b/test/commands/event/view.test.ts @@ -5,7 +5,7 @@ * in src/commands/event/view.ts */ -import { beforeEach, describe, expect, spyOn, test } from "bun:test"; +import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"; import { parsePositionalArgs, resolveFromProjectSearch, @@ -100,6 +100,10 @@ describe("resolveFromProjectSearch", () => { findProjectsBySlugSpy = spyOn(apiClient, "findProjectsBySlug"); }); + afterEach(() => { + findProjectsBySlugSpy.mockRestore(); + }); + describe("no projects found", () => { test("throws ContextError when project not found", async () => { findProjectsBySlugSpy.mockResolvedValue([]);