Skip to content

Commit

Permalink
server: add http server tests
Browse files Browse the repository at this point in the history
  • Loading branch information
andydunstall committed May 2, 2024
1 parent 4f790eb commit ac22ad4
Show file tree
Hide file tree
Showing 9 changed files with 281 additions and 35 deletions.
3 changes: 3 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ jobs:
- name: Unit Tests
run: go test ./...

- name: Integration Tests
run: go test ./... -tags integration

lint:
runs-on: ubuntu-latest
needs: build
Expand Down
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ pico:
mkdir -p build
go build -o build/pico main.go

.PHONY: unit-test
unit-test:
go test ./...

.PHONY: integration-test
integration-test:
go test ./... -tags integration

.PHONY: fmt
fmt:
go fmt ./...
Expand Down
8 changes: 7 additions & 1 deletion cli/agent/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package agent
import (
"context"
"fmt"
"net"
"os"
"os/signal"
"syscall"
Expand Down Expand Up @@ -110,8 +111,13 @@ func run(conf *config.Config, logger log.Logger) error {
logger.Info("starting pico agent", zap.Any("conf", conf))

registry := prometheus.NewRegistry()

adminLn, err := net.Listen("tcp", conf.Admin.BindAddr)
if err != nil {
return fmt.Errorf("admin listen: %s: %w", conf.Admin.BindAddr, err)
}
adminServer := adminserver.NewServer(
conf.Admin.BindAddr,
adminLn,
registry,
logger,
)
Expand Down
20 changes: 17 additions & 3 deletions cli/server/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,13 @@ func run(conf *config.Config, logger log.Logger) error {
logger.Info("starting pico server", zap.Any("conf", conf))

registry := prometheus.NewRegistry()

adminLn, err := net.Listen("tcp", conf.Admin.BindAddr)
if err != nil {
return fmt.Errorf("admin listen: %s: %w", conf.Admin.BindAddr, err)
}
adminServer := adminserver.NewServer(
conf.Admin.BindAddr,
adminLn,
registry,
logger,
)
Expand Down Expand Up @@ -189,15 +194,24 @@ func run(conf *config.Config, logger log.Logger) error {
p := proxy.NewProxy(networkMap, registry, logger)
adminServer.AddStatus("/proxy", proxy.NewStatus(p))

proxyLn, err := net.Listen("tcp", conf.Proxy.BindAddr)
if err != nil {
return fmt.Errorf("proxy listen: %s: %w", conf.Proxy.BindAddr, err)
}
proxyServer := proxyserver.NewServer(
conf.Proxy.BindAddr,
proxyLn,
p,
&conf.Proxy,
registry,
logger,
)

upstreamLn, err := net.Listen("tcp", conf.Upstream.BindAddr)
if err != nil {
return fmt.Errorf("upstream listen: %s: %w", conf.Upstream.BindAddr, err)
}
upstreamServer := upstreamserver.NewServer(
conf.Upstream.BindAddr,
upstreamLn,
p,
registry,
logger,
Expand Down
17 changes: 9 additions & 8 deletions server/server/admin/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package admin
import (
"context"
"fmt"
"net"
"net/http"

"github.com/andydunstall/pico/pkg/log"
Expand All @@ -17,7 +18,7 @@ import (
// Server is the admin HTTP server, which exposes endpoints for metrics, health
// and inspecting the node status.
type Server struct {
addr string
ln net.Listener

router *gin.Engine

Expand All @@ -29,24 +30,24 @@ type Server struct {
}

func NewServer(
addr string,
ln net.Listener,
registry *prometheus.Registry,
logger log.Logger,
) *Server {
router := gin.New()
server := &Server{
addr: addr,
ln: ln,
router: router,
httpServer: &http.Server{
Addr: addr,
Addr: ln.Addr().String(),
Handler: router,
},
registry: registry,
logger: logger.WithSubsystem("admin.server"),
}

// Recover from panics.
server.router.Use(gin.CustomRecovery(server.panicRoute))
server.router.Use(gin.CustomRecoveryWithWriter(nil, server.panicRoute))

server.router.Use(middleware.NewLogger(logger))
if registry != nil {
Expand All @@ -64,9 +65,9 @@ func (s *Server) AddStatus(route string, handler status.Handler) {
}

func (s *Server) Serve() error {
s.logger.Info("starting http server", zap.String("addr", s.addr))
s.logger.Info("starting http server", zap.String("addr", s.ln.Addr().String()))

if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
if err := s.httpServer.Serve(s.ln); err != nil && err != http.ErrServerClosed {
return fmt.Errorf("http serve: %w", err)
}
return nil
Expand All @@ -83,7 +84,7 @@ func (s *Server) Close() error {
}

func (s *Server) registerRoutes() {
s.router.GET("/healthz", s.healthRoute)
s.router.GET("/health", s.healthRoute)

if s.registry != nil {
s.router.GET("/metrics", s.metricsHandler())
Expand Down
114 changes: 114 additions & 0 deletions server/server/admin/server_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//go:build integration

package admin

import (
"bytes"
"context"
"fmt"
"net"
"net/http"
"testing"

"github.com/andydunstall/pico/pkg/log"
"github.com/andydunstall/pico/server/status"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

type fakeStatus struct {
}

func (s *fakeStatus) Register(group *gin.RouterGroup) {
group.GET("/foo", s.fooRoute)
}

func (s *fakeStatus) fooRoute(c *gin.Context) {
c.String(http.StatusOK, "foo")
}

var _ status.Handler = &fakeStatus{}

func TestServer_AdminRoutes(t *testing.T) {
adminLn, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)

adminServer := NewServer(
adminLn,
prometheus.NewRegistry(),
log.NewNopLogger(),
)
go func() {
require.NoError(t, adminServer.Serve())
}()
defer adminServer.Shutdown(context.TODO())

t.Run("health", func(t *testing.T) {
url := fmt.Sprintf("http://%s/health", adminLn.Addr().String())
resp, err := http.Get(url)
assert.NoError(t, err)
defer resp.Body.Close()

assert.Equal(t, http.StatusOK, resp.StatusCode)
})

t.Run("metrics", func(t *testing.T) {
url := fmt.Sprintf("http://%s/metrics", adminLn.Addr().String())
resp, err := http.Get(url)
assert.NoError(t, err)
defer resp.Body.Close()

assert.Equal(t, http.StatusOK, resp.StatusCode)
})

t.Run("not found", func(t *testing.T) {
url := fmt.Sprintf("http://%s/foo", adminLn.Addr().String())
resp, err := http.Get(url)
assert.NoError(t, err)
defer resp.Body.Close()

assert.Equal(t, http.StatusNotFound, resp.StatusCode)
})
}

func TestServer_StatusRoutes(t *testing.T) {
adminLn, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)

adminServer := NewServer(
adminLn,
prometheus.NewRegistry(),
log.NewNopLogger(),
)
adminServer.AddStatus("/mystatus", &fakeStatus{})

go func() {
require.NoError(t, adminServer.Serve())
}()
defer adminServer.Shutdown(context.TODO())

t.Run("status ok", func(t *testing.T) {
url := fmt.Sprintf("http://%s/status/mystatus/foo", adminLn.Addr().String())
resp, err := http.Get(url)
assert.NoError(t, err)
defer resp.Body.Close()

assert.Equal(t, http.StatusOK, resp.StatusCode)

buf := new(bytes.Buffer)
//nolint
buf.ReadFrom(resp.Body)
assert.Equal(t, []byte("foo"), buf.Bytes())
})

t.Run("not found", func(t *testing.T) {
url := fmt.Sprintf("http://%s/status/notfound", adminLn.Addr().String())
resp, err := http.Get(url)
assert.NoError(t, err)
defer resp.Body.Close()

assert.Equal(t, http.StatusNotFound, resp.StatusCode)
})
}
30 changes: 14 additions & 16 deletions server/server/proxy/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,33 @@ import (
"errors"
"fmt"
"io"
"net"
"net/http"
"strings"
"time"

"github.com/andydunstall/pico/pkg/log"
"github.com/andydunstall/pico/pkg/status"
"github.com/andydunstall/pico/server/config"
"github.com/andydunstall/pico/server/proxy"
"github.com/andydunstall/pico/server/server/middleware"
"github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus"
"go.uber.org/zap"
)

type Proxy interface {
Request(ctx context.Context, r *http.Request) (*http.Response, error)
}

// Server is the HTTP server for the proxy, which proxies all incoming
// requests.
type Server struct {
addr string
ln net.Listener

router *gin.Engine

httpServer *http.Server

proxy *proxy.Proxy
proxy Proxy

shutdownCtx context.Context
shutdownCancel func()
Expand All @@ -39,8 +42,8 @@ type Server struct {
}

func NewServer(
addr string,
proxy *proxy.Proxy,
ln net.Listener,
proxy Proxy,
conf *config.ProxyConfig,
registry *prometheus.Registry,
logger log.Logger,
Expand All @@ -49,10 +52,10 @@ func NewServer(

router := gin.New()
server := &Server{
addr: addr,
ln: ln,
router: router,
httpServer: &http.Server{
Addr: addr,
Addr: ln.Addr().String(),
Handler: router,
},
shutdownCtx: shutdownCtx,
Expand All @@ -63,7 +66,7 @@ func NewServer(
}

// Recover from panics.
server.router.Use(gin.CustomRecovery(server.panicRoute))
server.router.Use(gin.CustomRecoveryWithWriter(nil, server.panicRoute))

server.router.Use(middleware.NewLogger(logger))
if registry != nil {
Expand All @@ -76,9 +79,9 @@ func NewServer(
}

func (s *Server) Serve() error {
s.logger.Info("starting http server", zap.String("addr", s.addr))
s.logger.Info("starting http server", zap.String("addr", s.ln.Addr().String()))

if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
if err := s.httpServer.Serve(s.ln); err != nil && err != http.ErrServerClosed {
return fmt.Errorf("http serve: %w", err)
}
return nil
Expand Down Expand Up @@ -123,11 +126,6 @@ func (s *Server) proxyRoute(c *gin.Context) {
}

func (s *Server) notFoundRoute(c *gin.Context) {
// All /pico endpoints are reserved. All others are proxied.
if strings.HasPrefix(c.Request.URL.Path, "/pico") {
c.Status(http.StatusNotFound)
return
}
s.proxyRoute(c)
}

Expand Down
Loading

0 comments on commit ac22ad4

Please sign in to comment.