Skip to content
This repository was archived by the owner on Aug 29, 2025. It is now read-only.

Commit 2ffda2b

Browse files
authored
fix: middleware refactor and more tests (#44)
* refactor HandleFunc into a bunch of middlewares * add tests for Authenticate and LoadEvent middleware
1 parent 254012d commit 2ffda2b

8 files changed

Lines changed: 299 additions & 121 deletions

File tree

app/app.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package app
33
import (
44
"errors"
55

6-
cloudevents "github.com/cloudevents/sdk-go/v2"
6+
"github.com/adfinis-sygroup/mopsos/app/models"
77
"github.com/sirupsen/logrus"
88
"gorm.io/gorm"
99
)
@@ -27,7 +27,7 @@ func NewApp(c *Config, db *gorm.DB) (*App, error) {
2727

2828
func (a *App) Run() {
2929
// eventChan is used to asynchronously pass events receiver from the Server to the Handler
30-
eventChan := make(chan cloudevents.Event)
30+
eventChan := make(chan models.EventData)
3131

3232
// handle events in background goroutine
3333
go func() {

app/handler.go

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package app
33
import (
44
"context"
55

6-
otelObs "github.com/cloudevents/sdk-go/observability/opentelemetry/v2/client"
7-
cloudevents "github.com/cloudevents/sdk-go/v2"
86
"github.com/sirupsen/logrus"
97
"gorm.io/gorm"
108
"gorm.io/gorm/clause"
@@ -26,36 +24,24 @@ func NewHandler(enableTracing bool, db *gorm.DB) *Handler {
2624
}
2725

2826
// HandleEvents blocks on the queue and handles events
29-
func (h *Handler) HandleEvents(eventChan chan cloudevents.Event) error {
27+
func (h *Handler) HandleEvents(eventChan chan models.EventData) error {
3028
// block on the event channel while ranging over its contents
31-
for event := range eventChan {
32-
err := h.HandleEvent(event)
29+
for data := range eventChan {
30+
err := h.HandleEvent(data)
3331
if err != nil {
34-
logrus.WithField("event", event).WithError(err).Error("failed to handle event")
32+
logrus.WithField("event", data.Event).WithError(err).Error("failed to handle event")
3533
}
3634
}
3735
return nil
3836
}
3937

40-
func (h *Handler) HandleEvent(event cloudevents.Event) error {
41-
log := logrus.WithField("event", event)
38+
func (h *Handler) HandleEvent(data models.EventData) error {
39+
log := logrus.WithField("event", data.Event)
4240
log.Debug("received event")
4341

4442
ctx := context.Background()
4543

46-
if h.enableTracing {
47-
ctx = otelObs.ExtractDistributedTracingExtension(ctx, event)
48-
}
49-
50-
record := &models.Record{}
51-
52-
err := event.DataAs(record)
53-
if err != nil {
54-
log.WithError(err).Errorf("failed to unmarshal event data")
55-
return err
56-
}
57-
58-
log.WithField("record", record).Debug("creating record")
44+
log.WithField("record", data.Record).Debug("creating record")
5945

6046
h.database.WithContext(ctx).Clauses(
6147
clause.OnConflict{
@@ -67,7 +53,7 @@ func (h *Handler) HandleEvent(event cloudevents.Event) error {
6753
},
6854
UpdateAll: true,
6955
},
70-
).Create(record)
56+
).Create(&data.Record)
7157

7258
return nil
7359
}

app/handler_test.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import (
1818
"github.com/adfinis-sygroup/mopsos/app/models"
1919
)
2020

21-
func eventStub(record *models.Record) cloudevents.Event {
21+
func eventStub(record *models.Record) models.EventData {
2222
ctx := context.Background()
2323

2424
evt := cloudevents.NewEvent(cloudevents.VersionV1)
@@ -47,12 +47,15 @@ func eventStub(record *models.Record) cloudevents.Event {
4747
fmt.Printf("%+v\n", ctx)
4848
fmt.Printf("%+v\n", evt)
4949

50-
return evt
50+
return models.EventData{
51+
Event: evt,
52+
Record: *record,
53+
}
5154
}
5255

5356
func Test_Handler_HandleEvent(t *testing.T) {
5457
type args struct {
55-
event cloudevents.Event
58+
eventData models.EventData
5659
}
5760
tests := []struct {
5861
name string
@@ -62,7 +65,7 @@ func Test_Handler_HandleEvent(t *testing.T) {
6265
{
6366
name: "simple event with minimal data",
6467
args: args{
65-
event: eventStub(
68+
eventData: eventStub(
6669
&models.Record{
6770
ClusterName: "test",
6871
ApplicationName: "test",
@@ -74,7 +77,7 @@ func Test_Handler_HandleEvent(t *testing.T) {
7477
{
7578
name: "event with complete data",
7679
args: args{
77-
event: eventStub(
80+
eventData: eventStub(
7881
&models.Record{
7982
ClusterName: "cluster-name",
8083
InstanceId: "cluster-instance",
@@ -88,7 +91,7 @@ func Test_Handler_HandleEvent(t *testing.T) {
8891
}
8992
for _, tt := range tests {
9093
t.Run(tt.name, func(t *testing.T) {
91-
evt := tt.args.event
94+
evt := tt.args.eventData.Event
9295
evtRecord := &models.Record{}
9396
err := evt.DataAs(evtRecord)
9497
if err != nil {
@@ -107,7 +110,7 @@ func Test_Handler_HandleEvent(t *testing.T) {
107110

108111
h := mopsos.NewHandler(true, gdb)
109112

110-
if err := h.HandleEvent(evt); (err != nil) != tt.wantErr {
113+
if err := h.HandleEvent(tt.args.eventData); (err != nil) != tt.wantErr {
111114
t.Errorf("Handler.HandleEvent() error = %v, wantErr %v", err, tt.wantErr)
112115
}
113116

app/models/eventData.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package models
2+
3+
import cloudevents "github.com/cloudevents/sdk-go/v2"
4+
5+
// EventData is the data structure for passing events between the server and the handler
6+
type EventData struct {
7+
Event cloudevents.Event
8+
Record Record
9+
}

app/server.go

Lines changed: 84 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@ import (
77

88
"github.com/adfinis-sygroup/mopsos/app/models"
99
otelObs "github.com/cloudevents/sdk-go/observability/opentelemetry/v2/client"
10-
cloudevents "github.com/cloudevents/sdk-go/v2"
1110
"github.com/cloudevents/sdk-go/v2/binding"
12-
"github.com/cloudevents/sdk-go/v2/protocol"
11+
"github.com/cloudevents/sdk-go/v2/event"
1312
httproto "github.com/cloudevents/sdk-go/v2/protocol/http"
1413
http_logrus "github.com/improbable-eng/go-httpwares/logging/logrus"
1514
"github.com/sirupsen/logrus"
@@ -20,9 +19,15 @@ import (
2019
type Server struct {
2120
config *Config
2221

23-
EventChan chan<- cloudevents.Event
22+
EventChan chan<- models.EventData
2423
}
2524

25+
type eventContext string
26+
27+
var ContextUsername eventContext = "mopsos.username"
28+
var ContextEvent eventContext = "mopsos.event"
29+
var ContextRecord eventContext = "mopsos.record"
30+
2631
// NewServer creates a server that receives CloudEvents from the network
2732
func NewServer(cfg *Config) *Server {
2833
return &Server{
@@ -33,91 +38,116 @@ func NewServer(cfg *Config) *Server {
3338
// Start starts the server and listens for incoming events
3439
func (s *Server) Start() {
3540
mux := http.NewServeMux()
36-
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
37-
// an example API handler
38-
err := json.NewEncoder(w).Encode(map[string]bool{"ok": true})
39-
if err != nil {
40-
logrus.WithError(err).Error("error encoding response")
41-
}
42-
})
43-
mux.Handle("/webhook", otelhttp.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
44-
ctx := r.Context()
41+
mux.HandleFunc("/health", s.HandleHealthCheck)
42+
mux.Handle("/webhook", otelhttp.NewHandler(
43+
s.Authenticate(
44+
s.LoadEvent(
45+
s.Validate(
46+
http.HandlerFunc(s.HandleWebhook),
47+
),
48+
),
49+
),
50+
"webhook-receiver"),
51+
)
52+
53+
logrus.WithField("listener", s.config.HttpListener).Info("Starting server")
54+
loggingMiddleware := http_logrus.Middleware(
55+
logrus.WithFields(logrus.Fields{}),
56+
)(mux)
57+
logrus.Fatal(http.ListenAndServe(s.config.HttpListener, loggingMiddleware))
58+
}
4559

60+
// WithEventChannel sets the event channel for the server
61+
func (s *Server) WithEventChannel(eventChan chan<- models.EventData) *Server {
62+
s.EventChan = eventChan
63+
return s
64+
}
65+
66+
// Authenticate middleware handles checking credentials
67+
func (s *Server) Authenticate(next http.Handler) http.Handler {
68+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
4669
// get basic auth credentials
4770
username, password, ok := r.BasicAuth()
4871
if !ok {
4972
http.Error(w, "missing Authorization header", http.StatusUnauthorized)
5073
return
5174
}
52-
if !s.checkAuth(username, password) {
75+
76+
logrus.WithFields(logrus.Fields{
77+
"username": username,
78+
}).Debug("checking credentials")
79+
if s.config.BasicAuthUsers[username] != password {
5380
http.Error(w, "invalid credentials", http.StatusUnauthorized)
5481
return
5582
}
83+
ctx := context.WithValue(r.Context(), ContextUsername, username)
5684

85+
next.ServeHTTP(w, r.WithContext(ctx))
86+
})
87+
}
88+
89+
// LoadEvent middlerware loads event from the request
90+
func (s *Server) LoadEvent(next http.Handler) http.Handler {
91+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
5792
// get event
5893
message := httproto.NewMessageFromHttpRequest(r)
59-
event, err := binding.ToEvent(context.TODO(), message)
94+
event, err := binding.ToEvent(r.Context(), message)
6095
if err != nil {
6196
logrus.WithError(err).Error("failed to decode event")
6297
return
6398
}
99+
if s.config.EnableTracing {
100+
// inject the span context into the event so it can be use i.e. while inserting to the database
101+
otelObs.InjectDistributedTracingExtension(r.Context(), *event)
102+
}
103+
logrus.Debugf("received event: %v", event)
64104

65-
// TODO consider how to harmonise this with what the handler does later on
105+
ctx := context.WithValue(r.Context(), ContextEvent, event)
106+
next.ServeHTTP(w, r.WithContext(ctx))
107+
})
108+
}
109+
110+
// Validate middleware handles checking received events for validity
111+
func (s *Server) Validate(next http.Handler) http.Handler {
112+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
113+
event := r.Context().Value(ContextEvent).(*event.Event)
66114
record := &models.Record{}
115+
67116
if err := event.DataAs(record); err != nil {
68117
logrus.WithError(err).Errorf("failed to unmarshal event data")
69118
http.Error(w, "failed to unmarshal event data", http.StatusInternalServerError)
70119
return
71120
}
72121

73122
// reject record that have not been sent from the right auth
74-
if record.ClusterName != username {
123+
if record.ClusterName != r.Context().Value(ContextUsername).(string) {
75124
http.Error(w, "event data does not match username", http.StatusUnauthorized)
76125
return
77126
}
78127

79-
err = s.HandleReceivedEvent(ctx, *event)
80-
if err != nil {
81-
logrus.WithError(err).Error("failed to handle event")
82-
return
83-
}
84-
// return 202 accepted once the event is on the queue
85-
w.WriteHeader(http.StatusAccepted)
86-
}), "webhook-receiver"))
87-
88-
logrus.WithField("listener", s.config.HttpListener).Info("Starting server")
89-
loggingMiddleware := http_logrus.Middleware(
90-
logrus.WithFields(logrus.Fields{}),
91-
)(mux)
92-
logrus.Fatal(http.ListenAndServe(s.config.HttpListener, loggingMiddleware))
128+
ctx := context.WithValue(r.Context(), ContextRecord, record)
129+
next.ServeHTTP(w, r.WithContext(ctx))
130+
})
93131
}
94132

95-
// WithEventChannel sets the event channel for the server
96-
func (s *Server) WithEventChannel(eventChan chan<- cloudevents.Event) *Server {
97-
s.EventChan = eventChan
98-
return s
133+
func (s *Server) HandleHealthCheck(w http.ResponseWriter, r *http.Request) {
134+
// an example API handler
135+
err := json.NewEncoder(w).Encode(map[string]bool{"ok": true})
136+
if err != nil {
137+
logrus.WithError(err).Error("error encoding response")
138+
}
99139
}
100140

101-
// HandleReceivedEvent is the handler for the cloudevents receiver, public for testing
102-
func (s *Server) HandleReceivedEvent(ctx context.Context, event cloudevents.Event) protocol.Result {
103-
104-
if s.config.EnableTracing {
105-
// inject the span context into the event so it can be use i.e. while inserting to the database
106-
otelObs.InjectDistributedTracingExtension(ctx, event)
107-
}
141+
func (s *Server) HandleWebhook(w http.ResponseWriter, r *http.Request) {
142+
// get middleware data from context
143+
event := r.Context().Value(ContextEvent).(*event.Event)
144+
record := r.Context().Value(ContextRecord).(*models.Record)
108145

109146
// send the event to the main app via the async channel
110-
s.EventChan <- event
111-
112-
logrus.Debugf("received event: %v", event)
113-
114-
return nil
115-
}
116-
117-
// checkAuth checks if the username and password are correct
118-
func (s *Server) checkAuth(username, password string) bool {
119-
logrus.WithFields(logrus.Fields{
120-
"username": username,
121-
}).Debug("checking credentials")
122-
return s.config.BasicAuthUsers[username] == password
147+
s.EventChan <- models.EventData{
148+
Event: *event,
149+
Record: *record,
150+
}
151+
// return 202 accepted once the event is on the queue
152+
w.WriteHeader(http.StatusAccepted)
123153
}

0 commit comments

Comments
 (0)