Skip to content

Commit e207be9

Browse files
committed
test(issue): add fast-path coverage for shortid fan-out
Exercise the three distinct outcomes of the new shortid fast path in resolveProjectSearch(): - ambiguous: shortid resolves in multiple orgs → ResolutionError - all 403: every org returns forbidden → surfaces real ApiError(403) - all 500: every org returns server error → surfaces real ApiError(500) The existing success test (line 419) already exercises the fast path via its shortid mock. These new tests cover the error classification logic that was previously untested.
1 parent 681c715 commit e207be9

File tree

1 file changed

+196
-1
lines changed

1 file changed

+196
-1
lines changed

test/commands/issue/utils.test.ts

Lines changed: 196 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { DEFAULT_SENTRY_URL } from "../../../src/lib/constants.js";
1616
import { setAuthToken } from "../../../src/lib/db/auth.js";
1717
import { setCachedProject } from "../../../src/lib/db/project-cache.js";
1818
import { setOrgRegion } from "../../../src/lib/db/regions.js";
19-
import { ResolutionError } from "../../../src/lib/errors.js";
19+
import { ApiError, ResolutionError } from "../../../src/lib/errors.js";
2020
import { useTestConfigDir } from "../../helpers.js";
2121

2222
describe("buildCommandHint", () => {
@@ -707,6 +707,201 @@ describe("resolveOrgAndIssueId", () => {
707707
})
708708
).rejects.toThrow("500");
709709
});
710+
711+
test("fast path: ambiguous when shortid resolves in multiple orgs", async () => {
712+
const { clearProjectAliases } = await import(
713+
"../../../src/lib/db/project-aliases.js"
714+
);
715+
await clearProjectAliases();
716+
717+
await setOrgRegion("org2", DEFAULT_SENTRY_URL);
718+
719+
const makeShortIdResponse = (orgSlug: string, groupId: string) =>
720+
new Response(
721+
JSON.stringify({
722+
organizationSlug: orgSlug,
723+
projectSlug: "shared",
724+
groupId,
725+
group: {
726+
id: groupId,
727+
shortId: "SHARED-G",
728+
title: "Test Issue",
729+
status: "unresolved",
730+
platform: "javascript",
731+
type: "error",
732+
count: "1",
733+
userCount: 1,
734+
},
735+
}),
736+
{
737+
status: 200,
738+
headers: { "Content-Type": "application/json" },
739+
}
740+
);
741+
742+
// @ts-expect-error - partial mock
743+
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
744+
const req = new Request(input, init);
745+
const url = req.url;
746+
747+
if (url.includes("/users/me/regions/")) {
748+
return new Response(JSON.stringify({ regions: [] }), {
749+
status: 200,
750+
headers: { "Content-Type": "application/json" },
751+
});
752+
}
753+
754+
if (
755+
url.includes("/organizations/") &&
756+
!url.includes("/projects/") &&
757+
!url.includes("/issues/") &&
758+
!url.includes("/shortids/")
759+
) {
760+
return new Response(
761+
JSON.stringify([
762+
{ id: "1", slug: "org1", name: "Org 1" },
763+
{ id: "2", slug: "org2", name: "Org 2" },
764+
]),
765+
{
766+
status: 200,
767+
headers: { "Content-Type": "application/json" },
768+
}
769+
);
770+
}
771+
772+
// Both orgs resolve the shortid — triggers fast-path ambiguity
773+
if (url.includes("organizations/org1/shortids/SHARED-G")) {
774+
return makeShortIdResponse("org1", "111");
775+
}
776+
if (url.includes("organizations/org2/shortids/SHARED-G")) {
777+
return makeShortIdResponse("org2", "222");
778+
}
779+
780+
return new Response(JSON.stringify({ detail: "Not found" }), {
781+
status: 404,
782+
});
783+
};
784+
785+
await expect(
786+
resolveOrgAndIssueId({
787+
issueArg: "shared-g",
788+
cwd: getConfigDir(),
789+
command: "explain",
790+
})
791+
).rejects.toThrow("is ambiguous");
792+
});
793+
794+
test("fast path: surfaces 403 when all orgs return forbidden", async () => {
795+
const { clearProjectAliases } = await import(
796+
"../../../src/lib/db/project-aliases.js"
797+
);
798+
await clearProjectAliases();
799+
800+
// @ts-expect-error - partial mock
801+
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
802+
const req = new Request(input, init);
803+
const url = req.url;
804+
805+
if (url.includes("/users/me/regions/")) {
806+
return new Response(JSON.stringify({ regions: [] }), {
807+
status: 200,
808+
headers: { "Content-Type": "application/json" },
809+
});
810+
}
811+
812+
if (
813+
url.includes("/organizations/") &&
814+
!url.includes("/projects/") &&
815+
!url.includes("/issues/") &&
816+
!url.includes("/shortids/")
817+
) {
818+
return new Response(
819+
JSON.stringify([{ id: "1", slug: "my-org", name: "My Org" }]),
820+
{
821+
status: 200,
822+
headers: { "Content-Type": "application/json" },
823+
}
824+
);
825+
}
826+
827+
// Shortid endpoint returns 403 for all orgs
828+
if (url.includes("/shortids/")) {
829+
return new Response(
830+
JSON.stringify({ detail: "You do not have permission" }),
831+
{ status: 403 }
832+
);
833+
}
834+
835+
return new Response(JSON.stringify({ detail: "Not found" }), {
836+
status: 404,
837+
});
838+
};
839+
840+
const err = await resolveOrgAndIssueId({
841+
issueArg: "restricted-g",
842+
cwd: getConfigDir(),
843+
command: "explain",
844+
}).catch((e: unknown) => e);
845+
846+
expect(err).toBeInstanceOf(ApiError);
847+
expect((err as ApiError).status).toBe(403);
848+
});
849+
850+
test("fast path: surfaces 500 when all orgs return server error", async () => {
851+
const { clearProjectAliases } = await import(
852+
"../../../src/lib/db/project-aliases.js"
853+
);
854+
await clearProjectAliases();
855+
856+
// @ts-expect-error - partial mock
857+
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
858+
const req = new Request(input, init);
859+
const url = req.url;
860+
861+
if (url.includes("/users/me/regions/")) {
862+
return new Response(JSON.stringify({ regions: [] }), {
863+
status: 200,
864+
headers: { "Content-Type": "application/json" },
865+
});
866+
}
867+
868+
if (
869+
url.includes("/organizations/") &&
870+
!url.includes("/projects/") &&
871+
!url.includes("/issues/") &&
872+
!url.includes("/shortids/")
873+
) {
874+
return new Response(
875+
JSON.stringify([{ id: "1", slug: "my-org", name: "My Org" }]),
876+
{
877+
status: 200,
878+
headers: { "Content-Type": "application/json" },
879+
}
880+
);
881+
}
882+
883+
// Shortid endpoint returns 500 for all orgs
884+
if (url.includes("/shortids/")) {
885+
return new Response(
886+
JSON.stringify({ detail: "Internal Server Error" }),
887+
{ status: 500 }
888+
);
889+
}
890+
891+
return new Response(JSON.stringify({ detail: "Not found" }), {
892+
status: 404,
893+
});
894+
};
895+
896+
const err = await resolveOrgAndIssueId({
897+
issueArg: "broken-g",
898+
cwd: getConfigDir(),
899+
command: "explain",
900+
}).catch((e: unknown) => e);
901+
902+
expect(err).toBeInstanceOf(ApiError);
903+
expect((err as ApiError).status).toBe(500);
904+
});
710905
});
711906

712907
describe("pollAutofixState", () => {

0 commit comments

Comments
 (0)