github.com/jfrog/jfrog-cli-platform-services@v1.2.0/commands/dry_run_cmd_test.go (about) 1 package commands 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net/http" 10 "net/http/httptest" 11 "os" 12 "reflect" 13 "regexp" 14 "testing" 15 "time" 16 17 "github.com/stretchr/testify/assert" 18 "github.com/stretchr/testify/require" 19 20 "github.com/jfrog/jfrog-cli-platform-services/model" 21 ) 22 23 type dryRunAssertFunc func(t *testing.T, stdOutput []byte, err error, serverBehavior *dryRunServerStubBehavior) 24 25 func TestDryRun(t *testing.T) { 26 tests := []struct { 27 name string 28 commandArgs []string 29 assert dryRunAssertFunc 30 // Token to be sent in the request 31 token string 32 // The server behavior 33 serverBehavior *dryRunServerStubBehavior 34 // If provided the cliIn will be filled with this content 35 stdInput string 36 // If provided a temp file will be generated with this content and the file path will be added at the end of the command 37 fileInput string 38 }{ 39 { 40 name: "nominal case", 41 token: "my-token", 42 serverBehavior: &dryRunServerStubBehavior{ 43 responseStatus: http.StatusOK, 44 responseBody: map[string]any{ 45 "my": "payload", 46 }, 47 requestToken: "my-token", 48 }, 49 commandArgs: []string{mustJsonMarshal(t, map[string]any{"my": "payload"})}, 50 assert: assertDryRunSucceed, 51 }, 52 { 53 name: "fails if not OK status", 54 token: "invalid-token", 55 serverBehavior: &dryRunServerStubBehavior{ 56 requestToken: "valid-token", 57 }, 58 commandArgs: []string{`{}`}, 59 assert: assertDryRunFail("command failed with status %d", http.StatusForbidden), 60 }, 61 { 62 name: "reads from stdin", 63 token: "valid-token", 64 stdInput: mustJsonMarshal(t, map[string]any{"my": "request"}), 65 serverBehavior: &dryRunServerStubBehavior{ 66 requestToken: "valid-token", 67 requestBody: map[string]any{"my": "request"}, 68 responseBody: map[string]any{"valid": "response"}, 69 responseStatus: http.StatusOK, 70 }, 71 commandArgs: []string{"-"}, 72 assert: assertDryRunSucceed, 73 }, 74 { 75 name: "reads from file", 76 token: "valid-token", 77 fileInput: mustJsonMarshal(t, map[string]any{"my": "file-content"}), 78 serverBehavior: &dryRunServerStubBehavior{ 79 requestToken: "valid-token", 80 requestBody: map[string]any{"my": "file-content"}, 81 responseBody: map[string]any{"valid": "response"}, 82 responseStatus: http.StatusOK, 83 }, 84 assert: assertDryRunSucceed, 85 }, 86 { 87 name: "fails if invalid json from argument", 88 commandArgs: []string{`{"my":`}, 89 assert: assertDryRunFail("invalid json payload: unexpected end of JSON input"), 90 }, 91 { 92 name: "fails if invalid json from file argument", 93 fileInput: `{"my":`, 94 assert: assertDryRunFail("invalid json payload: unexpected end of JSON input"), 95 }, 96 { 97 name: "fails if invalid json from standard input", 98 commandArgs: []string{"-"}, 99 stdInput: `{"my":`, 100 assert: assertDryRunFail("unexpected EOF"), 101 }, 102 { 103 name: "fails if missing file", 104 commandArgs: []string{"@non-existing-file.json"}, 105 assert: assertDryRunFail("open non-existing-file.json: no such file or directory"), 106 }, 107 { 108 name: "fails if timeout exceeds", 109 commandArgs: []string{"--" + model.FlagTimeout, "500", `{}`}, 110 serverBehavior: &dryRunServerStubBehavior{ 111 waitFor: 5 * time.Second, 112 }, 113 assert: assertDryRunFail("request timed out after 500ms"), 114 }, 115 { 116 name: "fails if invalid timeout", 117 commandArgs: []string{"--" + model.FlagTimeout, "abc", `{}`}, 118 assert: assertDryRunFail("invalid timeout provided"), 119 }, 120 { 121 name: "fails if empty file path", 122 commandArgs: []string{"@"}, 123 assert: assertDryRunFail("missing file path"), 124 }, 125 } 126 127 for _, tt := range tests { 128 t.Run(tt.name, func(t *testing.T) { 129 ctx, cancelCtx := context.WithCancel(context.Background()) 130 t.Cleanup(cancelCtx) 131 132 runCmd := createCliRunner(t, GetInitCommand(), GetDryRunCommand()) 133 134 _, workerName := prepareWorkerDirForTest(t) 135 136 err := runCmd("worker", "init", "BEFORE_DOWNLOAD", workerName) 137 require.NoError(t, err) 138 139 serverResponseStubs := map[string]*dryRunServerStubBehavior{} 140 if tt.serverBehavior != nil { 141 serverResponseStubs[workerName] = tt.serverBehavior 142 } 143 144 err = os.Setenv(model.EnvKeyServerUrl, newDryRunServerStub(t, ctx, serverResponseStubs)) 145 require.NoError(t, err) 146 t.Cleanup(func() { 147 _ = os.Unsetenv(model.EnvKeyServerUrl) 148 }) 149 150 err = os.Setenv(model.EnvKeyAccessToken, tt.token) 151 require.NoError(t, err) 152 t.Cleanup(func() { 153 _ = os.Unsetenv(model.EnvKeyAccessToken) 154 }) 155 156 err = os.Setenv(model.EnvKeySecretsPassword, secretPassword) 157 require.NoError(t, err) 158 t.Cleanup(func() { 159 _ = os.Unsetenv(model.EnvKeySecretsPassword) 160 }) 161 162 if tt.stdInput != "" { 163 cliIn = bytes.NewReader([]byte(tt.stdInput)) 164 t.Cleanup(func() { 165 cliIn = os.Stdin 166 }) 167 } 168 169 if tt.fileInput != "" { 170 tt.commandArgs = append(tt.commandArgs, "@"+createTempFileWithContent(t, tt.fileInput)) 171 } 172 173 var output bytes.Buffer 174 175 cliOut = &output 176 t.Cleanup(func() { 177 cliOut = os.Stdout 178 }) 179 180 cmd := append([]string{"worker", "dry-run"}, tt.commandArgs...) 181 182 err = runCmd(cmd...) 183 184 cancelCtx() 185 186 tt.assert(t, output.Bytes(), err, tt.serverBehavior) 187 }) 188 } 189 } 190 191 func assertDryRunSucceed(t *testing.T, output []byte, err error, serverBehavior *dryRunServerStubBehavior) { 192 require.NoError(t, err) 193 194 outputData := map[string]any{} 195 196 err = json.Unmarshal(output, &outputData) 197 require.NoError(t, err) 198 199 assert.Equal(t, serverBehavior.responseBody, outputData) 200 } 201 202 func assertDryRunFail(errorMessage string, errorMessageArgs ...any) dryRunAssertFunc { 203 return func(t *testing.T, stdOutput []byte, err error, serverResponse *dryRunServerStubBehavior) { 204 require.Error(t, err) 205 assert.EqualError(t, err, fmt.Sprintf(errorMessage, errorMessageArgs...)) 206 } 207 } 208 209 var dryRunUrlPattern = regexp.MustCompile(`^/worker/api/v1/test/([\S/]+)$`) 210 211 type dryRunServerStubBehavior struct { 212 waitFor time.Duration 213 responseStatus int 214 responseBody map[string]any 215 requestToken string 216 requestBody map[string]any 217 } 218 219 type dryRunServerStub struct { 220 t *testing.T 221 ctx context.Context 222 stubs map[string]*dryRunServerStubBehavior 223 } 224 225 func newDryRunServerStub(t *testing.T, ctx context.Context, responseStubs map[string]*dryRunServerStubBehavior) string { 226 stub := dryRunServerStub{stubs: responseStubs, ctx: ctx} 227 server := httptest.NewUnstartedServer(&stub) 228 t.Cleanup(server.Close) 229 server.Start() 230 return "http:" + "//" + server.Listener.Addr().String() 231 } 232 233 func (s *dryRunServerStub) ServeHTTP(res http.ResponseWriter, req *http.Request) { 234 matches := dryRunUrlPattern.FindAllStringSubmatch(req.URL.Path, -1) 235 if len(matches) == 0 || len(matches[0][1]) < 1 { 236 res.WriteHeader(http.StatusNotFound) 237 return 238 } 239 240 if req.Header.Get("content-type") != "application/json" { 241 res.WriteHeader(http.StatusBadRequest) 242 return 243 } 244 245 workerName := matches[0][1] 246 247 behavior, exists := s.stubs[workerName] 248 if !exists { 249 res.WriteHeader(http.StatusNotFound) 250 return 251 } 252 253 if behavior.waitFor > 0 { 254 select { 255 case <-s.ctx.Done(): 256 return 257 case <-time.After(behavior.waitFor): 258 } 259 } 260 261 // Validate token 262 if req.Header.Get("authorization") != "Bearer "+behavior.requestToken { 263 res.WriteHeader(http.StatusForbidden) 264 return 265 } 266 267 // Validate body if requested 268 if behavior.requestBody != nil { 269 wantData, checkRequestData := behavior.responseBody["data"] 270 271 if checkRequestData { 272 gotData, err := io.ReadAll(req.Body) 273 if err != nil { 274 s.t.Logf("Read request body error: %+v", err) 275 res.WriteHeader(http.StatusInternalServerError) 276 return 277 } 278 279 decodedData := map[string]any{} 280 err = json.Unmarshal(gotData, &decodedData) 281 if err != nil { 282 s.t.Logf("Unmarshall request body error: %+v", err) 283 res.WriteHeader(http.StatusInternalServerError) 284 return 285 } 286 287 if !reflect.DeepEqual(wantData, decodedData) { 288 res.WriteHeader(http.StatusBadRequest) 289 return 290 } 291 } 292 } 293 294 bodyBytes, err := json.Marshal(behavior.responseBody) 295 if err != nil { 296 s.t.Logf("Marshall error: %+v", err) 297 res.WriteHeader(http.StatusInternalServerError) 298 return 299 } 300 301 res.WriteHeader(behavior.responseStatus) 302 _, err = res.Write(bodyBytes) 303 if err != nil { 304 s.t.Logf("Write error: %+v", err) 305 res.WriteHeader(http.StatusInternalServerError) 306 } 307 }