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  }