check / check (push) Waiting to run
The five counts the cache_stats table held are now Prometheus counters in memory, as fields of the cache. NewCache makes them and registers them on the registry it is given. pixad gives it Prometheus' default registry, which /metrics serves; each test gives it a new one. The Increment methods add to their cache's counters, so counting makes no database call. cache_stats, Cache.Stats, Service.Stats, CacheStats and the pending counts are gone, with their tests. The background goroutine now writes only access times, and its names say so: StartPendingAccessTimeWrites, StopPendingAccessTimeWrites and pendingaccesstimes.go. The two stop tests whose write under way was a count now use access times. Model: opus-5-5
272 lines
7.2 KiB
Go
272 lines
7.2 KiB
Go
package server
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"go.uber.org/fx"
|
|
"go.uber.org/fx/fxtest"
|
|
|
|
"sneak.berlin/go/pixa/internal/config"
|
|
"sneak.berlin/go/pixa/internal/database"
|
|
"sneak.berlin/go/pixa/internal/globals"
|
|
"sneak.berlin/go/pixa/internal/handlers"
|
|
"sneak.berlin/go/pixa/internal/healthcheck"
|
|
"sneak.berlin/go/pixa/internal/logger"
|
|
"sneak.berlin/go/pixa/internal/middleware"
|
|
"sneak.berlin/go/pixa/internal/session"
|
|
)
|
|
|
|
// logoutFormPattern extracts the CSRF token rendered into the generator
|
|
// page's logout form.
|
|
var logoutFormPattern = regexp.MustCompile(
|
|
`(?s)action="/logout".*?name="gorilla\.csrf\.Token" value="([^"]+)"`)
|
|
|
|
// startPixa starts the components pixad's fx app builds, from cfg, and
|
|
// returns the server with its routes, as newTestServer does.
|
|
func startPixa(t *testing.T, cfg *config.Config) *Server {
|
|
t.Helper()
|
|
|
|
var (
|
|
h *handlers.Handlers
|
|
mw *middleware.Middleware
|
|
)
|
|
|
|
app := fxtest.New(t,
|
|
fx.Supply(cfg),
|
|
fx.Provide(
|
|
globals.New,
|
|
logger.New,
|
|
database.New,
|
|
healthcheck.New,
|
|
handlers.New,
|
|
middleware.New,
|
|
func() prometheus.Registerer { return prometheus.NewRegistry() },
|
|
),
|
|
fx.Populate(&h, &mw),
|
|
)
|
|
app.RequireStart()
|
|
t.Cleanup(app.RequireStop)
|
|
|
|
s := &Server{config: cfg, mw: mw, h: h}
|
|
s.SetupRoutes()
|
|
|
|
return s
|
|
}
|
|
|
|
// logIn logs in to s with the signing key, as postLogin does, and returns
|
|
// the session cookie it sets.
|
|
func logIn(t *testing.T, s *Server) *http.Cookie {
|
|
t.Helper()
|
|
|
|
rec := postLogin(t, s, firstClient, "", testSigningKey)
|
|
|
|
for _, c := range rec.Result().Cookies() {
|
|
if c.Name == session.CookieName {
|
|
return c
|
|
}
|
|
}
|
|
|
|
t.Fatal("logging in set no session cookie")
|
|
|
|
return nil
|
|
}
|
|
|
|
// opensGeneratorPage reports whether GET / with cookie shows s's generator
|
|
// page, which only a valid login session opens.
|
|
func opensGeneratorPage(t *testing.T, s *Server, cookie *http.Cookie) bool {
|
|
t.Helper()
|
|
|
|
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
|
|
req.AddCookie(cookie)
|
|
|
|
rec := httptest.NewRecorder()
|
|
s.ServeHTTP(rec, req)
|
|
|
|
return rec.Code == http.StatusOK &&
|
|
strings.Contains(rec.Body.String(), `action="/generate"`)
|
|
}
|
|
|
|
// postLogout loads the generator page with GET / and sessionCookie and
|
|
// submits its logout form with POST /logout, as a browser does.
|
|
func postLogout(
|
|
t *testing.T, s *Server, sessionCookie *http.Cookie,
|
|
) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
|
|
req := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
|
|
req.AddCookie(sessionCookie)
|
|
|
|
page := httptest.NewRecorder()
|
|
s.ServeHTTP(page, req)
|
|
|
|
match := logoutFormPattern.FindStringSubmatch(page.Body.String())
|
|
if match == nil {
|
|
t.Fatal("the generator page has no logout form with a CSRF token")
|
|
}
|
|
|
|
form := url.Values{"gorilla.csrf.Token": {match[1]}}
|
|
req = httptest.NewRequestWithContext(t.Context(), http.MethodPost,
|
|
"/logout", strings.NewReader(form.Encode()))
|
|
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
|
req.AddCookie(sessionCookie)
|
|
|
|
for _, c := range page.Result().Cookies() {
|
|
req.AddCookie(c)
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
s.ServeHTTP(rec, req)
|
|
|
|
return rec
|
|
}
|
|
|
|
// TestLogoutWithoutCSRFTokenEndsNoSession verifies that a request to /logout
|
|
// carrying the session cookie but not the logout form's CSRF token ends no
|
|
// session: GET /logout, as an image on another page of the same site makes a
|
|
// browser send, answers 405, and POST /logout without the token answers 403.
|
|
func TestLogoutWithoutCSRFTokenEndsNoSession(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
method string
|
|
want int
|
|
}{
|
|
{http.MethodGet, http.StatusMethodNotAllowed},
|
|
{http.MethodPost, http.StatusForbidden},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.method, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := newTestServer(t)
|
|
cookie := logIn(t, s)
|
|
|
|
req := httptest.NewRequestWithContext(
|
|
t.Context(), tc.method, "/logout", nil)
|
|
req.AddCookie(cookie)
|
|
|
|
rec := httptest.NewRecorder()
|
|
s.ServeHTTP(rec, req)
|
|
t.Logf("%s /logout without the CSRF token: status %d",
|
|
tc.method, rec.Code)
|
|
|
|
if rec.Code != tc.want {
|
|
t.Errorf("%s /logout without the CSRF token status = %d, "+
|
|
"want %d", tc.method, rec.Code, tc.want)
|
|
}
|
|
|
|
if !opensGeneratorPage(t, s, cookie) {
|
|
t.Errorf("%s /logout without the CSRF token ended the "+
|
|
"login session", tc.method)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestLoginAfterLogoutOpensGeneratorPage verifies that after a logout from
|
|
// the generator page a copy of the session cookie taken before it is
|
|
// refused, and that a login after it opens the generator page.
|
|
func TestLoginAfterLogoutOpensGeneratorPage(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := newTestServer(t)
|
|
|
|
copied := logIn(t, s)
|
|
if !opensGeneratorPage(t, s, copied) {
|
|
t.Fatal("logging in did not open the generator page")
|
|
}
|
|
|
|
logout := postLogout(t, s, copied)
|
|
if logout.Code != http.StatusSeeOther {
|
|
t.Fatalf("POST /logout status = %d, want %d",
|
|
logout.Code, http.StatusSeeOther)
|
|
}
|
|
|
|
if opensGeneratorPage(t, s, copied) {
|
|
t.Error("a cookie copied before the logout still opens the " +
|
|
"generator page")
|
|
}
|
|
|
|
opens := opensGeneratorPage(t, s, logIn(t, s))
|
|
t.Logf("a login after the logout opens the generator page: %v", opens)
|
|
|
|
if !opens {
|
|
t.Error("a login after the logout does not open the generator page")
|
|
}
|
|
}
|
|
|
|
// TestLogoutOutlastsRestart verifies that a copy of the session cookie taken
|
|
// before a logout is still refused by a pixa started again on the same state
|
|
// directory, as pixad is after a restart.
|
|
func TestLogoutOutlastsRestart(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := newTestServer(t)
|
|
|
|
copied := logIn(t, s)
|
|
if !opensGeneratorPage(t, s, copied) {
|
|
t.Fatal("logging in did not open the generator page")
|
|
}
|
|
|
|
logout := postLogout(t, s, copied)
|
|
if logout.Code != http.StatusSeeOther {
|
|
t.Fatalf("POST /logout status = %d, want %d",
|
|
logout.Code, http.StatusSeeOther)
|
|
}
|
|
|
|
restarted := startPixa(t, s.config)
|
|
|
|
opens := opensGeneratorPage(t, restarted, copied)
|
|
t.Logf("after the restart, the copied cookie opens the generator page: %v",
|
|
opens)
|
|
|
|
if opens {
|
|
t.Error("after a restart, a cookie copied before the logout " +
|
|
"still opens the generator page")
|
|
}
|
|
}
|
|
|
|
// TestLostStateDirectoryEndsEveryLoginSession verifies that a pixa started on
|
|
// an empty state directory, as pixad is after a restart that lost its state
|
|
// directory, refuses a session cookie from before, and that a login on it
|
|
// opens the generator page.
|
|
func TestLostStateDirectoryEndsEveryLoginSession(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
s := newTestServer(t)
|
|
|
|
before := logIn(t, s)
|
|
if !opensGeneratorPage(t, s, before) {
|
|
t.Fatal("logging in did not open the generator page")
|
|
}
|
|
|
|
emptyStateDir := *s.config
|
|
emptyStateDir.StateDir = t.TempDir()
|
|
emptyStateDir.DBURL = "file:" +
|
|
filepath.Join(emptyStateDir.StateDir, "state.sqlite3")
|
|
|
|
restarted := startPixa(t, &emptyStateDir)
|
|
|
|
opens := opensGeneratorPage(t, restarted, before)
|
|
t.Logf("on an empty state directory, the cookie from before opens the "+
|
|
"generator page: %v", opens)
|
|
|
|
if opens {
|
|
t.Error("on an empty state directory, a cookie from before still " +
|
|
"opens the generator page")
|
|
}
|
|
|
|
if !opensGeneratorPage(t, restarted, logIn(t, restarted)) {
|
|
t.Error("a login on an empty state directory does not open the " +
|
|
"generator page")
|
|
}
|
|
}
|