From 8c646116ee2ec02545ca52406d559c26c95ddd45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joan=20L=C3=B3pez=20de=20la=20Franca=20Beltran?= <5459617+joanlopez@users.noreply.github.com> Date: Mon, 11 Nov 2024 00:36:09 +0100 Subject: [PATCH 1/3] Fix imposters search (#173) * Fix imposters search --- internal/server/http/imposter.go | 13 +-- internal/server/http/imposter_test.go | 134 ++++++++++++++++++++++++- internal/server/http/route_matchers.go | 2 +- internal/server/http/server.go | 19 ++-- 4 files changed, 149 insertions(+), 19 deletions(-) diff --git a/internal/server/http/imposter.go b/internal/server/http/imposter.go index e5ae638..5ca451b 100644 --- a/internal/server/http/imposter.go +++ b/internal/server/http/imposter.go @@ -151,8 +151,8 @@ func NewImposterFS(path string) (ImposterFs, error) { }, nil } -func (i ImposterFs) FindImposters(impostersCh chan []Imposter) error { - err := fs.WalkDir(i.fs, ".", func(path string, info fs.DirEntry, err error) error { +func (ifs ImposterFs) FindImposters(impostersCh chan []Imposter) error { + err := fs.WalkDir(ifs.fs, ".", func(path string, info fs.DirEntry, err error) error { if err != nil { return fmt.Errorf("%w: error finding imposters", err) } @@ -168,7 +168,7 @@ func (i ImposterFs) FindImposters(impostersCh chan []Imposter) error { default: return nil } - imposters, err := i.unmarshalImposters(cfg) + imposters, err := ifs.unmarshalImposters(cfg) if err != nil { return err } @@ -176,11 +176,12 @@ func (i ImposterFs) FindImposters(impostersCh chan []Imposter) error { } return nil }) + close(impostersCh) return err } -func (i ImposterFs) unmarshalImposters(imposterConfig ImposterConfig) ([]Imposter, error) { - imposterFile, _ := i.fs.Open(imposterConfig.FilePath) +func (ifs ImposterFs) unmarshalImposters(imposterConfig ImposterConfig) ([]Imposter, error) { + imposterFile, _ := ifs.fs.Open(imposterConfig.FilePath) defer imposterFile.Close() bytes, _ := io.ReadAll(imposterFile) @@ -202,7 +203,7 @@ func (i ImposterFs) unmarshalImposters(imposterConfig ImposterConfig) ([]Imposte } for i := range imposters { - imposters[i].BasePath = filepath.Dir(imposterConfig.FilePath) + imposters[i].BasePath = filepath.Dir(filepath.Join(ifs.path, imposterConfig.FilePath)) imposters[i].Path = imposterConfig.FilePath } diff --git a/internal/server/http/imposter_test.go b/internal/server/http/imposter_test.go index 6eb9e0c..3077a92 100644 --- a/internal/server/http/imposter_test.go +++ b/internal/server/http/imposter_test.go @@ -1,9 +1,9 @@ package http import ( + "encoding/json" "testing" - "encoding/json" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v2" @@ -22,6 +22,138 @@ func TestNewImposterFS(t *testing.T) { }) } +func TestImposterFS_FindImposters(t *testing.T) { + // Set up + const expected = 7 + ifs, err := NewImposterFS("test/testdata/imposters") + require.NoError(t, err) + + // We trigger the imposters search. + // We expect exactly [expected] imposters. + ch := make(chan []Imposter, expected) + err = ifs.FindImposters(ch) + require.NoError(t, err) + + // We collect all the imposters. + received := make([]Imposter, 0, expected) + for ii := range ch { + received = append(received, ii...) + } + require.Len(t, received, expected) + + // Imposter 1 + schemaFile := "schemas/create_gopher_request.json" + bodyFile := "responses/create_gopher_response.json" + assert.EqualValues(t, Imposter{ + BasePath: "test/testdata/imposters", + Path: "create_gopher.imp.json", + Request: Request{ + Method: "POST", + Endpoint: "/gophers", + SchemaFile: &schemaFile, + Params: &map[string]string{ + "gopherColor": "{v:[a-z]+}", + }, + Headers: &map[string]string{ + "Content-Type": "application/json", + }, + }, + Response: Responses{{ + Status: 200, + Headers: &map[string]string{ + "Content-Type": "application/json", + }, + BodyFile: &bodyFile, + }}, + }, received[0]) + + // Imposter 2 + assert.EqualValues(t, Imposter{ + BasePath: "test/testdata/imposters", + Path: "create_gopher.imp.json", + Request: Request{}, + }, received[1]) + + // Imposter 3 + assert.EqualValues(t, Imposter{ + BasePath: "test/testdata/imposters", + Path: "test_request.imp.json", + Request: Request{ + Method: "GET", + Endpoint: "/testRequest", + }, + Response: Responses{{ + Status: 200, + Body: "Handled", + }}, + }, received[2]) + + // Imposter 4 + assert.EqualValues(t, Imposter{ + BasePath: "test/testdata/imposters", + Path: "test_request.imp.yaml", + Request: Request{ + Method: "GET", + Endpoint: "/yamlTestRequest", + }, + Response: Responses{{ + Status: 200, + Body: "Yaml Handled", + }}, + }, received[3]) + + // Imposter 5 + assert.EqualValues(t, Imposter{ + BasePath: "test/testdata/imposters", + Path: "test_request.imp.yml", + Request: Request{ + Method: "GET", + Endpoint: "/ymlTestRequest", + }, + Response: Responses{{ + Status: 200, + Body: "Yml Handled", + Delay: ResponseDelay{ + delay: 1000000000, + offset: 4000000000, + }, + }}, + }, received[4]) + + // Imposter 6 + assert.EqualValues(t, Imposter{ + BasePath: "test/testdata/imposters", + Path: "test_request.imp.yml", + Request: Request{ + Method: "POST", + Endpoint: "/yamlGophers", + Headers: &map[string]string{ + "Content-Type": "application/json", + }, + }, + Response: Responses{{ + Status: 201, + Headers: &map[string]string{ + "Content-Type": "application/json", + "X-Source": "YAML", + }, + BodyFile: &bodyFile, + }}, + }, received[5]) + + // Imposter 7 + assert.EqualValues(t, Imposter{ + BasePath: "test/testdata/imposters", + Path: "test_request.imp.yml", + Request: Request{}, + }, received[6]) + + // Finally, once the search is done, + // the channel must be closed. + _, open := <-ch + require.False(t, open) +} + func TestResponses_MarshalJSON(t *testing.T) { tcs := map[string]struct { rr *Responses diff --git a/internal/server/http/route_matchers.go b/internal/server/http/route_matchers.go index 0f6cbf2..f243b37 100644 --- a/internal/server/http/route_matchers.go +++ b/internal/server/http/route_matchers.go @@ -55,7 +55,7 @@ func validateSchema(imposter Imposter, req *http.Request) error { return fmt.Errorf("unexpected empty body request") } - schemaFilePath, _ := filepath.Abs(schemaFile) + schemaFilePath, err := filepath.Abs(schemaFile) if err != nil { return fmt.Errorf("%w: impossible find the schema", err) } diff --git a/internal/server/http/server.go b/internal/server/http/server.go index d334e84..794cc78 100644 --- a/internal/server/http/server.go +++ b/internal/server/http/server.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" _ "embed" + "errors" "log" "net/http" @@ -86,23 +87,19 @@ func (s *Server) Build() error { } var impostersCh = make(chan []Imposter) - var done = make(chan struct{}) go func() { s.imposterFs.FindImposters(impostersCh) - done <- struct{}{} }() loop: for { - select { - case imposters := <-impostersCh: - s.addImposterHandler(imposters) - log.Printf("imposter %s loaded\n", imposters[0].Path) - case <-done: - close(impostersCh) - close(done) + imposters, ok := <-impostersCh + if !ok { break loop } + + s.addImposterHandler(imposters) + log.Printf("imposter %s loaded\n", imposters[0].Path) } if s.proxy.mode == killgrave.ProxyMissing { s.router.NotFoundHandler = s.proxy.Handler() @@ -120,7 +117,7 @@ func (s *Server) Run() { } log.Printf("The fake server is on tap now: %s%s\n", s.httpServer.Addr, tlsString) err := s.run(s.secure) - if err != http.ErrServerClosed { + if !errors.Is(err, http.ErrServerClosed) { log.Fatal(err) } }() @@ -143,7 +140,7 @@ func (s *Server) run(secure bool) error { return s.httpServer.ListenAndServeTLS("", "") } -// Shutdown shutdown the current http server +// Shutdown shutdowns the current http server func (s *Server) Shutdown() error { log.Println("stopping server...") if err := s.httpServer.Shutdown(context.TODO()); err != nil { From d41b4b181aa77a41c4deb1e21bbc89f54e6f0eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joan=20L=C3=B3pez=20de=20la=20Franca=20Beltran?= <5459617+joanlopez@users.noreply.github.com> Date: Fri, 15 Nov 2024 11:12:48 +0100 Subject: [PATCH 2/3] Acceptance testing framework (#139) * Baseline for acceptance tests * Refine the acceptance testing framework and the test cases * Run acceptance tests as part of the CI * Update to Go v1.21 (CI) * Downgrade to golang.org/x/tools@v0.24.0 * Use specific go build flag for acceptance tests --- .github/workflows/main.yaml | 16 +- .github/workflows/release.yml | 6 +- Makefile | 8 +- acceptance/acceptance_test.go | 350 ++++++++++++++++++ acceptance/tests/simple/config.txtar | 65 ++++ acceptance/tests/simple/http/create.txtar | 24 ++ .../tests/simple/http/fetchExisting.txtar | 21 ++ .../tests/simple/http/fetchNotFound.txtar | 9 + acceptance/utils/network/network.go | 19 + go.mod | 7 +- go.sum | 17 +- 11 files changed, 522 insertions(+), 20 deletions(-) create mode 100644 acceptance/acceptance_test.go create mode 100644 acceptance/tests/simple/config.txtar create mode 100644 acceptance/tests/simple/http/create.txtar create mode 100644 acceptance/tests/simple/http/fetchExisting.txtar create mode 100644 acceptance/tests/simple/http/fetchNotFound.txtar create mode 100644 acceptance/utils/network/network.go diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 7ba8526..2a66f8e 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -1,23 +1,27 @@ name: Test & Build on: + pull_request: push: paths: - 'cmd/**' - 'internal/**' + - 'acceptance/**' jobs: build: runs-on: ubuntu-latest strategy: matrix: - go: ['1.20'] + go: [ '1.21' ] steps: - - uses: actions/checkout@v3 - - uses: actions/setup-go@v4 + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v4 with: go-version: ${{ matrix.go }} - - name: Run Tests... + - name: Run unit tests run: go test -v -vet=off -race ./... - - name: Build... - run: go build -race cmd/killgrave/main.go \ No newline at end of file + - name: Run acceptance tests + run: make acceptance \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 54f8a17..f018cc5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,13 +10,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v4 + uses: actions/setup-go@v5 with: - go-version: '1.20' + go-version: '1.21' - name: Run GoReleaser uses: goreleaser/goreleaser-action@v4 if: startsWith(github.ref, 'refs/tags/') diff --git a/Makefile b/Makefile index b773558..a193783 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,7 @@ -.PHONY: build +.PHONY: build acceptance + build: - go build -ldflags "-s -w -X 'github.com/friendsofgo/killgrave/internal/app/cmd._version=`git rev-parse --abbrev-ref HEAD`-`git rev-parse --short HEAD`'" -o bin/killgrave cmd/killgrave/main.go \ No newline at end of file + go build -ldflags "-s -w -X 'github.com/friendsofgo/killgrave/internal/app/cmd._version=`git rev-parse --abbrev-ref HEAD`-`git rev-parse --short HEAD`'" -o bin/killgrave cmd/killgrave/main.go + +acceptance: build + @(cd acceptance && go test -count=1 -tags=acceptance -v ./...) \ No newline at end of file diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go new file mode 100644 index 0000000..d2a1b6b --- /dev/null +++ b/acceptance/acceptance_test.go @@ -0,0 +1,350 @@ +//go:build acceptance + +package acceptance + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "io" + "io/fs" + "log" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + "regexp" + "strconv" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/tools/txtar" + + "github.com/friendsofgo/killgrave/acceptance/utils/network" +) + +const ( + // testsDir is the directory, within the `acceptance` folder, + // where the acceptance tests are located at. + testsDir = "tests" + + // addr is the address where the Killgrave binary + addr = "localhost" + + // bin is the path to where the Killgrave binary + // is expected to run acceptance tests. + bin = "../bin/killgrave" +) + +// Test is the entry point for the acceptance tests. +func Test(t *testing.T) { + // First of all, we extract the Killgrave version by running `killgrave version`. + // This is useful, not only to write a log that can serve as metadata for the test results, + // but also to ensure that the Killgrave binary is available. + version, err := extractKillgraveVersion(t) + if err != nil { + var pathErr *fs.PathError + if errors.As(err, &pathErr) || strings.Contains(err.Error(), "executable file not found in $PATH") { + log.Fatalf("Attention! It looks like you haven't compiled Killgrave, the execution of the Killgrave "+ + "binary has failed with: %v", err) + } + log.Fatalf("The execution of `killgrave version` has failed with: %v", err) + } + t.Logf("Running acceptance tests with Killgrave version: %s", version) + + // Once we now that the Killgrave binary is available, we can proceed with the acceptance tests. + // The first step is to collect all test cases from the `tests` directory. For each test: + // + // 0. The test case self-contain on each directory, which name is used as the test name. + // 1. Requires a `config.txtar` file at the root level of the test case directory, + // it is used to initialize a file system with all the configuration-related files, + // which not only includes the Killgrave configuration file but also the imposters. + // 2. Runs Killgrave in any available port (so we can run multiple test cases at the same + // time), using the configuration files from the previous step. + // 3. Requires an `http` directory which contains a set of request and response pairs. + // Each pair is defined by two files: `req.http` and `res.http`, where the request + // is the HTTP request that will be performed as part of the test, and the response + // is the response expected from Killgrave (which will be asserted). + // Each pair is considered one of the test cases that compose the acceptance test, + // defined by the aforementioned parent directory. + tcs := collectTestCases(t) + + for _, tc := range tcs { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + // 1. Create a temporary directory with the configuration files. + path := createTmpCfgDir(t, tc) + + // 2. Start the Killgrave process. + address := runApplication(t, path) + + // 3. Collect the request and response pairs + // and iterate over them to perform the tests. + rrs := collectRequestResponses(t, tc.path) + for _, rr := range rrs { + rr := rr + t.Run(rr.name, func(t *testing.T) { + // Override the address + rr.overrideAddress(address) + + // Send the request + res, err := http.DefaultClient.Do(rr.req) + require.NoError(t, err) + + // Assert the res + rr.assertResponse(t, res) + }) + } + }) + } +} + +// testCase represents a test case to be run. +// It is defined by the name of the test case and the path +// to the directory where the testCase files live in. +type testCase struct { + name string + path string +} + +// collectTestCases walks over the `tests` directory and +// constructs all the testCase's from the directories found. +func collectTestCases(t *testing.T) []testCase { + var tcs []testCase + + cwd, err := os.Getwd() + require.NoError(t, err) + + testsDir := filepath.Join(cwd, testsDir) + entries, err := os.ReadDir(testsDir) + require.NoError(t, err) + + for _, entry := range entries { + if entry.IsDir() { + tcs = append(tcs, testCase{ + name: entry.Name(), + path: filepath.Join(testsDir, entry.Name()), + }) + } + } + + return tcs +} + +// reqRes is a data structure that holds the information required to run +// each of the test cases that compose each acceptance test: +// - the name of the test case. +// - the request to be sent to Killgrave, as *http.Request. +// - the expected response from Killgrave, as []byte. +type reqRes struct { + name string + req *http.Request + res []byte +} + +// overrideAddress changes the request's URL to use the provided address. +// This is useful to run the tests against different addresses, e.g. different ports, +// which is a requirement to be able to run the acceptance tests concurrently. +func (rr reqRes) overrideAddress(address string) { + rr.req.URL.Scheme = "http" + rr.req.URL.Host = address +} + +// assertResponse is a self-contained function that can be used to assert +// that the response received from Killgrave matches the expected response. +// +// It builds the response string from the response object, and then it +// compares it with the expected response (from the test definition). +func (rr reqRes) assertResponse(t *testing.T, response *http.Response) { + t.Helper() + + // Read the response body + body, err := io.ReadAll(response.Body) + require.NoError(t, err) + + // Format the status line + statusLine := fmt.Sprintf("HTTP/%d.%d %s", response.ProtoMajor, response.ProtoMinor, response.Status) + + // Strip dynamic headers (to prevent false negatives) + response.Header.Del("Date") + + // Format the headers + var headersBuilder strings.Builder + err = response.Header.Write(&headersBuilder) + require.NoError(t, err) + headers := strings.ReplaceAll(headersBuilder.String(), "\r\n", "\n") + + // Format the response + res := fmt.Sprintf("%s\n%s\n\n%s", statusLine, headers, body) + assert.Equal(t, string(rr.res), res) +} + +// collectRequestResponses walks over the `http` directory of the test case +// and collects all the reqRes pairs. In other words, it collects all the test cases. +func collectRequestResponses(t *testing.T, path string) (rrs []reqRes) { + httpDir := filepath.Join(path, "http") + entries, err := os.ReadDir(httpDir) + require.NoError(t, err) + + for _, entry := range entries { + if entry.IsDir() { + continue + } + + rrFilePath := filepath.Join(httpDir, entry.Name()) + + contents, err := os.ReadFile(rrFilePath) + require.NoError(t, err) + + archive := txtar.Parse(contents) + + rrs = append(rrs, reqRes{ + name: entry.Name(), + req: readRequest(t, find(archive.Files, "req.http")), + res: find(archive.Files, "res.http"), + }) + } + + return +} + +func find(ff []txtar.File, name string) []byte { + for _, f := range ff { + if f.Name == name { + return f.Data + } + } + return nil +} + +// readRequest reads a raw HTTP request from a []byte (e.g. read from a file), +// and instantiates the equivalent *http.Request object from it. +func readRequest(t *testing.T, raw []byte) *http.Request { + req, err := http.ReadRequest(bufio.NewReader(bytes.NewReader(raw))) + require.NoError(t, err) + + baseURL, err := url.Parse(addr) + require.NoError(t, err) + + req.RequestURI = "" + req.URL = baseURL.ResolveReference(req.URL) + + return req +} + +// createTmpCfgDir creates a temporary directory with the configuration files defined +// in the `config.txtar` file, replicating the structure to be used by Killgrave, when executed. +// +// We follow this approach because this way we can test the application assuming the binary +// already exists, so these tests can be run with a recently generated binary (e.g. a release). +// +// Additionally, in the future we might explore ways to reuse this setup to run these tests +// as "integration tests", so faking using a fake, likely in-memory, file system but directly +// calling app.Run(). +func createTmpCfgDir(t *testing.T, tc testCase) string { + // First, we read the `config.txtar` file and initialize a txtar.Archive with its contents. + tmpCfgDir := filepath.Join(os.TempDir(), tc.name) + cfgFilePath := filepath.Join(tc.path, "config.txtar") + contents, err := os.ReadFile(cfgFilePath) + require.NoError(t, err) + archive := txtar.Parse(contents) + + // Then, we create the temporary directory and write the files. + for _, f := range archive.Files { + filePath := filepath.Join(tmpCfgDir, f.Name) + fileDir := filepath.Dir(filePath) + + err := os.MkdirAll(fileDir, os.ModePerm) + require.NoError(t, err) + + err = os.WriteFile(filePath, f.Data, os.ModePerm) + require.NoError(t, err) + } + + // Tell the testing framework to clean up the temporary directory after the test is done. + t.Cleanup(func() { + err := os.RemoveAll(tmpCfgDir) + require.NoError(t, err) + }) + + return tmpCfgDir +} + +// runApplication runs Killgrave assuming the binary already exists. +// It uses the imposters located at `from`, which path must be absolute. +// It runs the application on any available port, so we can run multiple +// tests concurrently. It returns the address as the first return value. +// +// For now, it redirects the application's output (stdout and stderr) +// to the test's output, but in the future, we might want to capture +// the output to assert the logs, and or use it in a smarter way. +func runApplication(t *testing.T, from string) string { + // Look for any available port. + port, err := network.AnyAvailablePort() + address := addr + ":" + strconv.Itoa(port) + require.NoError(t, err, "failed to find an available port") + + // Prepare the `killgrave` command, and start it. + cmd := exec.Command(bin, "-P", strconv.Itoa(port), "--imposters", filepath.Join(from, "imposters")) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err = cmd.Start() + require.NoError(t, err) + + // Tell the testing framework to stop the process after the test is done. + t.Cleanup(func() { + err := cmd.Process.Signal(os.Interrupt) + require.NoError(t, err) + + err = cmd.Wait() + require.NoError(t, err) + }) + + // Wait for the application to be ready. + const ( + maxWaitTime = 2 * time.Second + checkEvery = 100 * time.Millisecond + ) + require.Eventually(t, func() bool { + res, err := http.Get("http://" + address + "/nonExistingEndpoint") + return err == nil && res != nil && res.StatusCode == http.StatusNotFound + }, maxWaitTime, checkEvery) + + return address +} + +// extractKillgraveVersion runs the `killgrave version` command and uses a regular expression +// to extract the version from the output. In case there's any error (e.g. the binary is not +// available), it returns the error. +func extractKillgraveVersion(t *testing.T) (string, error) { + t.Helper() + + // Prepare the `killgrave version` command. + cmd := exec.Command(bin, "version", "-v") + + // Capture the command's output. + out := new(bytes.Buffer) + cmd.Stdout = out + + // Run the command, and check for errors. + err := cmd.Run() + if err != nil { + return "", err + } + + // Extract the Killgrave version from the output. + re := regexp.MustCompile(`Killgrave version:\s*([a-zA-Z0-9\-]+)`) + match := re.FindStringSubmatch(out.String()) + if len(match) == 2 { + return match[1], nil + } + + return "", errors.New("version not found") +} diff --git a/acceptance/tests/simple/config.txtar b/acceptance/tests/simple/config.txtar new file mode 100644 index 0000000..a857747 --- /dev/null +++ b/acceptance/tests/simple/config.txtar @@ -0,0 +1,65 @@ +-- imposters/gophers.imp.json -- +[ + { + "request": { + "method": "GET", + "endpoint": "/gophers/01D8EMQ185CA8PRGE20DKZTGSR", + "headers": { + "Content-Type": "application/json" + } + }, + "response": { + "status": 200, + "headers": { + "Content-Type": "application/json" + }, + "bodyFile": "responses/create_gopher_response.json" + } + }, + { + "request": { + "method": "POST", + "endpoint": "/gophers", + "headers": { + "Content-Type": "application/json" + }, + "params": { + "gopherColor": "{v:[a-z]+}" + } + }, + "response": { + "status": 201, + "headers": { + "Content-Type": "application/json" + }, + "bodyFile": "responses/create_gopher_response.json" + } + }, + { + "request": { + "method": "GET", + "endpoint": "/gophers/{_id:[\\w]{26}}", + "headers": { + "Content-Type": "application/json" + } + }, + "response": { + "status": 404 + } + }, + { + "t": "random_text" + } +] +-- imposters/responses/create_gopher_response.json -- +{ + "data": { + "type": "gophers", + "id": "01D8EMQ185CA8PRGE20DKZTGSR", + "attributes": { + "name": "Zebediah", + "color": "Purple", + "age": 54 + } + } +} \ No newline at end of file diff --git a/acceptance/tests/simple/http/create.txtar b/acceptance/tests/simple/http/create.txtar new file mode 100644 index 0000000..4444642 --- /dev/null +++ b/acceptance/tests/simple/http/create.txtar @@ -0,0 +1,24 @@ +-- req.http -- +POST /gophers?gopherColor=red HTTP/1.1 +Content-Length: 95 +Content-Type: application/json + +{"data": {"type": "gophers", "attributes": {"name": "Zebediah", "color": "Purple", "age": 54}}} + +-- res.http -- +HTTP/1.1 201 Created +Content-Length: 214 +Content-Type: application/json + + +{ + "data": { + "type": "gophers", + "id": "01D8EMQ185CA8PRGE20DKZTGSR", + "attributes": { + "name": "Zebediah", + "color": "Purple", + "age": 54 + } + } +} diff --git a/acceptance/tests/simple/http/fetchExisting.txtar b/acceptance/tests/simple/http/fetchExisting.txtar new file mode 100644 index 0000000..4a78d3f --- /dev/null +++ b/acceptance/tests/simple/http/fetchExisting.txtar @@ -0,0 +1,21 @@ +-- req.http -- +GET /gophers/01D8EMQ185CA8PRGE20DKZTGSR HTTP/1.1 +Content-Type: application/json + +-- res.http -- +HTTP/1.1 200 OK +Content-Length: 214 +Content-Type: application/json + + +{ + "data": { + "type": "gophers", + "id": "01D8EMQ185CA8PRGE20DKZTGSR", + "attributes": { + "name": "Zebediah", + "color": "Purple", + "age": 54 + } + } +} diff --git a/acceptance/tests/simple/http/fetchNotFound.txtar b/acceptance/tests/simple/http/fetchNotFound.txtar new file mode 100644 index 0000000..702a1e0 --- /dev/null +++ b/acceptance/tests/simple/http/fetchNotFound.txtar @@ -0,0 +1,9 @@ +-- req.http -- +GET /gophers/01D8EMQ185CA8PRGE20DKZT404 HTTP/1.1 +Content-Type: application/json + +-- res.http -- +HTTP/1.1 404 Not Found +Content-Length: 0 + + diff --git a/acceptance/utils/network/network.go b/acceptance/utils/network/network.go new file mode 100644 index 0000000..2b5f77c --- /dev/null +++ b/acceptance/utils/network/network.go @@ -0,0 +1,19 @@ +package network + +import "net" + +func AnyAvailablePort() (int, error) { + // Create a new TCP listener on port 0 (which means "any available port") + listener, err := net.Listen("tcp", ":0") + if err != nil { + return 0, err + } + + // Extract the port number from the listener address + port := listener.Addr().(*net.TCPAddr).Port + + // Close the listener to free up the port + err = listener.Close() + + return port, err +} diff --git a/go.mod b/go.mod index a14f6b3..549f841 100644 --- a/go.mod +++ b/go.mod @@ -3,24 +3,25 @@ module github.com/friendsofgo/killgrave go 1.21 require ( - github.com/gorilla/handlers v1.5.1 + github.com/gorilla/handlers v1.5.2 github.com/gorilla/mux v1.8.1 github.com/radovskyb/watcher v1.0.7 github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 github.com/xeipuuv/gojsonschema v1.2.0 + golang.org/x/tools v0.24.0 gopkg.in/yaml.v2 v2.4.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/felixge/httpsnoop v1.0.1 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index aa67312..4590e07 100644 --- a/go.sum +++ b/go.sum @@ -3,16 +3,19 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= -github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= -github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE= +github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w= github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -38,9 +41,11 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= From 2424d0a3c275d4b41a323bdaba2b0656100de975 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 09:03:59 +0100 Subject: [PATCH 3/3] Bump github.com/stretchr/testify from 1.9.0 to 1.10.0 (#181) Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.9.0 to 1.10.0. - [Release notes](https://github.com/stretchr/testify/releases) - [Commits](https://github.com/stretchr/testify/compare/v1.9.0...v1.10.0) --- updated-dependencies: - dependency-name: github.com/stretchr/testify dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 549f841..4288f68 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/radovskyb/watcher v1.0.7 github.com/spf13/cobra v1.8.1 - github.com/stretchr/testify v1.9.0 + github.com/stretchr/testify v1.10.0 github.com/xeipuuv/gojsonschema v1.2.0 golang.org/x/tools v0.24.0 gopkg.in/yaml.v2 v2.4.0 diff --git a/go.sum b/go.sum index 4590e07..9843ed8 100644 --- a/go.sum +++ b/go.sum @@ -32,8 +32,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=