Skip to content

Commit 4a94576

Browse files
test: add table-driven tests for ReceiverServer
Adds httptest-based unit tests for receiver_server.go covering: - NewReceiverServer constructor field validation - ListenAndServe routing: unknown hook token → 404, non-hook path → 404, exportHTTPPathMetrics variant → 404 - Graceful shutdown via stop channel - Webhook path routing dispatches to handlePayload (not 404) Each subtest uses a private Prometheus registry to avoid duplicate metric registration panics. Addresses #496.
1 parent efc6bbf commit 4a94576

1 file changed

Lines changed: 213 additions & 0 deletions

File tree

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
/*
2+
Copyright 2024 The Flux authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package server
18+
19+
import (
20+
"net"
21+
"net/http"
22+
"strconv"
23+
"testing"
24+
"time"
25+
26+
. "github.com/onsi/gomega"
27+
"github.com/prometheus/client_golang/prometheus"
28+
prommetrics "github.com/slok/go-http-metrics/metrics/prometheus"
29+
"github.com/slok/go-http-metrics/middleware"
30+
corev1 "k8s.io/api/core/v1"
31+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
32+
"k8s.io/apimachinery/pkg/runtime"
33+
fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake"
34+
log "sigs.k8s.io/controller-runtime/pkg/log"
35+
36+
"github.com/fluxcd/pkg/apis/meta"
37+
38+
apiv1 "github.com/fluxcd/notification-controller/api/v1"
39+
)
40+
41+
// newTestReceiverServer creates a ReceiverServer listening on a free port and
42+
// returns the server, its base URL and a stop channel. The caller must close
43+
// the stop channel to shut the server down.
44+
func newTestReceiverServer(t *testing.T, exportHTTPPathMetrics bool, objs ...runtime.Object) (*ReceiverServer, string, chan struct{}) {
45+
t.Helper()
46+
g := NewWithT(t)
47+
48+
scheme := runtime.NewScheme()
49+
g.Expect(corev1.AddToScheme(scheme)).To(Succeed())
50+
g.Expect(apiv1.AddToScheme(scheme)).To(Succeed())
51+
52+
builder := fakeclient.NewClientBuilder().WithScheme(scheme).
53+
WithIndex(&apiv1.Receiver{}, WebhookPathIndexKey, IndexReceiverWebhookPath)
54+
for _, o := range objs {
55+
builder = builder.WithRuntimeObjects(o)
56+
}
57+
kc := builder.Build()
58+
59+
l, err := net.Listen("tcp", "127.0.0.1:0")
60+
g.Expect(err).ToNot(HaveOccurred())
61+
port := strconv.Itoa(l.Addr().(*net.TCPAddr).Port)
62+
g.Expect(l.Close()).ToNot(HaveOccurred())
63+
64+
// Use a fresh private Prometheus registry per test to avoid duplicate
65+
// metric registration panics when subtests each create a new server.
66+
mdlw := middleware.New(middleware.Config{
67+
Recorder: prommetrics.NewRecorder(prommetrics.Config{
68+
Prefix: "gotk_receiver_test",
69+
Registry: prometheus.NewRegistry(),
70+
}),
71+
})
72+
73+
srv := NewReceiverServer(
74+
"127.0.0.1:"+port,
75+
log.Log,
76+
kc,
77+
false,
78+
exportHTTPPathMetrics,
79+
)
80+
stopCh := make(chan struct{})
81+
go srv.ListenAndServe(stopCh, mdlw)
82+
83+
// Wait until the server is ready.
84+
baseURL := "http://127.0.0.1:" + port
85+
g.Eventually(func() error {
86+
resp, err := http.Get(baseURL + "/")
87+
if err != nil {
88+
return err
89+
}
90+
resp.Body.Close()
91+
return nil
92+
}, 5*time.Second, 100*time.Millisecond).Should(Succeed())
93+
94+
return srv, baseURL, stopCh
95+
}
96+
97+
func TestNewReceiverServer(t *testing.T) {
98+
g := NewWithT(t)
99+
100+
scheme := runtime.NewScheme()
101+
g.Expect(corev1.AddToScheme(scheme)).To(Succeed())
102+
g.Expect(apiv1.AddToScheme(scheme)).To(Succeed())
103+
104+
kc := fakeclient.NewClientBuilder().WithScheme(scheme).Build()
105+
106+
srv := NewReceiverServer(":9292", log.Log, kc, true, true)
107+
g.Expect(srv).ToNot(BeNil())
108+
g.Expect(srv.port).To(Equal(":9292"))
109+
g.Expect(srv.kubeClient).ToNot(BeNil())
110+
g.Expect(srv.noCrossNamespaceRefs).To(BeTrue())
111+
g.Expect(srv.exportHTTPPathMetrics).To(BeTrue())
112+
}
113+
114+
func TestReceiverServer_ListenAndServe(t *testing.T) {
115+
tests := []struct {
116+
name string
117+
path string
118+
exportHTTPPathMetrics bool
119+
wantStatus int
120+
}{
121+
{
122+
name: "unknown hook path returns 404",
123+
path: apiv1.ReceiverWebhookPath + "unknowntoken",
124+
wantStatus: http.StatusNotFound,
125+
},
126+
{
127+
name: "non-hook path returns 404",
128+
path: "/healthz",
129+
wantStatus: http.StatusNotFound,
130+
},
131+
{
132+
name: "unknown hook path with exportHTTPPathMetrics returns 404",
133+
path: apiv1.ReceiverWebhookPath + "unknowntoken",
134+
exportHTTPPathMetrics: true,
135+
wantStatus: http.StatusNotFound,
136+
},
137+
}
138+
139+
for _, tt := range tests {
140+
t.Run(tt.name, func(t *testing.T) {
141+
g := NewWithT(t)
142+
143+
_, baseURL, stopCh := newTestReceiverServer(t, tt.exportHTTPPathMetrics)
144+
defer close(stopCh)
145+
146+
resp, err := http.Post(baseURL+tt.path, "application/json", nil)
147+
g.Expect(err).ToNot(HaveOccurred())
148+
resp.Body.Close()
149+
g.Expect(resp.StatusCode).To(Equal(tt.wantStatus))
150+
})
151+
}
152+
}
153+
154+
func TestReceiverServer_Shutdown(t *testing.T) {
155+
g := NewWithT(t)
156+
157+
_, baseURL, stopCh := newTestReceiverServer(t, false)
158+
159+
// Server is up.
160+
resp, err := http.Get(baseURL + "/")
161+
g.Expect(err).ToNot(HaveOccurred())
162+
resp.Body.Close()
163+
164+
// Signal shutdown.
165+
close(stopCh)
166+
167+
// Server should stop accepting connections.
168+
g.Eventually(func() error {
169+
_, err := http.Get(baseURL + "/")
170+
return err
171+
}, 5*time.Second, 100*time.Millisecond).ShouldNot(Succeed())
172+
}
173+
174+
func TestReceiverServer_WebhookPathRouting(t *testing.T) {
175+
g := NewWithT(t)
176+
177+
// Create a Receiver with a known token so handlePayload can look it up.
178+
token := "test-token-abc123"
179+
secret := &corev1.Secret{
180+
ObjectMeta: metav1.ObjectMeta{
181+
Name: "receiver-token",
182+
Namespace: "default",
183+
},
184+
Data: map[string][]byte{
185+
"token": []byte(token),
186+
},
187+
}
188+
receiver := &apiv1.Receiver{
189+
ObjectMeta: metav1.ObjectMeta{
190+
Name: "test-receiver",
191+
Namespace: "default",
192+
},
193+
Spec: apiv1.ReceiverSpec{
194+
Type: apiv1.GenericReceiver,
195+
SecretRef: meta.LocalObjectReference{
196+
Name: "receiver-token",
197+
},
198+
},
199+
Status: apiv1.ReceiverStatus{
200+
WebhookPath: apiv1.ReceiverWebhookPath + token,
201+
},
202+
}
203+
204+
_, baseURL, stopCh := newTestReceiverServer(t, false, secret, receiver)
205+
defer close(stopCh)
206+
207+
// A POST to the webhook path is dispatched to handlePayload (not 404).
208+
resp, err := http.Post(baseURL+apiv1.ReceiverWebhookPath+token, "application/json", nil)
209+
g.Expect(err).ToNot(HaveOccurred())
210+
resp.Body.Close()
211+
// handlePayload returns 200 on success for GenericReceiver; any non-404 means routing worked.
212+
g.Expect(resp.StatusCode).ToNot(Equal(http.StatusNotFound))
213+
}

0 commit comments

Comments
 (0)