@@ -11,12 +11,15 @@ import { array, constantFrom, assert as fcAssert, property } from "fast-check";
1111import { DEFAULT_SENTRY_URL } from "../../src/lib/constants.js" ;
1212import { setAuthToken } from "../../src/lib/db/auth.js" ;
1313import { setOrgRegion } from "../../src/lib/db/regions.js" ;
14+ import { AuthError , ResolutionError } from "../../src/lib/errors.js" ;
1415import {
16+ fetchProjectId ,
1517 isValidDirNameForInference ,
1618 resolveAllTargets ,
1719 resolveOrg ,
1820 resolveOrgAndProject ,
1921 resolveOrgsForListing ,
22+ toNumericId ,
2023} from "../../src/lib/resolve-target.js" ;
2124import { mockFetch , useTestConfigDir } from "../helpers.js" ;
2225
@@ -104,6 +107,44 @@ describe("isValidDirNameForInference edge cases", () => {
104107 } ) ;
105108} ) ;
106109
110+ // ============================================================================
111+ // toNumericId — pure function for ID coercion
112+ // ============================================================================
113+
114+ describe ( "toNumericId" , ( ) => {
115+ test ( "returns undefined for undefined" , ( ) => {
116+ expect ( toNumericId ( undefined ) ) . toBeUndefined ( ) ;
117+ } ) ;
118+
119+ test ( "returns undefined for null (cast)" , ( ) => {
120+ expect ( toNumericId ( null as unknown as undefined ) ) . toBeUndefined ( ) ;
121+ } ) ;
122+
123+ test ( "converts string number to number" , ( ) => {
124+ expect ( toNumericId ( "123" ) ) . toBe ( 123 ) ;
125+ } ) ;
126+
127+ test ( "returns number as-is" , ( ) => {
128+ expect ( toNumericId ( 123 ) ) . toBe ( 123 ) ;
129+ } ) ;
130+
131+ test ( "returns undefined for string '0' (falsy)" , ( ) => {
132+ expect ( toNumericId ( "0" ) ) . toBeUndefined ( ) ;
133+ } ) ;
134+
135+ test ( "returns undefined for numeric 0 (falsy)" , ( ) => {
136+ expect ( toNumericId ( 0 ) ) . toBeUndefined ( ) ;
137+ } ) ;
138+
139+ test ( "returns undefined for empty string" , ( ) => {
140+ expect ( toNumericId ( "" ) ) . toBeUndefined ( ) ;
141+ } ) ;
142+
143+ test ( "returns undefined for non-numeric string" , ( ) => {
144+ expect ( toNumericId ( "abc" ) ) . toBeUndefined ( ) ;
145+ } ) ;
146+ } ) ;
147+
107148// ============================================================================
108149// Environment Variable Resolution (SENTRY_ORG / SENTRY_PROJECT)
109150//
@@ -295,3 +336,74 @@ describe("Environment variable resolution (SENTRY_ORG / SENTRY_PROJECT)", () =>
295336 expect ( result . orgs ) . toContain ( "env-org" ) ;
296337 } ) ;
297338} ) ;
339+
340+ // ============================================================================
341+ // fetchProjectId — async project ID lookup with error handling
342+ // ============================================================================
343+
344+ describe ( "fetchProjectId" , ( ) => {
345+ useTestConfigDir ( "test-fetchProjectId-" ) ;
346+
347+ let originalFetch : typeof globalThis . fetch ;
348+
349+ beforeEach ( ( ) => {
350+ originalFetch = globalThis . fetch ;
351+ } ) ;
352+
353+ afterEach ( ( ) => {
354+ globalThis . fetch = originalFetch ;
355+ } ) ;
356+
357+ test ( "returns numeric project ID on success" , async ( ) => {
358+ await setAuthToken ( "test-token" ) ;
359+ await setOrgRegion ( "test-org" , DEFAULT_SENTRY_URL ) ;
360+ globalThis . fetch = mockFetch ( async ( input , init ) => {
361+ const req = new Request ( input , init ) ;
362+ if ( req . url . includes ( "/api/0/projects/test-org/test-project/" ) ) {
363+ return Response . json ( { id : "456" , slug : "test-project" } ) ;
364+ }
365+ return new Response ( "Not found" , { status : 404 } ) ;
366+ } ) ;
367+
368+ const result = await fetchProjectId ( "test-org" , "test-project" ) ;
369+ expect ( result ) . toBe ( 456 ) ;
370+ } ) ;
371+
372+ test ( "throws ResolutionError on 404" , async ( ) => {
373+ await setAuthToken ( "test-token" ) ;
374+ await setOrgRegion ( "test-org" , DEFAULT_SENTRY_URL ) ;
375+ globalThis . fetch = mockFetch (
376+ async ( ) =>
377+ new Response ( JSON . stringify ( { detail : "Not found" } ) , {
378+ status : 404 ,
379+ } )
380+ ) ;
381+
382+ expect ( fetchProjectId ( "test-org" , "test-project" ) ) . rejects . toThrow (
383+ ResolutionError
384+ ) ;
385+ } ) ;
386+
387+ test ( "rethrows AuthError when not authenticated" , async ( ) => {
388+ // No auth token set — refreshToken() will throw AuthError
389+ await setOrgRegion ( "test-org" , DEFAULT_SENTRY_URL ) ;
390+
391+ expect ( fetchProjectId ( "test-org" , "test-project" ) ) . rejects . toThrow (
392+ AuthError
393+ ) ;
394+ } ) ;
395+
396+ test ( "returns undefined on transient server error" , async ( ) => {
397+ await setAuthToken ( "test-token" ) ;
398+ await setOrgRegion ( "test-org" , DEFAULT_SENTRY_URL ) ;
399+ globalThis . fetch = mockFetch (
400+ async ( ) =>
401+ new Response ( JSON . stringify ( { detail : "Internal error" } ) , {
402+ status : 500 ,
403+ } )
404+ ) ;
405+
406+ const result = await fetchProjectId ( "test-org" , "test-project" ) ;
407+ expect ( result ) . toBeUndefined ( ) ;
408+ } ) ;
409+ } ) ;
0 commit comments