Skip to content

Commit 9929a16

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. Signed-off-by: Hamza Younas <hamza.younas94@gmail.com>
1 parent 811fc8a commit 9929a16

1 file changed

Lines changed: 220 additions & 0 deletions

File tree

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
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+
apimeta "github.com/fluxcd/pkg/apis/meta"
37+
"github.com/fluxcd/pkg/apis/meta"
38+
39+
apiv1 "github.com/fluxcd/notification-controller/api/v1"
40+
)
41+
42+
// newTestReceiverServer creates a ReceiverServer listening on a free port and
43+
// returns the server, its base URL and a stop channel. The caller must close
44+
// the stop channel to shut the server down.
45+
func newTestReceiverServer(t *testing.T, exportHTTPPathMetrics bool, objs ...runtime.Object) (*ReceiverServer, string, chan struct{}) {
46+
t.Helper()
47+
g := NewWithT(t)
48+
49+
scheme := runtime.NewScheme()
50+
g.Expect(corev1.AddToScheme(scheme)).To(Succeed())
51+
g.Expect(apiv1.AddToScheme(scheme)).To(Succeed())
52+
53+
builder := fakeclient.NewClientBuilder().WithScheme(scheme).
54+
WithIndex(&apiv1.Receiver{}, WebhookPathIndexKey, IndexReceiverWebhookPath)
55+
for _, o := range objs {
56+
builder = builder.WithRuntimeObjects(o)
57+
}
58+
kc := builder.Build()
59+
60+
l, err := net.Listen("tcp", "127.0.0.1:0")
61+
g.Expect(err).ToNot(HaveOccurred())
62+
port := strconv.Itoa(l.Addr().(*net.TCPAddr).Port)
63+
g.Expect(l.Close()).ToNot(HaveOccurred())
64+
65+
// Use a fresh private Prometheus registry per test to avoid duplicate
66+
// metric registration panics when subtests each create a new server.
67+
mdlw := middleware.New(middleware.Config{
68+
Recorder: prommetrics.NewRecorder(prommetrics.Config{
69+
Prefix: "gotk_receiver_test",
70+
Registry: prometheus.NewRegistry(),
71+
}),
72+
})
73+
74+
srv := NewReceiverServer(
75+
"127.0.0.1:"+port,
76+
log.Log,
77+
kc,
78+
false,
79+
exportHTTPPathMetrics,
80+
)
81+
stopCh := make(chan struct{})
82+
go srv.ListenAndServe(stopCh, mdlw)
83+
84+
// Wait until the server is ready.
85+
baseURL := "http://127.0.0.1:" + port
86+
g.Eventually(func() error {
87+
resp, err := http.Get(baseURL + "/")
88+
if err != nil {
89+
return err
90+
}
91+
resp.Body.Close()
92+
return nil
93+
}, 5*time.Second, 100*time.Millisecond).Should(Succeed())
94+
95+
return srv, baseURL, stopCh
96+
}
97+
98+
func TestNewReceiverServer(t *testing.T) {
99+
g := NewWithT(t)
100+
101+
scheme := runtime.NewScheme()
102+
g.Expect(corev1.AddToScheme(scheme)).To(Succeed())
103+
g.Expect(apiv1.AddToScheme(scheme)).To(Succeed())
104+
105+
kc := fakeclient.NewClientBuilder().WithScheme(scheme).Build()
106+
107+
srv := NewReceiverServer(":9292", log.Log, kc, true, true)
108+
g.Expect(srv).ToNot(BeNil())
109+
g.Expect(srv.port).To(Equal(":9292"))
110+
g.Expect(srv.kubeClient).ToNot(BeNil())
111+
g.Expect(srv.noCrossNamespaceRefs).To(BeTrue())
112+
g.Expect(srv.exportHTTPPathMetrics).To(BeTrue())
113+
}
114+
115+
func TestReceiverServer_ListenAndServe(t *testing.T) {
116+
tests := []struct {
117+
name string
118+
path string
119+
exportHTTPPathMetrics bool
120+
wantStatus int
121+
}{
122+
{
123+
name: "unknown hook path returns 404",
124+
path: apiv1.ReceiverWebhookPath + "unknowntoken",
125+
wantStatus: http.StatusNotFound,
126+
},
127+
{
128+
name: "non-hook path returns 404",
129+
path: "/healthz",
130+
wantStatus: http.StatusNotFound,
131+
},
132+
{
133+
name: "unknown hook path with exportHTTPPathMetrics returns 404",
134+
path: apiv1.ReceiverWebhookPath + "unknowntoken",
135+
exportHTTPPathMetrics: true,
136+
wantStatus: http.StatusNotFound,
137+
},
138+
}
139+
140+
for _, tt := range tests {
141+
t.Run(tt.name, func(t *testing.T) {
142+
g := NewWithT(t)
143+
144+
_, baseURL, stopCh := newTestReceiverServer(t, tt.exportHTTPPathMetrics)
145+
defer close(stopCh)
146+
147+
resp, err := http.Post(baseURL+tt.path, "application/json", nil)
148+
g.Expect(err).ToNot(HaveOccurred())
149+
resp.Body.Close()
150+
g.Expect(resp.StatusCode).To(Equal(tt.wantStatus))
151+
})
152+
}
153+
}
154+
155+
func TestReceiverServer_Shutdown(t *testing.T) {
156+
g := NewWithT(t)
157+
158+
_, baseURL, stopCh := newTestReceiverServer(t, false)
159+
160+
// Server is up.
161+
resp, err := http.Get(baseURL + "/")
162+
g.Expect(err).ToNot(HaveOccurred())
163+
resp.Body.Close()
164+
165+
// Signal shutdown.
166+
close(stopCh)
167+
168+
// Server should stop accepting connections.
169+
g.Eventually(func() error {
170+
_, err := http.Get(baseURL + "/")
171+
return err
172+
}, 5*time.Second, 100*time.Millisecond).ShouldNot(Succeed())
173+
}
174+
175+
func TestReceiverServer_WebhookPathRouting(t *testing.T) {
176+
g := NewWithT(t)
177+
178+
// Create a Receiver with a known token so handlePayload can look it up.
179+
token := "test-token-abc123"
180+
secret := &corev1.Secret{
181+
ObjectMeta: metav1.ObjectMeta{
182+
Name: "receiver-token",
183+
Namespace: "default",
184+
},
185+
Data: map[string][]byte{
186+
"token": []byte(token),
187+
},
188+
}
189+
receiver := &apiv1.Receiver{
190+
ObjectMeta: metav1.ObjectMeta{
191+
Name: "test-receiver",
192+
Namespace: "default",
193+
},
194+
Spec: apiv1.ReceiverSpec{
195+
Type: apiv1.GenericReceiver,
196+
SecretRef: meta.LocalObjectReference{
197+
Name: "receiver-token",
198+
},
199+
},
200+
Status: apiv1.ReceiverStatus{
201+
WebhookPath: apiv1.ReceiverWebhookPath + token,
202+
Conditions: []metav1.Condition{
203+
{
204+
Type: apimeta.ReadyCondition,
205+
Status: metav1.ConditionTrue,
206+
},
207+
},
208+
},
209+
}
210+
211+
_, baseURL, stopCh := newTestReceiverServer(t, false, secret, receiver)
212+
defer close(stopCh)
213+
214+
// A POST to the webhook path with a valid token should reach handlePayload
215+
// and return 200 (GenericReceiver with Ready=True).
216+
resp, err := http.Post(baseURL+apiv1.ReceiverWebhookPath+token, "application/json", nil)
217+
g.Expect(err).ToNot(HaveOccurred())
218+
resp.Body.Close()
219+
g.Expect(resp.StatusCode).To(Equal(http.StatusOK))
220+
}

0 commit comments

Comments
 (0)