Rewrite tests to run without a database

- Unit tests use an in-memory fake database (internal/testutil/fakedb),
  temp folders and httptest; `go test ./...` needs no Docker or env vars
- Integration tests (build tag "integration") start PostgreSQL with
  testcontainers: migrations step by step, SQL dumps (skipped if
  missing) and a full sync against a real database
- Removed tests that only checked copies of the logic or had empty
  bodies, the broken root integration_test.go, compose.test.yaml and
  .env.test with hardcoded paths
- testMusic MP3s replaced with tiny placeholders (sync never decodes
  audio), 194 MB -> 168 KB
This commit is contained in:
Sansan committed 2026-10-06 15:20:31 +02:00
1 parent 090e794460
commit 7b144ae419
42 files changed
+2334 -40

No files matched your search

+283
View File
@@ -0,0 +1,283 @@
package server
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"testing"
"time"
"music-server/internal/backend"
"music-server/internal/config"
"music-server/internal/db/repository"
"github.com/jackc/pgx/v5/pgtype"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMusicRandomAndQueue(t *testing.T) {
e := newTestEnv(t)
e.addSong("Game A", "Song", "0123456789")
rec := e.do(http.MethodGet, "/music/rand")
require.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "audio/mpeg", rec.Header().Get("Content-Type"))
assert.Equal(t, "0123456789", rec.Body.String())
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/music/addQue").Code)
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/music/addPlayed").Code)
var info backend.SongInfo
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/music/info").Body.Bytes(), &info))
assert.Equal(t, "Game A", info.Soundtrack)
assert.Equal(t, "Song", info.Song)
for _, path := range []string{"/music/next", "/music/previous", "/music?song=0", "/api/v1/music?song=0"} {
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, path).Code, path)
}
assert.Equal(t, http.StatusOK, e.do(http.MethodPut, "/music/played?song=0").Code)
assert.Equal(t, http.StatusBadRequest, e.do(http.MethodPut, "/music/played?song=x").Code)
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/music/reset").Code)
}
func TestMusicRangeRequest(t *testing.T) {
e := newTestEnv(t)
e.addSong("Game A", "Song", "0123456789")
rec := e.do(http.MethodGet, "/music/rand", "Range", "bytes=2-5")
assert.Equal(t, http.StatusPartialContent, rec.Code)
assert.Equal(t, "bytes 2-5/10", rec.Header().Get("Content-Range"))
assert.Equal(t, "2345", rec.Body.String())
assert.Equal(t, "audio/mpeg", rec.Header().Get("Content-Type"))
}
func TestMusicEmptyStates(t *testing.T) {
e := newTestEnv(t)
for _, path := range []string{"/music/next", "/music/previous", "/music?song=0", "/music/rand", "/music/rand/low", "/music/rand/classic"} {
assert.Equal(t, http.StatusNotFound, e.do(http.MethodGet, path).Code, path)
}
assert.Equal(t, http.StatusBadRequest, e.do(http.MethodGet, "/music?song=abc").Code)
assert.Equal(t, http.StatusBadRequest, e.do(http.MethodGet, "/music").Code)
rec := e.do(http.MethodGet, "/music/list")
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "null\n", rec.Body.String())
}
func TestMusicSoundCheck(t *testing.T) {
e := newTestEnv(t)
rec := e.do(http.MethodGet, "/music/soundTest")
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "sound check", rec.Body.String())
}
func TestMusicSoundtrackLists(t *testing.T) {
e := newTestEnv(t)
e.addSong("B Game", "Song", "b")
e.addSong("A Game", "Song", "a")
var ordered []string
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/music/all/order").Body.Bytes(), &ordered))
assert.Equal(t, []string{"A Game", "B Game"}, ordered)
var random []string
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/music/all").Body.Bytes(), &random))
assert.ElementsMatch(t, ordered, random)
}
func TestMusicDatabaseError(t *testing.T) {
e := newTestEnv(t)
e.store.Err = errors.New("db down")
assert.Equal(t, http.StatusInternalServerError, e.do(http.MethodGet, "/music/rand").Code)
assert.Equal(t, http.StatusInternalServerError, e.do(http.MethodGet, "/music/all/order").Code)
}
func TestCharacterEndpoints(t *testing.T) {
e := newTestEnv(t)
var list []string
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/characters").Body.Bytes(), &list))
assert.Equal(t, []string{"Adol.png", "Aloy.jpg"}, list)
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/api/v1/character/list").Body.Bytes(), &list))
assert.Equal(t, []string{"Adol.png", "Aloy.jpg"}, list)
rec := e.do(http.MethodGet, "/character?name=Adol.png")
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "image/png", rec.Header().Get("Content-Type"))
rec = e.do(http.MethodGet, "/api/v1/character?name=Aloy.jpg")
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "image/jpeg", rec.Header().Get("Content-Type"))
assert.Equal(t, http.StatusNotFound, e.do(http.MethodGet, "/character?name=Missing.png").Code)
}
func TestCharacterPathTraversal(t *testing.T) {
e := newTestEnv(t)
for _, name := range []string{"../secret.png", "..%2Fsecret.png", "%2Fetc%2Fpasswd", "../../etc/passwd", ""} {
rec := e.do(http.MethodGet, "/character?name="+name)
assert.Equal(t, http.StatusBadRequest, rec.Code, name)
assert.NotContains(t, rec.Body.String(), "secret", name)
}
}
func newGitea(t *testing.T, status int) string {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(status)
w.Write([]byte(`{"id":1,"name":"v2.0.0","assets":[{"name":"Player.exe","browser_download_url":"https://example.com/Player.exe"}]}`))
}))
t.Cleanup(server.Close)
return server.URL
}
func TestDownloadEndpoints(t *testing.T) {
url := newGitea(t, http.StatusOK)
e := newTestEnv(t, func(c *config.Config) { c.ReleasesURL = url })
rec := e.do(http.MethodGet, "/download")
assert.Equal(t, http.StatusOK, rec.Code)
assert.JSONEq(t, `"v2.0.0"`, rec.Body.String())
rec = e.do(http.MethodGet, "/api/v1/download/list")
assert.JSONEq(t, `["Player.exe"]`, rec.Body.String())
rec = e.do(http.MethodGet, "/download/windows")
assert.Equal(t, http.StatusFound, rec.Code)
assert.Equal(t, "https://example.com/Player.exe", rec.Header().Get("Location"))
assert.Equal(t, http.StatusNotFound, e.do(http.MethodGet, "/download/linux").Code)
}
func TestDownloadUpstreamDown(t *testing.T) {
e := newTestEnv(t, func(c *config.Config) { c.ReleasesURL = newGitea(t, http.StatusServiceUnavailable) })
for _, path := range []string{"/download", "/download/list", "/download/windows"} {
assert.Equal(t, http.StatusBadGateway, e.do(http.MethodGet, path).Code, path)
}
}
func withAuth(password string) func(*config.Config) {
return func(c *config.Config) {
c.DisableSessionAuth = false
c.APIPassword = password
}
}
func (e *testEnv) createToken(password string) string {
e.t.Helper()
rec := e.doBody(http.MethodPost, "/api/v1/token", `{"password":"`+password+`"}`)
require.Equal(e.t, http.StatusOK, rec.Code, rec.Body.String())
var resp TokenResponse
require.NoError(e.t, json.Unmarshal(rec.Body.Bytes(), &resp))
assert.Equal(e.t, "web", resp.ClientType)
assert.WithinDuration(e.t, time.Now().Add(tokenLifetime), resp.ExpiresAt, time.Minute)
return resp.Token
}
func TestTokenAuth(t *testing.T) {
e := newTestEnv(t, withAuth("secret"))
assert.Equal(t, http.StatusUnauthorized, e.do(http.MethodGet, "/api/v1/version").Code)
assert.Equal(t, http.StatusUnauthorized, e.do(http.MethodGet, "/api/v1/version", "Authorization", "Basic abc").Code)
assert.Equal(t, http.StatusUnauthorized, e.do(http.MethodGet, "/api/v1/version", "Authorization", "Bearer unknown").Code)
assert.Equal(t, http.StatusUnauthorized, e.doBody(http.MethodPost, "/api/v1/token", `{"password":"wrong"}`).Code)
assert.Equal(t, http.StatusBadRequest, e.doBody(http.MethodPost, "/api/v1/token", `{not json`).Code)
token := e.createToken("secret")
auth := []string{"Authorization", "Bearer " + token}
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/api/v1/version", auth...).Code)
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/version").Code, "legacy routes need no token")
assert.Equal(t, http.StatusOK, e.do(http.MethodDelete, "/api/v1/token", auth...).Code)
assert.Equal(t, http.StatusUnauthorized, e.do(http.MethodGet, "/api/v1/version", auth...).Code)
assert.Equal(t, http.StatusUnauthorized, e.do(http.MethodDelete, "/api/v1/token").Code)
}
func TestTokenOpenAccessWithoutPassword(t *testing.T) {
e := newTestEnv(t, withAuth(""))
token := e.createToken("anything")
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/api/v1/version", "Authorization", "Bearer "+token).Code)
}
func TestTokenCleanupRequiresAuth(t *testing.T) {
e := newTestEnv(t, withAuth("secret"))
assert.Equal(t, http.StatusUnauthorized, e.do(http.MethodPost, "/api/v1/token/cleanup").Code)
token := e.createToken("secret")
e.store.Sessions["old"] = repository.Session{Token: "old", ExpiresAt: pgtype.Timestamptz{Time: time.Now().Add(-time.Hour), Valid: true}}
assert.Equal(t, http.StatusOK, e.do(http.MethodPost, "/api/v1/token/cleanup", "Authorization", "Bearer "+token).Code)
assert.False(t, e.store.HasSession("old"))
assert.True(t, e.store.HasSession(token))
}
func TestExpiredTokenIsDeleted(t *testing.T) {
e := newTestEnv(t, withAuth("secret"))
_, err := e.store.CreateSession(context.Background(), repository.CreateSessionParams{
Token: "expired",
ExpiresAt: pgtype.Timestamptz{Time: time.Now().Add(-time.Minute), Valid: true},
})
require.NoError(t, err)
rec := e.do(http.MethodGet, "/api/v1/version", "Authorization", "Bearer expired")
assert.Equal(t, http.StatusUnauthorized, rec.Code)
assert.Contains(t, rec.Body.String(), "Token expired")
// The fake refuses to delete with a cancelled context, like a real database would.
assert.Eventually(t, func() bool { return !e.store.HasSession("expired") }, time.Second, 10*time.Millisecond)
}
func TestTokenRateLimit(t *testing.T) {
e := newTestEnv(t)
for range 10 {
assert.Equal(t, http.StatusOK, e.doBody(http.MethodPost, "/api/v1/token", `{}`).Code)
}
rec := e.doBody(http.MethodPost, "/api/v1/token", `{}`)
assert.Equal(t, http.StatusTooManyRequests, rec.Code)
assert.Contains(t, rec.Body.String(), "Rate limit exceeded")
}
// statsStore returns canned statistics.
type statsStore struct {
repository.Querier
err error
}
func (s statsStore) GetMostPlayedSoundtracksWithSongs(_ context.Context, limit int32) ([]repository.GetMostPlayedSoundtracksWithSongsRow, error) {
return []repository.GetMostPlayedSoundtracksWithSongsRow{{
SoundtrackID: int32(limit),
SoundtrackName: "Game A",
Songs: []byte(`[{"song_name":"Song","times_played":3}]`),
}}, s.err
}
func (s statsStore) GetStatisticsSummary(context.Context) (repository.GetStatisticsSummaryRow, error) {
return repository.GetStatisticsSummaryRow{TotalSoundtracks: 5, AvgSoundtrackPlays: 1.5}, s.err
}
func TestStatisticsEndpoints(t *testing.T) {
e := newTestEnv(t)
e.server.stats = backend.NewStatisticsService(statsStore{})
e.handler = e.server.RegisterRoutes()
var soundtracks []backend.SoundtrackWithSongs
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/api/v1/statistics/soundtracks/most-played").Body.Bytes(), &soundtracks))
require.Len(t, soundtracks, 1)
assert.Equal(t, int32(10), soundtracks[0].SoundtrackID, "default limit")
assert.Equal(t, "Song", soundtracks[0].Songs[0].SongName)
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/api/v1/statistics/soundtracks/most-played?limit=500").Body.Bytes(), &soundtracks))
assert.Equal(t, int32(100), soundtracks[0].SoundtrackID, "limit is capped")
for _, limit := range []string{"0", "-1", "abc"} {
assert.Equal(t, http.StatusBadRequest, e.do(http.MethodGet, "/api/v1/statistics/soundtracks/most-played?limit="+limit).Code, limit)
}
var summary backend.StatisticsSummary
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/api/v1/statistics/summary").Body.Bytes(), &summary))
assert.Equal(t, backend.StatisticsSummary{TotalSoundtracks: 5, AvgSoundtrackPlays: 1.5}, summary)
e.server.stats = backend.NewStatisticsService(statsStore{err: errors.New("db down")})
e.handler = e.server.RegisterRoutes()
assert.Equal(t, http.StatusInternalServerError, e.do(http.MethodGet, "/api/v1/statistics/summary").Code)
}
+120
View File
@@ -0,0 +1,120 @@
package server
import (
"context"
"io"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"sync/atomic"
"testing"
"music-server/internal/config"
"music-server/internal/db/repository"
"music-server/internal/logging"
"music-server/internal/testutil/fakedb"
"github.com/stretchr/testify/require"
)
func TestMain(m *testing.M) {
logging.Init("fatal", false)
os.Exit(m.Run())
}
type fakeDatabase struct {
closed atomic.Bool
onClose func()
}
func (f *fakeDatabase) Health(context.Context) map[string]string {
return map[string]string{"status": "up"}
}
func (f *fakeDatabase) Close() {
if f.onClose != nil {
f.onClose()
}
f.closed.Store(true)
}
// testEnv is a Server backed by an in-memory database and temporary folders.
type testEnv struct {
t *testing.T
cfg config.Config
store *fakedb.Store
db *fakeDatabase
server *Server
handler http.Handler
}
func writeFile(t *testing.T, path, content string) {
t.Helper()
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
}
func newTestEnv(t *testing.T, configure ...func(*config.Config)) *testEnv {
t.Helper()
base := t.TempDir()
cfg := config.Config{
Port: 0,
MusicPath: filepath.Join(base, "music"),
CharactersPath: filepath.Join(base, "characters"),
SoundCheckPath: filepath.Join(base, "songs"),
FrontendPath: filepath.Join(base, "frontend"),
DisableSessionAuth: true,
ReleasesURL: "http://127.0.0.1:1",
}
for _, c := range configure {
c(&cfg)
}
writeFile(t, filepath.Join(cfg.CharactersPath, "Adol.png"), "png")
writeFile(t, filepath.Join(cfg.CharactersPath, "Aloy.jpg"), "jpg")
writeFile(t, filepath.Join(base, "secret.png"), "secret")
writeFile(t, filepath.Join(cfg.SoundCheckPath, "check.mp3"), "sound check")
writeFile(t, filepath.Join(cfg.FrontendPath, "index.html"), "<html>frontend</html>")
store := fakedb.New()
database := &fakeDatabase{}
s, err := New(cfg, store, database)
require.NoError(t, err)
t.Cleanup(func() { s.sync.Wait(); s.characters.Close() })
return &testEnv{t: t, cfg: cfg, store: store, db: database, server: s, handler: s.httpServer.Handler}
}
// addSong creates a song file and its database rows.
func (e *testEnv) addSong(soundtrack, song, content string) repository.Song {
e.t.Helper()
dir := e.cfg.MusicPath + "/" + soundtrack + "/"
fileName := song + ".mp3"
writeFile(e.t, dir+fileName, content)
st, ok := e.store.Soundtrack(soundtrack)
if !ok {
st = e.store.AddSoundtrack(repository.Soundtrack{SoundtrackName: soundtrack, Path: dir})
}
return e.store.AddTestSong(repository.Song{SoundtrackID: st.ID, SongName: song, Path: dir + fileName, FileName: &fileName})
}
func (e *testEnv) do(method, path string, headers ...string) *httptest.ResponseRecorder {
return e.doBody(method, path, "", headers...)
}
func (e *testEnv) doBody(method, path, body string, headers ...string) *httptest.ResponseRecorder {
var reader io.Reader
if body != "" {
reader = strings.NewReader(body)
}
req := httptest.NewRequest(method, path, reader)
if body != "" {
req.Header.Set("Content-Type", "application/json")
}
for i := 0; i+1 < len(headers); i += 2 {
req.Header.Set(headers[i], headers[i+1])
}
rec := httptest.NewRecorder()
e.handler.ServeHTTP(rec, req)
return rec
}
+191
View File
@@ -0,0 +1,191 @@
package server
import (
"context"
"encoding/json"
"net"
"net/http"
"sync/atomic"
"testing"
"time"
"music-server/internal/backend"
"music-server/internal/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// blockSync replaces the sync service with one that keeps running until the returned function is called.
func (e *testEnv) blockSync() (release func()) {
ch := make(chan struct{})
e.server.sync = backend.NewSyncService(e.store, e.cfg.MusicPath, func(context.Context) { <-ch })
e.handler = e.server.RegisterRoutes()
return func() { close(ch); e.server.sync.Wait() }
}
func TestDestructiveSyncEndpointsIgnoreGet(t *testing.T) {
e := newTestEnv(t)
e.addSong("Game A", "Song", "a")
for _, prefix := range []string{"", "/api/v1"} {
for _, path := range []string{"/sync", "/sync/new", "/sync/quick", "/sync/full", "/sync/new/full", "/sync/reset"} {
// GET falls through to the frontend's static files, which answer 404.
assert.Equal(t, http.StatusNotFound, e.do(http.MethodGet, prefix+path).Code, prefix+path)
}
}
assert.False(t, e.server.sync.IsSyncing(), "no sync was started")
assert.Len(t, e.store.Songs, 1, "nothing was deleted")
}
func TestSyncStartAndLock(t *testing.T) {
for _, prefix := range []string{"", "/api/v1"} {
t.Run("prefix="+prefix, func(t *testing.T) {
e := newTestEnv(t)
e.addSong("Game A", "Song", "a")
release := e.blockSync()
assert.Equal(t, http.StatusOK, e.do(http.MethodPost, prefix+"/sync").Code)
assert.Equal(t, http.StatusLocked, e.do(http.MethodPost, prefix+"/sync/full").Code)
assert.Equal(t, http.StatusLocked, e.do(http.MethodPost, prefix+"/sync/reset").Code)
assert.Equal(t, http.StatusLocked, e.do(http.MethodGet, prefix+"/music/rand").Code)
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, prefix+"/music/info").Code, "info is available during sync")
progress := e.do(http.MethodGet, prefix+"/sync/progress")
assert.Equal(t, http.StatusOK, progress.Code)
assert.Contains(t, progress.Body.String(), `"progress"`)
release()
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, prefix+"/music/all").Code)
result := e.do(http.MethodGet, prefix+"/sync/progress")
assert.Contains(t, result.Body.String(), `"total_time"`)
})
}
}
func TestSyncResetEndpoint(t *testing.T) {
e := newTestEnv(t)
e.addSong("Game A", "Song", "a")
assert.Equal(t, http.StatusOK, e.do(http.MethodPost, "/sync/reset").Code)
assert.Empty(t, e.store.Soundtracks)
assert.Empty(t, e.store.Songs)
}
func TestLegacyRoutesAreDeprecated(t *testing.T) {
e := newTestEnv(t)
rec := e.do(http.MethodGet, "/version")
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "true", rec.Header().Get("Deprecation"))
rec = e.do(http.MethodGet, "/api/v1/version")
assert.Equal(t, http.StatusOK, rec.Code)
assert.Empty(t, rec.Header().Get("Deprecation"))
}
func TestVersionEndpoints(t *testing.T) {
e := newTestEnv(t)
var latest backend.VersionData
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/version").Body.Bytes(), &latest))
assert.Equal(t, backend.GetLatestVersion(), latest)
var history []backend.VersionData
require.NoError(t, json.Unmarshal(e.do(http.MethodGet, "/api/v1/version/history").Body.Bytes(), &history))
assert.Equal(t, backend.GetVersionHistory(), history)
}
func TestHealthEndpoint(t *testing.T) {
e := newTestEnv(t)
rec := e.do(http.MethodGet, "/health")
assert.Equal(t, http.StatusOK, rec.Code)
assert.JSONEq(t, `{"status":"up"}`, rec.Body.String())
}
func TestOpenAPIAndSwagger(t *testing.T) {
e := newTestEnv(t)
for _, path := range []string{"/openapi", "/swagger/doc.json"} {
rec := e.do(http.MethodGet, path)
require.Equal(t, http.StatusOK, rec.Code, path)
var spec struct {
Paths map[string]map[string]any `json:"paths"`
}
require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &spec), path)
assert.Contains(t, spec.Paths, "/api/v1/token", path)
assert.Contains(t, spec.Paths["/sync/reset"], "post", path)
}
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/swagger/index.html").Code)
}
func TestSearchPageAndFrontend(t *testing.T) {
e := newTestEnv(t)
e.addSong("Kingdom Hearts", "Dearly Beloved", "x")
assert.Equal(t, http.StatusOK, e.do(http.MethodGet, "/search").Code)
rec := e.do(http.MethodPost, "/find")
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "Kingdom Hearts")
rec = e.do(http.MethodGet, "/")
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "frontend")
}
func TestShutdownWaitsForRequestsBeforeClosingDatabase(t *testing.T) {
e := newTestEnv(t)
started := make(chan struct{})
release := make(chan struct{})
var requestDone atomic.Bool
e.server.httpServer.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
close(started)
<-release
requestDone.Store(true)
})
var closedAfterRequest atomic.Bool
e.db.onClose = func() { closedAfterRequest.Store(requestDone.Load()) }
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
go e.server.httpServer.Serve(ln)
responded := make(chan error, 1)
go func() {
resp, err := http.Get("http://" + ln.Addr().String())
if err == nil {
resp.Body.Close()
}
responded <- err
}()
<-started
shutdownDone := make(chan error, 1)
go func() { shutdownDone <- e.server.Shutdown(context.Background()) }()
time.Sleep(50 * time.Millisecond)
assert.False(t, e.db.closed.Load(), "database must stay open while a request is running")
close(release)
require.NoError(t, <-responded)
require.NoError(t, <-shutdownDone)
assert.True(t, e.db.closed.Load())
assert.True(t, closedAfterRequest.Load())
}
func TestTrustedProxies(t *testing.T) {
// Without trusted proxies the forwarded header is ignored, so all requests share one bucket.
e := newTestEnv(t)
for i := range 10 {
e.doBody(http.MethodPost, "/api/v1/token", `{}`, "X-Forwarded-For", "10.0.0."+string(rune('0'+i)))
}
assert.Equal(t, http.StatusTooManyRequests, e.doBody(http.MethodPost, "/api/v1/token", `{}`, "X-Forwarded-For", "10.0.1.1").Code)
// httptest requests come from 192.0.2.1, which is trusted here.
e = newTestEnv(t, func(c *config.Config) {
_, trusted, _ := net.ParseCIDR("192.0.2.0/24")
c.TrustedProxies = []*net.IPNet{trusted}
})
for range 10 {
e.doBody(http.MethodPost, "/api/v1/token", `{}`, "X-Forwarded-For", "10.0.0.1")
}
assert.Equal(t, http.StatusTooManyRequests, e.doBody(http.MethodPost, "/api/v1/token", `{}`, "X-Forwarded-For", "10.0.0.1").Code)
assert.Equal(t, http.StatusOK, e.doBody(http.MethodPost, "/api/v1/token", `{}`, "X-Forwarded-For", "10.0.0.2").Code)
}