Skip to content

Commit

Permalink
feat: http mock server and steps (#17)
Browse files Browse the repository at this point in the history
* feat: http mock server and steps

* fix: remove debug tag

* fix: remove github errors

* feat: clean mock requests

* fix: minor improvements in errors format

* fix: pull request feedback
  • Loading branch information
jlorgal authored Mar 24, 2021
1 parent ac3f23f commit e7970fb
Show file tree
Hide file tree
Showing 26 changed files with 659 additions and 136 deletions.
28 changes: 28 additions & 0 deletions cmd/mockhttp/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright 2021 Telefonica Cybersecurity & Cloud Tech SL
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
"flag"
"log"

"github.com/Telefonica/golium/mock/http"
)

func main() {
port := flag.Int("port", 9000, "port for the mock server")
mock := http.NewServer(*port)
log.Fatal(mock.Start())
}
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ require (
github.com/miekg/dns v1.1.40
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.8.0
github.com/spf13/pflag v1.0.5
github.com/tidwall/gjson v1.6.8
github.com/tidwall/pretty v1.1.0 // indirect
github.com/tidwall/sjson v1.1.5
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonschema v1.2.0
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
)
17 changes: 3 additions & 14 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,6 @@ github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-redis/redis/v8 v8.6.0 h1:swqbqOrxaPztsj2Hf1p94M3YAgl7hYEpcw21z299hh8=
github.com/go-redis/redis/v8 v8.6.0/go.mod h1:DQ9q4Rk2HtwkrwVrdgmphoOQDMfpvcd/nHEwRsicg8s=
github.com/go-redis/redis/v8 v8.7.1 h1:8IYi6RO83fNcG5amcUUYTN/qH2h4OjZHlim3KWGFSsA=
github.com/go-redis/redis/v8 v8.7.1/go.mod h1:BRxHBWn3pO3CfjyX6vAoyeRmCquvxr6QG+2onGV2gYs=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
Expand Down Expand Up @@ -187,8 +185,6 @@ github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaO
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.38 h1:MtIY+fmHUVVgv1AXzmKMWcwdCYxTRPG1EDjpqF4RCEw=
github.com/miekg/dns v1.1.38/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
Expand Down Expand Up @@ -272,15 +268,15 @@ github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE=
github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU=
github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.0.5 h1:RlE8xFRj9RrbHWXXNGa4MymvI1Ovlm70omjXQ1sWgFU=
github.com/tidwall/pretty v1.0.5/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.1.0 h1:K3hMW5epkdAVwibsQEfR/7Zj0Qgt4DxtNumTq/VloO8=
github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/sjson v1.1.5 h1:wsUceI/XDyZk3J1FUvuuYlK62zJv2HO2Pzb8A5EWdUE=
github.com/tidwall/sjson v1.1.5/go.mod h1:VuJzsZnTowhSxWdOgsAnb886i4AjEyTkk7tNtsL7EYE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
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=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
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=
Expand All @@ -290,19 +286,12 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opentelemetry.io/otel v0.17.0 h1:6MKOu8WY4hmfpQ4oQn34u6rYhnf2sWf1LXYO/UFm71U=
go.opentelemetry.io/otel v0.17.0/go.mod h1:Oqtdxmf7UtEvL037ohlgnaYa1h7GtMh0NcSd9eqkC9s=
go.opentelemetry.io/otel v0.18.0 h1:d5Of7+Zw4ANFOJB+TIn2K3QWsgS2Ht7OU9DqZHI6qu8=
go.opentelemetry.io/otel v0.18.0/go.mod h1:PT5zQj4lTsR1YeARt8YNKcFb88/c2IKoSABK9mX0r78=
go.opentelemetry.io/otel/metric v0.17.0 h1:t+5EioN8YFXQ2EH+1j6FHCKMUj+57zIDSnSGr/mWuug=
go.opentelemetry.io/otel/metric v0.17.0/go.mod h1:hUz9lH1rNXyEwWAhIWCMFWKhYtpASgSnObJFnU26dJ0=
go.opentelemetry.io/otel/metric v0.18.0 h1:yuZCmY9e1ZTaMlZXLrrbAPmYW6tW1A5ozOZeOYGaTaY=
go.opentelemetry.io/otel/metric v0.18.0/go.mod h1:kEH2QtzAyBy3xDVQfGZKIcok4ZZFvd5xyKPfPcuK6pE=
go.opentelemetry.io/otel/oteltest v0.17.0 h1:TyAihUowTDLqb4+m5ePAsR71xPJaTBJl4KDArIdi9k4=
go.opentelemetry.io/otel/oteltest v0.17.0/go.mod h1:JT/LGFxPwpN+nlsTiinSYjdIx3hZIGqHCpChcIZmdoE=
go.opentelemetry.io/otel/oteltest v0.18.0 h1:FbKDFm/LnQDOHuGjED+fy3s5YMVg0z019GJ9Er66hYo=
go.opentelemetry.io/otel/oteltest v0.18.0/go.mod h1:NyierCU3/G8DLTva7KRzGii2fdxdR89zXKH1bNWY7Bo=
go.opentelemetry.io/otel/trace v0.17.0 h1:SBOj64/GAOyWzs5F680yW1ITIfJkm6cJWL2YAvuL9xY=
go.opentelemetry.io/otel/trace v0.17.0/go.mod h1:bIujpqg6ZL6xUTubIUgziI1jSaUPthmabA/ygf/6Cfg=
go.opentelemetry.io/otel/trace v0.18.0 h1:ilCfc/fptVKaDMK1vWk0elxpolurJbEgey9J6g6s+wk=
go.opentelemetry.io/otel/trace v0.18.0/go.mod h1:FzdUu3BPwZSZebfQ1vl5/tAa8LyMLXSJN57AXIt/iDk=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
Expand Down
27 changes: 12 additions & 15 deletions launcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ package golium

import (
"context"
"flag"
"os"
"path"
"time"
Expand All @@ -25,6 +24,7 @@ import (
"github.com/cucumber/godog"
"github.com/cucumber/godog/colors"
"github.com/sirupsen/logrus"
"github.com/spf13/pflag"
)

// Launcher is responsible to launch golium (based on godog).
Expand All @@ -34,24 +34,24 @@ type Launcher struct {

// NewLauncher with a default configuration.
func NewLauncher() *Launcher {
config := GetConfig()
if err := cfg.LoadEnv(config); err != nil {
logrus.Fatalf("Error configuring golium with environment variables. %s", err)
}
return &Launcher{}
return NewLauncherWithYaml("")
}

// NewLauncherWithYaml with a configuration from a yaml file.
// The yaml file is merged with environment variables.
func NewLauncherWithYaml(path string) *Launcher {
config := GetConfig()
if err := cfg.LoadYaml(path, &config); err != nil {
logrus.Fatalf("Error configuring golium with yaml file: %s. %s", path, err)
if path != "" {
if err := cfg.LoadYaml(path, config); err != nil {
logrus.Fatalf("Error configuring golium with yaml file: %s. %s", path, err)
}
}
if err := cfg.LoadEnv(&config); err != nil {
if err := cfg.LoadEnv(config); err != nil {
logrus.Fatalf("Error configuring golium with environment variables. %s", err)
}
return &Launcher{}
l := &Launcher{}
l.configLogger()
return l
}

// Launch golium.
Expand All @@ -61,11 +61,8 @@ func (l *Launcher) Launch(testSuiteInitializer func(context.Context, *godog.Test
godogOpts := godog.Options{
Output: colors.Colored(os.Stdout),
}
godog.BindFlags("godog.", flag.CommandLine, &godogOpts)
flag.Parse()

// Configure the logger (based on logrus)
l.configLogger()
godog.BindCommandLineFlags("godog.", &godogOpts)
pflag.Parse()

start := time.Now()
logRecord := logrus.WithField("suite", conf.Suite).WithField("environment", conf.Environment)
Expand Down
51 changes: 51 additions & 0 deletions mock/http/model.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Copyright 2021 Telefonica Cybersecurity & Cloud Tech SL
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package http

import "encoding/json"

// MockRequest contains the instruction to configure the behaviour of the HTTP mock server.
// The document configures which request is going to be attended (e.g. the path and method)
// and the response to be generated by the mock.
type MockRequest struct {
// Permanent is true if the configuration is permanent.
// If permanent is false, the mockRequest is removed after matching the first request.
Permanent bool `json:"permanent"`
Request Request `json:"request"`
Response Response `json:"response"`
// Latency is the duration in milliseconds to wait to deliver the response.
// If 0, there is no latency to apply.
// If negative, there will be no response (timeout simulation).
Latency int `json:"latency"`
}

// Request configures the filter for the request of the MockRequest.
type Request struct {
Method string `json:"method,omitempty"`
Path string `json:"path,omitempty"`
Headers map[string][]string `json:"headers,omitempty"`
}

// Response configures which response if the request filter applies.
type Response struct {
Status int `json:"status"`
Headers map[string][]string `json:"headers"`
Body string `json:"body"`
}

func (m MockRequest) String() string {
b, _ := json.Marshal(&m)
return string(b)
}
86 changes: 86 additions & 0 deletions mock/http/requests.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright 2021 Telefonica Cybersecurity & Cloud Tech SL
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package http

import (
"net/http"
"sync"

"github.com/Telefonica/golium"
)

type MockRequests struct {
mockRequests []*MockRequest
mutex sync.Mutex
}

// PushMockRequest adds a MockRequest to the list.
func (m *MockRequests) PushMockRequest(mockRequest *MockRequest) {
m.mutex.Lock()
defer m.mutex.Unlock()
m.mockRequests = append(m.mockRequests, mockRequest)
}

// CleanMockRequests removes all the mockRequests from the list.
func (m *MockRequests) CleanMockRequests() {
m.mutex.Lock()
defer m.mutex.Unlock()
m.mockRequests = nil
}

// MatchMockRequest finds the first mockRequest matching the HTTP request.
func (m *MockRequests) MatchMockRequest(r *http.Request) *MockRequest {
for _, mockRequest := range m.mockRequests {
if matchMockRequest(r, mockRequest) {
return mockRequest
}
}
return nil
}

// Remove a mockRequest. It returns true if it was found and removed.
func (m *MockRequests) RemoveMockRequest(mockRequest *MockRequest) bool {
m.mutex.Lock()
defer m.mutex.Unlock()
for i, mr := range m.mockRequests {
if mr != mockRequest {
continue
}
m.mockRequests = append(m.mockRequests[:i], m.mockRequests[i+1:]...)
return true
}
return false
}

func matchMockRequest(r *http.Request, mockRequest *MockRequest) bool {
mr := mockRequest.Request
if mr.Method != "" && r.Method != mr.Method {
return false
}
if mr.Path != "" && r.URL.Path != mr.Path {
return false
}
if len(mr.Headers) != 0 {
for header, values := range mr.Headers {
rValues := r.Header[header]
for _, value := range values {
if !golium.ContainsString(value, rValues) {
return false
}
}
}
}
return true
}
91 changes: 91 additions & 0 deletions mock/http/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2021 Telefonica Cybersecurity & Cloud Tech SL
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package http

import (
"encoding/json"
"fmt"
"net/http"
"time"

"github.com/sirupsen/logrus"
)

// Server type for the HTTP mock server.
type Server struct {
Port int
mockRequests MockRequests
logger *logrus.Entry
}

// NewServer creates an instance of Server.
func NewServer(port int) *Server {
return &Server{
Port: port,
mockRequests: MockRequests{},
logger: logrus.WithField("mock", "http"),
}
}

// Start the HTTP mock server.
// Note that it blocks the current goroutine with http.ListenAndServe function.
func (s *Server) Start() error {
http.HandleFunc("/_mock/requests", s.handleMockRequest)
http.HandleFunc("/", s.handle)
addr := fmt.Sprintf(":%d", s.Port)
s.logger.Infof("Starting server at '%s'", addr)
return http.ListenAndServe(addr, nil)
}

func (s *Server) handleMockRequest(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodPost:
var mockRequest MockRequest
if err := json.NewDecoder(r.Body).Decode(&mockRequest); err != nil {
s.logger.Errorf("Failed decoding mockRequest: %s", err)
return
}
s.logger.Infof("Pushing mockRequest: %s", mockRequest)
s.mockRequests.PushMockRequest(&mockRequest)
case http.MethodDelete:
s.mockRequests.CleanMockRequests()
default:
w.WriteHeader(http.StatusMethodNotAllowed)
}
}

func (s *Server) handle(w http.ResponseWriter, r *http.Request) {
mockRequest := s.mockRequests.MatchMockRequest(r)
if mockRequest == nil {
w.WriteHeader(http.StatusNotFound)
return
}
if !mockRequest.Permanent {
s.mockRequests.RemoveMockRequest(mockRequest)
}
if mockRequest.Latency > 0 {
time.Sleep(time.Duration(mockRequest.Latency) * time.Millisecond)
}
resp := mockRequest.Response
for header, values := range resp.Headers {
for _, value := range values {
w.Header().Add(header, value)
}
}
w.WriteHeader(resp.Status)
if _, err := w.Write([]byte(resp.Body)); err != nil {
s.logger.Errorf("Failed writing the response body: %s", err)
}
}
Loading

0 comments on commit e7970fb

Please sign in to comment.