@@ -2,10 +2,20 @@ import Validate from './validate.js'
22import { linkedAppContext } from '../../../services/app-context.js'
33import { validateApp } from '../../../services/validate.js'
44import { testAppLinked } from '../../../models/app/app.test-data.js'
5+ import { AppConfigurationAbortError } from '../../../models/app/error-parsing.js'
56import { describe , expect , test , vi } from 'vitest'
7+ import { AbortError } from '@shopify/cli-kit/node/error'
8+ import { outputResult } from '@shopify/cli-kit/node/output'
69
710vi . mock ( '../../../services/app-context.js' )
811vi . mock ( '../../../services/validate.js' )
12+ vi . mock ( '@shopify/cli-kit/node/output' , async ( importOriginal ) => {
13+ const actual = await importOriginal < typeof import ( '@shopify/cli-kit/node/output' ) > ( )
14+ return {
15+ ...actual ,
16+ outputResult : vi . fn ( ) ,
17+ }
18+ } )
919
1020describe ( 'app config validate command' , ( ) => {
1121 test ( 'calls validateApp with json: false by default' , async ( ) => {
@@ -46,4 +56,151 @@ describe('app config validate command', () => {
4656 // Then
4757 expect ( validateApp ) . toHaveBeenCalledWith ( app , { json : true } )
4858 } )
59+
60+ test ( 'rethrows AppConfigurationAbortError in non-json mode without emitting json' , async ( ) => {
61+ // Given
62+ vi . mocked ( linkedAppContext ) . mockRejectedValue (
63+ new AppConfigurationAbortError ( 'Validation errors in /tmp/shopify.app.toml' , '/tmp/shopify.app.toml' ) ,
64+ )
65+
66+ // When / Then
67+ await expect ( Validate . run ( [ '--path=/tmp/app' ] , import . meta. url ) ) . rejects . toThrow ( )
68+ expect ( outputResult ) . not . toHaveBeenCalled ( )
69+ expect ( validateApp ) . not . toHaveBeenCalled( )
70+ } )
71+
72+ test ( 'outputs structured configuration issues from app loading before validateApp runs' , async ( ) => {
73+ // Given
74+ vi . mocked ( linkedAppContext ) . mockRejectedValue (
75+ new AppConfigurationAbortError (
76+ 'Validation errors in /tmp/shopify.app.toml:\n\n• [name]: String is required' ,
77+ '/tmp/shopify.app.toml' ,
78+ [
79+ {
80+ filePath : '/tmp/shopify.app.toml' ,
81+ path : [ 'name' ] ,
82+ pathString : 'name' ,
83+ message : 'String is required' ,
84+ } ,
85+ ] ,
86+ ) ,
87+ )
88+
89+ // When / Then
90+ await expect ( Validate . run ( [ '--json' , '--path=/tmp/app' ] , import . meta. url ) ) . rejects . toThrow (
91+ 'process.exit unexpectedly called with "1"' ,
92+ )
93+ expect ( outputResult ) . toHaveBeenCalledTimes ( 1 )
94+ expect ( outputResult ) . toHaveBeenCalledWith (
95+ JSON . stringify (
96+ {
97+ valid : false ,
98+ issues : [
99+ {
100+ filePath : '/tmp/shopify.app.toml' ,
101+ path : [ 'name' ] ,
102+ pathString : 'name' ,
103+ message : 'String is required' ,
104+ } ,
105+ ] ,
106+ } ,
107+ null,
108+ 2 ,
109+ ) ,
110+ )
111+ expect ( validateApp ) . not . toHaveBeenCalled( )
112+ } )
113+
114+ test ( 'outputs a root json issue when app loading fails without structured issues ', async ( ) => {
115+ // Given
116+ vi . mocked ( linkedAppContext ) . mockRejectedValue (
117+ new AppConfigurationAbortError ( "Couldn 't find an app toml file at / tmp / app ", '/ tmp / app ') ,
118+ )
119+
120+ // When / Then
121+ await expect ( Validate . run ( [ '-- json ', '-- path = / t m p / app'], import.meta.url)).rejects.toThrow(
122+ ' process . exit unexpectedly called with "1" ',
123+ )
124+ expect ( outputResult ) . toHaveBeenCalledTimes ( 1 )
125+ expect ( outputResult ) . toHaveBeenCalledWith (
126+ JSON . stringify (
127+ {
128+ valid : false ,
129+ issues : [
130+ {
131+ filePath : '/tmp/app' ,
132+ path : [ ] ,
133+ pathString : 'root' ,
134+ message : "Couldn't find an app toml file at /tmp/app" ,
135+ } ,
136+ ] ,
137+ } ,
138+ null ,
139+ 2 ,
140+ ) ,
141+ )
142+ expect ( validateApp ) . not . toHaveBeenCalled ( )
143+ } )
144+
145+ test ( 'outputs json when validateApp throws a structured configuration abort' , async ( ) => {
146+ // Given
147+ const app = testAppLinked ( )
148+ vi . mocked ( linkedAppContext ) . mockResolvedValue ( { app} as Awaited < ReturnType < typeof linkedAppContext > > )
149+ vi . mocked ( validateApp ) . mockRejectedValue (
150+ new AppConfigurationAbortError (
151+ 'Validation errors in /tmp/shopify.app.toml:\n\n• [name]: String is required' ,
152+ '/tmp/shopify.app.toml' ,
153+ [
154+ {
155+ filePath : '/tmp/shopify.app.toml' ,
156+ path : [ 'name' ] ,
157+ pathString : 'name' ,
158+ message : 'String is required' ,
159+ } ,
160+ ] ,
161+ ) ,
162+ )
163+
164+ // When / Then
165+ await expect ( Validate . run ( [ '--json' ] , import . meta. url ) ) . rejects . toThrow ( 'process.exit unexpectedly called with "1"' )
166+ expect ( outputResult ) . toHaveBeenCalledTimes ( 1 )
167+ expect ( outputResult ) . toHaveBeenCalledWith (
168+ JSON . stringify (
169+ {
170+ valid : false ,
171+ issues : [
172+ {
173+ filePath : '/tmp/shopify.app.toml' ,
174+ path : [ 'name' ] ,
175+ pathString : 'name' ,
176+ message : 'String is required' ,
177+ } ,
178+ ] ,
179+ } ,
180+ null ,
181+ 2 ,
182+ ) ,
183+ )
184+ } )
185+
186+ test ( 'rethrows non-configuration errors from validateApp in json mode without converting them to validation json' , async ( ) => {
187+ // Given
188+ const app = testAppLinked ( )
189+ vi . mocked ( linkedAppContext ) . mockResolvedValue ( { app} as Awaited < ReturnType < typeof linkedAppContext > > )
190+ vi . mocked ( validateApp ) . mockRejectedValue ( new AbortError ( 'network problem' ) )
191+
192+ // When / Then
193+ await expect ( Validate . run ( [ '--json' ] , import . meta. url ) ) . rejects . toThrow ( )
194+ expect ( outputResult ) . not . toHaveBeenCalled ( )
195+ } )
196+
197+ test ( 'rethrows unrelated abort errors in json mode without converting them to validation json' , async ( ) => {
198+ // Given
199+ vi . mocked ( linkedAppContext ) . mockRejectedValue ( new AbortError ( 'Could not find store for domain shop.example.com' ) )
200+
201+ // When / Then
202+ await expect ( Validate . run ( [ '--json' , '--path=/tmp/app' ] , import . meta. url ) ) . rejects . toThrow ( )
203+ expect ( outputResult ) . not . toHaveBeenCalled ( )
204+ expect ( validateApp ) . not . toHaveBeenCalled ( )
205+ } )
49206} )
0 commit comments