A pixa started on an empty state directory must refuse a session cookie from before, and a login on it must open the generator page. With the count of logouts, the new pixa starts from 0 again and accepts the old cookie, so this test fails until the count is replaced. Model: opus-5-5
270 lines
7.1 KiB
Go
270 lines
7.1 KiB
Go
package server
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
|
|
"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,
|
|
),
|
|
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")
|
|
}
|
|
}
|