@@ -31,6 +31,9 @@ const sampleProject: SentryProject = {
3131 dateCreated : "2026-02-12T10:00:00Z" ,
3232} ;
3333
34+ /** Default flags for non-dry-run, non-JSON, confirmed deletion */
35+ const defaultFlags = { yes : true , "dry-run" : false , json : false } ;
36+
3437function createMockContext ( ) {
3538 const stdoutWrite = mock ( ( ) => true ) ;
3639 const stderrWrite = mock ( ( ) => true ) ;
@@ -76,7 +79,7 @@ describe("project delete", () => {
7679 test ( "deletes project with explicit org/project and --yes" , async ( ) => {
7780 const { context, stdoutWrite } = createMockContext ( ) ;
7881 const func = await deleteCommand . loader ( ) ;
79- await func . call ( context , { yes : true , json : false } , "acme-corp/my-app" ) ;
82+ await func . call ( context , defaultFlags , "acme-corp/my-app" ) ;
8083
8184 expect ( getProjectSpy ) . toHaveBeenCalledWith ( "acme-corp" , "my-app" ) ;
8285 expect ( deleteProjectSpy ) . toHaveBeenCalledWith ( "acme-corp" , "my-app" ) ;
@@ -89,7 +92,7 @@ describe("project delete", () => {
8992 test ( "deletes project with bare slug and --yes" , async ( ) => {
9093 const { context } = createMockContext ( ) ;
9194 const func = await deleteCommand . loader ( ) ;
92- await func . call ( context , { yes : true , json : false } , "my-app" ) ;
95+ await func . call ( context , defaultFlags , "my-app" ) ;
9396
9497 expect ( resolveProjectBySlugSpy ) . toHaveBeenCalledWith (
9598 "my-app" ,
@@ -104,7 +107,7 @@ describe("project delete", () => {
104107 const func = await deleteCommand . loader ( ) ;
105108
106109 await expect (
107- func . call ( context , { yes : true , json : false } , "acme-corp/" )
110+ func . call ( context , defaultFlags , "acme-corp/" )
108111 ) . rejects . toThrow ( ContextError ) ;
109112
110113 expect ( deleteProjectSpy ) . not . toHaveBeenCalled ( ) ;
@@ -116,7 +119,7 @@ describe("project delete", () => {
116119
117120 // isatty(0) returns false in test environments (non-TTY)
118121 await expect (
119- func . call ( context , { yes : false , json : false } , "acme-corp/my-app" )
122+ func . call ( context , { ... defaultFlags , yes : false } , "acme-corp/my-app" )
120123 ) . rejects . toThrow ( "non-interactive mode" ) ;
121124
122125 expect ( deleteProjectSpy ) . not . toHaveBeenCalled ( ) ;
@@ -125,7 +128,11 @@ describe("project delete", () => {
125128 test ( "outputs JSON when --json flag is set" , async ( ) => {
126129 const { context, stdoutWrite } = createMockContext ( ) ;
127130 const func = await deleteCommand . loader ( ) ;
128- await func . call ( context , { yes : true , json : true } , "acme-corp/my-app" ) ;
131+ await func . call (
132+ context ,
133+ { ...defaultFlags , json : true } ,
134+ "acme-corp/my-app"
135+ ) ;
129136
130137 const output = stdoutWrite . mock . calls . map ( ( c ) => c [ 0 ] ) . join ( "" ) ;
131138 const parsed = JSON . parse ( output . trim ( ) ) ;
@@ -145,33 +152,79 @@ describe("project delete", () => {
145152 const func = await deleteCommand . loader ( ) ;
146153
147154 await expect (
148- func . call ( context , { yes : true , json : false } , "acme-corp/my-app" )
155+ func . call ( context , defaultFlags , "acme-corp/my-app" )
149156 ) . rejects . toThrow ( ApiError ) ;
150157
151158 expect ( deleteProjectSpy ) . not . toHaveBeenCalled ( ) ;
152159 } ) ;
153160
154- test ( "shows actionable message on 403 from deleteProject" , async ( ) => {
161+ test ( "shows actionable ApiError on 403 from deleteProject" , async ( ) => {
155162 deleteProjectSpy . mockRejectedValue (
156163 new ApiError ( "Forbidden" , 403 , "You do not have permission" )
157164 ) ;
158165
159166 const { context } = createMockContext ( ) ;
160167 const func = await deleteCommand . loader ( ) ;
161168
162- await expect (
163- func . call ( context , { yes : true , json : false } , "acme-corp/my-app" )
164- ) . rejects . toThrow ( "project:admin" ) ;
169+ try {
170+ await func . call ( context , defaultFlags , "acme-corp/my-app" ) ;
171+ expect . unreachable ( "should have thrown" ) ;
172+ } catch ( error ) {
173+ expect ( error ) . toBeInstanceOf ( ApiError ) ;
174+ const apiErr = error as ApiError ;
175+ expect ( apiErr . status ) . toBe ( 403 ) ;
176+ expect ( apiErr . message ) . toContain ( "project:admin" ) ;
177+ expect ( apiErr . message ) . toContain ( "sentry auth login" ) ;
178+ }
165179 } ) ;
166180
167181 test ( "verifies project exists before attempting delete" , async ( ) => {
168182 const { context } = createMockContext ( ) ;
169183 const func = await deleteCommand . loader ( ) ;
170- await func . call ( context , { yes : true , json : false } , "acme-corp/my-app" ) ;
184+ await func . call ( context , defaultFlags , "acme-corp/my-app" ) ;
171185
172186 // getProject must be called before deleteProject
173187 const getProjectOrder = getProjectSpy . mock . invocationCallOrder [ 0 ] ;
174188 const deleteProjectOrder = deleteProjectSpy . mock . invocationCallOrder [ 0 ] ;
175189 expect ( getProjectOrder ) . toBeLessThan ( deleteProjectOrder ?? 0 ) ;
176190 } ) ;
191+
192+ // Dry-run tests
193+
194+ test ( "dry-run shows what would be deleted without calling deleteProject" , async ( ) => {
195+ const { context, stdoutWrite } = createMockContext ( ) ;
196+ const func = await deleteCommand . loader ( ) ;
197+ await func . call (
198+ context ,
199+ { ...defaultFlags , "dry-run" : true } ,
200+ "acme-corp/my-app"
201+ ) ;
202+
203+ expect ( getProjectSpy ) . toHaveBeenCalledWith ( "acme-corp" , "my-app" ) ;
204+ expect ( deleteProjectSpy ) . not . toHaveBeenCalled ( ) ;
205+
206+ const output = stdoutWrite . mock . calls . map ( ( c ) => c [ 0 ] ) . join ( "" ) ;
207+ expect ( output ) . toContain ( "Would delete project 'My App'" ) ;
208+ expect ( output ) . toContain ( "acme-corp/my-app" ) ;
209+ } ) ;
210+
211+ test ( "dry-run outputs JSON when --json is also set" , async ( ) => {
212+ const { context, stdoutWrite } = createMockContext ( ) ;
213+ const func = await deleteCommand . loader ( ) ;
214+ await func . call (
215+ context ,
216+ { ...defaultFlags , "dry-run" : true , json : true } ,
217+ "acme-corp/my-app"
218+ ) ;
219+
220+ const output = stdoutWrite . mock . calls . map ( ( c ) => c [ 0 ] ) . join ( "" ) ;
221+ const parsed = JSON . parse ( output . trim ( ) ) ;
222+ expect ( parsed . dryRun ) . toBe ( true ) ;
223+ expect ( parsed . org ) . toBe ( "acme-corp" ) ;
224+ expect ( parsed . project ) . toBe ( "my-app" ) ;
225+ expect ( parsed . name ) . toBe ( "My App" ) ;
226+ expect ( parsed . url ) . toContain ( "acme-corp" ) ;
227+
228+ expect ( deleteProjectSpy ) . not . toHaveBeenCalled ( ) ;
229+ } ) ;
177230} ) ;
0 commit comments