From 94424fbb775a2153f2b864d2cf0ea0979062f700 Mon Sep 17 00:00:00 2001 From: Janar Todesk Date: Tue, 22 Oct 2024 14:12:01 +0300 Subject: [PATCH] tooling: Implement a fake Defined.net HTTP API server Provide a fake server emulating Defined.net HTTP API for acceptance tests. --- go.mod | 4 +- go.sum | 10 +- internal/testing/server/host.go | 132 ++++++++++++++++++++++++++ internal/testing/server/repository.go | 83 ++++++++++++++++ internal/testing/server/server.go | 54 +++++++++++ 5 files changed, 278 insertions(+), 5 deletions(-) create mode 100644 internal/testing/server/host.go create mode 100644 internal/testing/server/repository.go create mode 100644 internal/testing/server/server.go diff --git a/go.mod b/go.mod index 21c3d52..ecbe3c9 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/sendsmaily/terraform-provider-definednet go 1.22.7 require ( + github.com/go-chi/chi/v5 v5.1.0 github.com/hashicorp/terraform-plugin-framework v1.12.0 github.com/onsi/ginkgo/v2 v2.20.2 github.com/onsi/gomega v1.34.2 @@ -30,11 +31,12 @@ require ( github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/oklog/run v1.0.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/stretchr/testify v1.9.0 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/sys v0.24.0 // indirect - golang.org/x/text v0.17.0 // indirect + golang.org/x/text v0.18.0 // indirect golang.org/x/tools v0.24.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 // indirect google.golang.org/grpc v1.66.2 // indirect diff --git a/go.sum b/go.sum index 358d216..ce066dd 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= +github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= @@ -70,8 +72,8 @@ github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +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/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= @@ -87,8 +89,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20240604185151-ef581f913117 h1:1GBuWVLM/KMVUv1t1En5Gs+gFZCNd360GGb4sSxtrhU= diff --git a/internal/testing/server/host.go b/internal/testing/server/host.go new file mode 100644 index 0000000..75b363e --- /dev/null +++ b/internal/testing/server/host.go @@ -0,0 +1,132 @@ +package server + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/go-chi/chi/v5" + "github.com/samber/lo" + "github.com/sendsmaily/terraform-provider-definednet/internal/definednet" +) + +// Host is a data model for a Defined.net host. +type Host struct { + Host definednet.Host + EnrollmentCode definednet.EnrollmentCode +} + +// Key returns the host's repository key. +func (h Host) Key() string { + return h.Host.ID +} + +func (s *Server) createHost(w http.ResponseWriter, r *http.Request) { + var req definednet.CreateHostRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + panic(err) + } + + state := Host{ + Host: definednet.Host{ + ID: fmt.Sprintf("host-%s", strings.ToUpper(lo.RandomString(8, lo.AlphanumericCharset))), + NetworkID: req.NetworkID, + RoleID: req.RoleID, + Name: req.Name, + IPAddress: func() string { + if !lo.IsEmpty(req.IPAddress) { + return req.IPAddress + } + + return "10.0.0.1" + }(), + StaticAddresses: req.StaticAddresses, + ListenPort: req.ListenPort, + IsLighthouse: req.IsLighthouse, + IsRelay: req.IsRelay, + Tags: req.Tags, + }, + } + + if err := s.Hosts.Add(state); err != nil { + panic(err) + } + + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(definednet.Response[definednet.Host]{ + Data: state.Host, + }); err != nil { + panic(err) + } +} + +func (s *Server) getHost(w http.ResponseWriter, r *http.Request) { + state, err := s.Hosts.Get(chi.URLParam(r, "id")) + if err != nil { + panic(err) + } + + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(definednet.Response[definednet.Host]{ + Data: state.Host, + }); err != nil { + panic(err) + } +} + +func (s *Server) updateHost(w http.ResponseWriter, r *http.Request) { + var req definednet.UpdateHostRequest + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + panic(err) + } + + state, err := s.Hosts.Get(chi.URLParam(r, "id")) + if err != nil { + panic(err) + } + + state.Host.Name = req.Name + state.Host.RoleID = req.RoleID + state.Host.StaticAddresses = req.StaticAddresses + state.Host.ListenPort = req.ListenPort + state.Host.Tags = req.Tags + + if err := s.Hosts.Replace(*state); err != nil { + panic(err) + } + + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(definednet.Response[definednet.Host]{ + Data: state.Host, + }); err != nil { + panic(err) + } +} + +func (s *Server) deleteHost(w http.ResponseWriter, r *http.Request) { + if err := s.Hosts.Remove(chi.URLParam(r, "id")); err != nil { + panic(err) + } + + w.WriteHeader(http.StatusOK) +} + +func (s *Server) createEnrollmentCode(w http.ResponseWriter, r *http.Request) { + state, err := s.Hosts.Get(chi.URLParam(r, "id")) + if err != nil { + panic(err) + } + + state.EnrollmentCode = definednet.EnrollmentCode{ + Code: lo.RandomString(16, lo.AlphanumericCharset), + LifetimeSeconds: 300, + } + + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(definednet.Response[definednet.EnrollmentCode]{ + Data: state.EnrollmentCode, + }); err != nil { + panic(err) + } +} diff --git a/internal/testing/server/repository.go b/internal/testing/server/repository.go new file mode 100644 index 0000000..feae495 --- /dev/null +++ b/internal/testing/server/repository.go @@ -0,0 +1,83 @@ +package server + +import ( + "fmt" + "sync" +) + +// NewRepository creates a fake API data repository. +func NewRepository[O Object]() *Repository[O] { + return &Repository[O]{ + data: make(map[string]O), + } +} + +// Repository is a fake API data repository. +type Repository[O Object] struct { + mu sync.Mutex + data map[string]O +} + +// Object is the object stored in the repository. +type Object interface { + Key() string +} + +// Add an object to repository. +func (r *Repository[O]) Add(m O) error { + r.mu.Lock() + defer r.mu.Unlock() + + // Primary identifiers must be unique. + if _, exists := r.data[m.Key()]; exists { + return fmt.Errorf("object with id %q already exists", m.Key()) + } + + // TODO: implement additional constraints enforcement. + // For example, Host.Name values must be unique. + + r.data[m.Key()] = m + + return nil +} + +// Get an object from the repository. +func (r *Repository[O]) Get(id string) (*O, error) { + r.mu.Lock() + defer r.mu.Unlock() + + obj, exists := r.data[id] + if !exists { + return nil, fmt.Errorf("object with id %q does not exist", id) + } + + return &obj, nil +} + +// Remove an object from the repository. +func (r *Repository[O]) Remove(id string) error { + r.mu.Lock() + defer r.mu.Unlock() + + if _, exists := r.data[id]; !exists { + return fmt.Errorf("object with id %q does not exist", id) + } + + delete(r.data, id) + + return nil +} + +// Replace an object in the repository. +func (r *Repository[O]) Replace(m O) error { + r.mu.Lock() + defer r.mu.Unlock() + + if _, exists := r.data[m.Key()]; !exists { + return fmt.Errorf("object with id %q does not exist", m.Key()) + } + + r.data[m.Key()] = m + + return nil +} diff --git a/internal/testing/server/server.go b/internal/testing/server/server.go new file mode 100644 index 0000000..39d9707 --- /dev/null +++ b/internal/testing/server/server.go @@ -0,0 +1,54 @@ +package server + +import ( + "log" + "net/http/httptest" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + "github.com/onsi/ginkgo/v2" + "github.com/samber/lo" + "github.com/sendsmaily/terraform-provider-definednet/internal/definednet" +) + +// New creates a fake Defined.net HTTP API server. +func New() *Server { + middleware.DefaultLogger = middleware.RequestLogger(&middleware.DefaultLogFormatter{ + Logger: log.New(ginkgo.GinkgoWriter, "", log.LstdFlags), + }) + + mux := chi.NewMux() + mux.Use(middleware.Logger) + mux.Use(middleware.Recoverer) + + srv := &Server{ + Hosts: NewRepository[Host](), + } + + // Hosts. + mux.Post("/v1/hosts", srv.createHost) + mux.Delete("/v1/hosts/{id}", srv.deleteHost) + mux.Get("/v1/hosts/{id}", srv.getHost) + mux.Put("/v2/hosts/{id}", srv.updateHost) + mux.Post("/v1/hosts/{id}/enrollment-code", srv.createEnrollmentCode) + + srv.server = httptest.NewServer(mux) + + return srv +} + +// Server is a fake Defined.net HTTP API server. +type Server struct { + Hosts *Repository[Host] + server *httptest.Server +} + +// Close the fake HTTP API server. +func (s *Server) Close() { + s.server.Close() +} + +// Client returns a client for the fake HTTP API server. +func (s *Server) Client() definednet.Client { + return lo.Must(definednet.NewClient(s.server.URL, "supersecret")) +}