@@ -3,17 +3,18 @@ package aircmd
33import (
44 "bytes"
55 "encoding/json"
6- "io"
76 "net/http"
87 "net/http/httptest"
98 "strings"
109 "sync/atomic"
1110 "testing"
1211 "time"
1312
13+ "github.com/databricks/cli/cmd/root"
1414 "github.com/databricks/cli/libs/cmdctx"
1515 "github.com/databricks/cli/libs/cmdio"
1616 "github.com/databricks/cli/libs/flags"
17+ "github.com/databricks/cli/libs/testserver"
1718 "github.com/stretchr/testify/assert"
1819 "github.com/stretchr/testify/require"
1920)
@@ -137,30 +138,22 @@ type dryRunRequest struct {
137138 body map [string ]any
138139}
139140
140- // dryRunServer records workspace reads and serves one validation response.
141- func dryRunServer (t * testing.T , status int , response string , requests * []dryRunRequest ) * httptest.Server {
141+ func dryRunServer (t * testing.T , response any , requests * []dryRunRequest ) * testserver.Server {
142142 t .Helper ()
143- srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
144- request := dryRunRequest {method : r .Method , path : r .URL .Path }
145- if r .URL .Path == "/.well-known/databricks-config" {
146- * requests = append (* requests , request )
147- w .WriteHeader (http .StatusNotFound )
148- return
149- }
150- if r .URL .Path == validateConfigPath {
151- if err := json .NewDecoder (r .Body ).Decode (& request .body ); err != nil {
152- t .Errorf ("decode validation request: %v" , err )
143+ server := testserver .New (t )
144+ if requests != nil {
145+ server .RequestCallback = func (req * testserver.Request ) {
146+ request := dryRunRequest {method : req .Method , path : req .URL .Path }
147+ if req .URL .Path == validateConfigPath {
148+ require .NoError (t , json .Unmarshal (req .Body , & request .body ))
153149 }
154150 * requests = append (* requests , request )
155- w .WriteHeader (status )
156- _ , _ = w .Write ([]byte (response ))
157- return
158151 }
159- * requests = append ( * requests , request )
160- _ , _ = w . Write ([] byte ( `{"userName":"u@example.com","workspace_id":1}` ) )
161- }) )
162- t .Cleanup (srv .Close )
163- return srv
152+ }
153+ server . Handle ( "POST" , validateConfigPath , func ( _ testserver. Request ) any { return response } )
154+ testserver . AddDefaultHandlers ( server )
155+ t .Cleanup (server .Close )
156+ return server
164157}
165158
166159type dryRunResult struct {
@@ -189,23 +182,9 @@ func runDryRunCmd(t *testing.T, timeout time.Duration, output flags.Output, srvU
189182
190183func TestRunDryRunValidatesBackendWithoutMutation (t * testing.T ) {
191184 var requests []dryRunRequest
192- srv := dryRunServer (t , http .StatusOK , `{}` , & requests )
193- config := minimalConfig + `
194- environment:
195- dependencies:
196- - numpy
197- code_source:
198- type: snapshot
199- snapshot:
200- root_path: .
201- max_retries: 2
202- usage_policy_name: team-policy
203- permissions:
204- - group_name: users
205- level: CAN_VIEW
206- `
207-
208- result := runDryRunCmd (t , dryRunValidationTimeout , flags .OutputText , srv .URL , config , "--dry-run" , "--idempotency-key" , "flag-token" )
185+ srv := dryRunServer (t , validateConfigResponse {}, & requests )
186+
187+ result := runDryRunCmd (t , dryRunValidationTimeout , flags .OutputText , srv .URL , minimalConfig , "--dry-run" , "--idempotency-key" , "flag-token" )
209188 require .NoError (t , result .err )
210189 assert .Contains (t , result .stderr , `Dry run: configuration for "my-run" is valid; not submitting.` )
211190
@@ -226,80 +205,55 @@ permissions:
226205 assert .Contains (t , validations [0 ].body , "run_options" )
227206 task := validations [0 ].body ["task" ].(map [string ]any )
228207 commandPath := task ["deployments" ].([]any )[0 ].(map [string ]any )["command_path" ].(string )
229- assert .True (t , strings .HasPrefix (commandPath , "/Workspace/Users/u@example .com/.air/cli_launch/my-run/my-run_" ))
208+ assert .True (t , strings .HasPrefix (commandPath , "/Workspace/Users/tester@databricks .com/.air/cli_launch/my-run/my-run_" ))
230209 assert .True (t , strings .HasSuffix (commandPath , "/command.sh" ))
231210 runOptions := validations [0 ].body ["run_options" ].(map [string ]any )
232211 assert .Equal (t , "flag-token" , runOptions ["idempotency_token" ])
233212}
234213
235214func TestRunDryRunConfigValidationHasDeadline (t * testing.T ) {
236215 requestCanceled := make (chan struct {}, 1 )
237- srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
238- switch r .URL .Path {
239- case "/.well-known/databricks-config" :
240- w .WriteHeader (http .StatusNotFound )
241- case "/api/2.0/preview/scim/v2/Me" :
242- _ , _ = w .Write ([]byte (`{"userName":"u@example.com"}` ))
243- case validateConfigPath :
244- _ , _ = io .Copy (io .Discard , r .Body )
245- <- r .Context ().Done ()
246- requestCanceled <- struct {}{}
247- default :
248- http .NotFound (w , r )
249- }
250- }))
216+ srv := testserver .New (t )
217+ srv .Handle ("POST" , validateConfigPath , func (req testserver.Request ) any {
218+ <- req .Context .Done ()
219+ requestCanceled <- struct {}{}
220+ return nil
221+ })
222+ testserver .AddDefaultHandlers (srv )
251223 t .Cleanup (srv .Close )
252224
253225 result := runDryRunCmd (t , 100 * time .Millisecond , flags .OutputText , srv .URL , minimalConfig , "--dry-run" )
254- require .NoError (t , result .err )
255- assert .Contains (t , result .stderr , "only local validation was performed." )
256- assert .Contains (t , result .stderr , `Dry run: local validation passed for "my-run"; not submitting.` )
226+ require .Error (t , result .err )
227+ assert .Contains (t , result .err .Error (), "config validation unavailable: failed to validate config:" )
228+ assert .Contains (t , result .err .Error (), "context deadline exceeded" )
229+ assert .NotContains (t , result .stderr , "Dry run: configuration" )
257230 select {
258231 case <- requestCanceled :
259232 case <- time .After (time .Second ):
260233 require .Fail (t , "dry-run validation request context was not canceled" )
261234 }
262235}
263236
264- func TestRunDryRunFailsOnAuthenticationFailure (t * testing.T ) {
265- var requests []dryRunRequest
266- srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
267- requests = append (requests , dryRunRequest {method : r .Method , path : r .URL .Path })
268- if r .URL .Path == "/.well-known/databricks-config" {
269- w .WriteHeader (http .StatusNotFound )
270- return
271- }
272- w .WriteHeader (http .StatusUnauthorized )
273- _ , _ = w .Write ([]byte (`{"error_code":"UNAUTHENTICATED","message":"log in"}` ))
274- }))
275- t .Cleanup (srv .Close )
276-
277- result := runDryRunCmd (t , dryRunValidationTimeout , flags .OutputText , srv .URL , minimalConfig , "--dry-run" )
278- require .Error (t , result .err )
279- assert .Contains (t , result .err .Error (), "failed to resolve current user" )
280- assert .NotContains (t , result .stderr , "only local validation was performed" )
281- for _ , request := range requests {
282- assert .NotEqual (t , validateConfigPath , request .path )
283- }
284- }
285-
286- func TestRunDryRunIncompleteKeepsJSONOutputContract (t * testing.T ) {
287- var requests []dryRunRequest
288- srv := dryRunServer (t , http .StatusInternalServerError , `{"error_code":"INTERNAL_ERROR","message":"backend failed"}` , & requests )
237+ func TestRunDryRunUnavailableUsesJSONErrorEnvelope (t * testing.T ) {
238+ srv := dryRunServer (t , testserver.Response {
239+ StatusCode : http .StatusInternalServerError ,
240+ Body : map [string ]string {"error_code" : "INTERNAL_ERROR" , "message" : "backend failed" },
241+ }, nil )
289242 result := runDryRunCmd (t , dryRunValidationTimeout , flags .OutputJSON , srv .URL , minimalConfig , "--dry-run" )
290- require .NoError (t , result .err )
291- var got struct {
292- Data runResult `json:"data"`
293- }
243+ require .ErrorIs (t , result .err , root .ErrAlreadyPrinted )
244+ var got errorEnvelope
294245 require .NoError (t , json .Unmarshal ([]byte (result .stdout ), & got ))
295- assert .Equal (t , "DRY_RUN_OK" , got .Data .Status )
296- assert .True (t , got .Data .DryRun )
297- assert .Contains (t , result .stderr , "only local validation was performed" )
246+ assert .Equal (t , "VALIDATION_UNAVAILABLE" , got .Error .Code )
247+ assert .Equal (t , "TRANSIENT" , got .Error .Kind )
248+ assert .True (t , got .Error .Retryable )
249+ assert .Contains (t , got .Error .Message , "config validation unavailable" )
250+ assert .NotContains (t , result .stdout , "DRY_RUN_OK" )
251+ assert .Empty (t , result .stderr )
298252}
299253
300- func TestRunDryRunValidatesEffectiveIdempotencyTokenBeforeLocalFallback (t * testing.T ) {
254+ func TestRunDryRunValidatesIdempotencyTokenAfterWorkspaceValidation (t * testing.T ) {
301255 var requests []dryRunRequest
302- srv := dryRunServer (t , http . StatusOK , `{}` , & requests )
256+ srv := dryRunServer (t , validateConfigResponse {} , & requests )
303257 tooLong := strings .Repeat ("x" , 65 )
304258
305259 result := runDryRunCmd (t , dryRunValidationTimeout , flags .OutputText , srv .URL , minimalConfig , "--dry-run" , "--idempotency-key" , tooLong )
0 commit comments