~bigbes/lethe

ref: 0b51b8ee59a86f13b764e305ebffa0c60507ec12 lethe/internal/domain/ingest/handler_test.go -rw-r--r-- 5.5 KiB
0b51b8ee — Eugene Blikh web: shell, theme, keyboard, stub routes, palette skeleton a month ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
package ingest_test

import (
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/go-chi/chi/v5"

	"sourcecraft.dev/bigbes/lethe/internal/config"
	"sourcecraft.dev/bigbes/lethe/internal/domain/ingest"
	"sourcecraft.dev/bigbes/lethe/internal/server/auth"
)

// fakeAuthMiddleware injects a fixed Identity onto the request context so the
// handler under test can call auth.MustIdentity without a real Authenticator.
func fakeAuthMiddleware(user string) func(http.Handler) http.Handler {
	return func(next http.Handler) http.Handler {
		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
			ctx := auth.WithIdentity(r.Context(), auth.Identity{User: user})
			next.ServeHTTP(w, r.WithContext(ctx))
		})
	}
}

// newHandler wires Service + Repository + observability + handler against a
// fresh in-memory database, then returns the handler.
func newHandler(t *testing.T, cfg config.IngestConfig) *ingest.Handler {
	t.Helper()
	svc, _ := newService(t, cfg)
	h := &ingest.Handler{Cfg: cfg, Service: svc}
	if err := h.Init(context.Background()); err != nil {
		t.Fatalf("handler.Init: %v", err)
	}
	return h
}

// mountHandler builds a chi router with the fake auth middleware and the
// ingest handler mounted under /api/v1.
func mountHandler(h *ingest.Handler, user string) http.Handler {
	r := chi.NewRouter()
	r.Route("/api/v1", func(r chi.Router) {
		r.Use(fakeAuthMiddleware(user))
		h.Mount(r)
	})
	return r
}

func TestHandler_Post_HappyPath(t *testing.T) {
	h := newHandler(t, defaultCfg())
	router := mountHandler(h, "alice")

	payload := strings.Join([]string{validLine("t1", 1700000000, "x"), validLine("t2", 1700000001, "y")}, "\n") + "\n"
	req := httptest.NewRequest(http.MethodPost, "/api/v1/ingest", strings.NewReader(payload))
	req.Header.Set("Content-Type", "application/x-ndjson")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
	}
	var got ingest.Result
	if err := json.Unmarshal(rec.Body.Bytes(), &got); err != nil {
		t.Fatalf("unmarshal: %v", err)
	}
	if got.Accepted != 2 {
		t.Errorf("Accepted=%d; want 2", got.Accepted)
	}
	if len(got.Errors) != 0 {
		t.Errorf("Errors=%v; want empty", got.Errors)
	}
}

func TestHandler_Post_WrongContentTypeReturns415(t *testing.T) {
	h := newHandler(t, defaultCfg())
	router := mountHandler(h, "alice")

	req := httptest.NewRequest(http.MethodPost, "/api/v1/ingest",
		strings.NewReader(validLine("t1", 1700000000, "x")+"\n"))
	req.Header.Set("Content-Type", "application/json")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusUnsupportedMediaType {
		t.Fatalf("status=%d; want 415; body=%s", rec.Code, rec.Body.String())
	}
	if ct := rec.Header().Get("Content-Type"); ct != "application/problem+json" {
		t.Errorf("Content-Type=%q; want application/problem+json", ct)
	}
}

func TestHandler_Post_MissingContentTypeReturns415(t *testing.T) {
	h := newHandler(t, defaultCfg())
	router := mountHandler(h, "alice")

	req := httptest.NewRequest(http.MethodPost, "/api/v1/ingest",
		strings.NewReader(validLine("t1", 1700000000, "x")+"\n"))
	// No Content-Type set.
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusUnsupportedMediaType {
		t.Fatalf("status=%d; want 415", rec.Code)
	}
}

func TestHandler_Post_BodyOverCapReturns413(t *testing.T) {
	cfg := defaultCfg()
	cfg.MaxBodyBytes = 100
	h := newHandler(t, cfg)
	router := mountHandler(h, "alice")

	// 1 KiB+ body that exceeds the cap.
	big := validLine("t1", 1700000000, strings.Repeat("a", 1024))
	req := httptest.NewRequest(http.MethodPost, "/api/v1/ingest", strings.NewReader(big+"\n"))
	req.Header.Set("Content-Type", "application/x-ndjson")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusRequestEntityTooLarge {
		t.Fatalf("status=%d; want 413; body=%s", rec.Code, rec.Body.String())
	}
}

func TestHandler_Post_DBDownReturns5xx(t *testing.T) {
	cfg := defaultCfg()
	svc, db := newService(t, cfg)
	h := &ingest.Handler{Cfg: cfg, Service: svc}
	if err := h.Init(context.Background()); err != nil {
		t.Fatalf("handler.Init: %v", err)
	}
	// Close the underlying *sql.DB without nilling the field so BeginTx
	// returns an error rather than nil-dereferencing. This mirrors the
	// runtime "database closed" failure mode (e.g. driver disconnect) more
	// accurately than calling Destroy.
	if err := db.DB.Close(); err != nil {
		t.Fatalf("close underlying db: %v", err)
	}
	router := mountHandler(h, "alice")

	req := httptest.NewRequest(http.MethodPost, "/api/v1/ingest",
		strings.NewReader(validLine("t1", 1700000000, "x")+"\n"))
	req.Header.Set("Content-Type", "application/x-ndjson")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code < 500 {
		t.Fatalf("status=%d; want 5xx; body=%s", rec.Code, rec.Body.String())
	}
}

// TestHandler_Post_ContentTypeWithCharset proves the canonical media check
// tolerates the `; charset=utf-8` parameter the collector may attach.
func TestHandler_Post_ContentTypeWithCharset(t *testing.T) {
	h := newHandler(t, defaultCfg())
	router := mountHandler(h, "alice")

	req := httptest.NewRequest(http.MethodPost, "/api/v1/ingest",
		strings.NewReader(validLine("t1", 1700000000, "x")+"\n"))
	req.Header.Set("Content-Type", "application/x-ndjson; charset=utf-8")
	rec := httptest.NewRecorder()
	router.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("status=%d; want 200; body=%s", rec.Code, rec.Body.String())
	}
}