All checks were successful
check / check (push) Successful in 2m56s
chi v1.5.5's middleware.Recoverer neither logged a handler panic nor answered 500. Its pretty-printer scans the stack for a frame beginning "panic(0x", which the runtime no longer emits, so the scan never terminates early and every line reaches decorateFuncCallLine, which slices pkg[strings.Index(pkg, "."):] without checking for -1. That second panic escaped chi's own deferred function, so its WriteHeader(500) never ran: net/http closed the connection and reported its own crash, losing the original panic value entirely. Middleware.Recoverer replaces it. It writes one ERROR record through internal/logger carrying the panic value, the stack and the request id, and answers 500. http.ErrAbortHandler is re-panicked rather than swallowed, and a response the handler already committed is left alone rather than overwritten. It is registered inside every middleware that observes the response, so the 500 is the status the access log records and the metrics count, and outside the sentryhttp handler, whose Repanic option needs something further out to catch what it re-raises. Both fields are bounded in encoded bytes, through the same internal/logfield budget the access log spends: 512 for the panic value, since a handler may build one out of the request, and 8192 for the stack, cut at its far end so the panic site survives. MaxPanicLogLineBytes states the resulting ceiling at 10240; measured, the widest line either handler produces is 8898, a figure that carries no source paths and reproduces across checkouts. The real case through the shipped chain measures roughly 3960 bytes; that one moves with the checkout, because debug.Stack() embeds absolute source paths, so it is stated as a measurement rather than as an invariant and no test asserts it. Because the panic record no longer reaches net/http's error log, the carve-outs in README.md and in the MaxAccessLogLineBytes doc comment that described that path are removed rather than reworded. What replaces them states the ceiling the record is now written under, and internal/server/recoverer_test.go asserts that "http: panic serving" appears in neither of the process's streams.
660 lines
17 KiB
Go
660 lines
17 KiB
Go
package server_test
|
|
|
|
import (
|
|
"context"
|
|
"html"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/fx"
|
|
"go.uber.org/fx/fxtest"
|
|
"gorm.io/gorm/clause"
|
|
"sneak.berlin/go/webhooker/internal/config"
|
|
"sneak.berlin/go/webhooker/internal/database"
|
|
"sneak.berlin/go/webhooker/internal/delivery"
|
|
"sneak.berlin/go/webhooker/internal/globals"
|
|
"sneak.berlin/go/webhooker/internal/handlers"
|
|
"sneak.berlin/go/webhooker/internal/healthcheck"
|
|
"sneak.berlin/go/webhooker/internal/logger"
|
|
"sneak.berlin/go/webhooker/internal/middleware"
|
|
"sneak.berlin/go/webhooker/internal/server"
|
|
"sneak.berlin/go/webhooker/internal/session"
|
|
"sneak.berlin/go/webhooker/static"
|
|
)
|
|
|
|
// csrfCookieName is the cookie gorilla/csrf issues when it runs. Its
|
|
// presence or absence on a response is how these tests tell whether
|
|
// the CSRF middleware executed.
|
|
const csrfCookieName = "_gorilla_csrf"
|
|
|
|
type noopNotifier struct{}
|
|
|
|
func (n *noopNotifier) Notify([]delivery.Task) {}
|
|
|
|
// noopEvictor satisfies handlers.New's delivery.WebhookEvictor
|
|
// dependency. These tests never delete a webhook, so there is
|
|
// nothing to record.
|
|
type noopEvictor struct{}
|
|
|
|
func (e *noopEvictor) EvictWebhook(string) {}
|
|
|
|
// testEnv is the real router from routes.go plus the collaborators
|
|
// tests need to seed users and forge sessions.
|
|
type testEnv struct {
|
|
router http.Handler
|
|
sess *session.Session
|
|
db *database.Database
|
|
dbMgr *database.WebhookDBManager
|
|
|
|
// The collaborators the router was built from, kept so a test
|
|
// that needs a second router over the same graph — one carrying
|
|
// a panicking probe route, or one with Sentry registered — can
|
|
// build it without wiring the graph again.
|
|
log *logger.Logger
|
|
cfg *config.Config
|
|
mw *middleware.Middleware
|
|
hnd *handlers.Handlers
|
|
}
|
|
|
|
// newTestEnv wires the dependency graph with fx and builds the
|
|
// production route tree, so middleware registration order is
|
|
// exercised exactly as it ships.
|
|
func newTestEnv(t *testing.T) *testEnv {
|
|
t.Helper()
|
|
|
|
var (
|
|
log *logger.Logger
|
|
cfg *config.Config
|
|
mw *middleware.Middleware
|
|
hnd *handlers.Handlers
|
|
sess *session.Session
|
|
db *database.Database
|
|
dbMgr *database.WebhookDBManager
|
|
)
|
|
|
|
app := fxtest.New(
|
|
t,
|
|
fx.Provide(
|
|
globals.New,
|
|
logger.New,
|
|
func() *config.Config {
|
|
return &config.Config{
|
|
DataDir: t.TempDir(),
|
|
Environment: config.EnvironmentDev,
|
|
}
|
|
},
|
|
database.New,
|
|
database.NewWebhookDBManager,
|
|
healthcheck.New,
|
|
session.New,
|
|
func() delivery.Notifier { return &noopNotifier{} },
|
|
func() delivery.WebhookEvictor { return &noopEvictor{} },
|
|
middleware.New,
|
|
handlers.New,
|
|
),
|
|
fx.Populate(&log, &cfg, &mw, &hnd, &sess, &db, &dbMgr),
|
|
)
|
|
app.RequireStart()
|
|
t.Cleanup(app.RequireStop)
|
|
|
|
return &testEnv{
|
|
router: server.NewRouterForTest(log.Get(), cfg, mw, hnd),
|
|
sess: sess,
|
|
db: db,
|
|
dbMgr: dbMgr,
|
|
log: log,
|
|
cfg: cfg,
|
|
mw: mw,
|
|
hnd: hnd,
|
|
}
|
|
}
|
|
|
|
// oversizeValue returns a form value one byte past the route-group
|
|
// body cap, so an encoded form containing it is guaranteed oversize.
|
|
func oversizeValue() string {
|
|
return strings.Repeat("a", int(server.MaxFormBodySizeForTest)+1)
|
|
}
|
|
|
|
// csrfCookieSet reports whether the response issued a gorilla/csrf
|
|
// cookie, which only happens if the CSRF middleware ran.
|
|
func csrfCookieSet(w *httptest.ResponseRecorder) bool {
|
|
for _, c := range w.Result().Cookies() {
|
|
if c.Name == csrfCookieName {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// get issues a GET through the router with the supplied cookies.
|
|
func (e *testEnv) get(
|
|
path string,
|
|
cookies []*http.Cookie,
|
|
) *httptest.ResponseRecorder {
|
|
req := httptest.NewRequestWithContext(
|
|
context.Background(), http.MethodGet, path, nil,
|
|
)
|
|
|
|
for _, c := range cookies {
|
|
req.AddCookie(c)
|
|
}
|
|
|
|
w := httptest.NewRecorder()
|
|
e.router.ServeHTTP(w, req)
|
|
|
|
return w
|
|
}
|
|
|
|
// post issues a urlencoded form POST through the router. The body is
|
|
// a strings.Reader, so the request carries an accurate
|
|
// Content-Length — the signal MaxBodySize checks up front.
|
|
func (e *testEnv) post(
|
|
path string,
|
|
form url.Values,
|
|
cookies []*http.Cookie,
|
|
) *httptest.ResponseRecorder {
|
|
req := httptest.NewRequestWithContext(
|
|
context.Background(), http.MethodPost, path,
|
|
strings.NewReader(form.Encode()),
|
|
)
|
|
req.Header.Set(
|
|
"Content-Type", "application/x-www-form-urlencoded",
|
|
)
|
|
|
|
for _, c := range cookies {
|
|
req.AddCookie(c)
|
|
}
|
|
|
|
w := httptest.NewRecorder()
|
|
e.router.ServeHTTP(w, req)
|
|
|
|
return w
|
|
}
|
|
|
|
// csrfFrom renders the page at path and returns the CSRF token from
|
|
// its form together with every cookie needed for the follow-up POST.
|
|
func (e *testEnv) csrfFrom(
|
|
t *testing.T,
|
|
path string,
|
|
cookies []*http.Cookie,
|
|
) (string, []*http.Cookie) {
|
|
t.Helper()
|
|
|
|
w := e.get(path, cookies)
|
|
require.Equal(t, http.StatusOK, w.Code)
|
|
|
|
pattern := regexp.MustCompile(
|
|
`name="csrf_token" value="([^"]+)"`,
|
|
)
|
|
|
|
match := pattern.FindStringSubmatch(w.Body.String())
|
|
require.Len(t, match, 2, "form must embed a CSRF token")
|
|
|
|
// html/template escapes "+" and "=" in attribute values, and
|
|
// gorilla/csrf tokens are standard base64, so the value read
|
|
// out of the markup has to be unescaped before it is submitted.
|
|
token := html.UnescapeString(match[1])
|
|
|
|
combined := make([]*http.Cookie, 0, len(cookies))
|
|
combined = append(combined, cookies...)
|
|
combined = append(combined, w.Result().Cookies()...)
|
|
|
|
return token, combined
|
|
}
|
|
|
|
// authCookies forges an authenticated session for the given user.
|
|
func (e *testEnv) authCookies(
|
|
t *testing.T,
|
|
userID, username string,
|
|
) []*http.Cookie {
|
|
t.Helper()
|
|
|
|
req := httptest.NewRequestWithContext(
|
|
context.Background(), http.MethodGet, "/setup", nil,
|
|
)
|
|
w := httptest.NewRecorder()
|
|
|
|
s, err := e.sess.Get(req)
|
|
require.NoError(t, err)
|
|
|
|
e.sess.SetUser(s, userID, username)
|
|
require.NoError(t, e.sess.Save(req, w, s))
|
|
|
|
cookies := w.Result().Cookies()
|
|
require.NotEmpty(t, cookies, "session cookie should be set")
|
|
|
|
return cookies
|
|
}
|
|
|
|
// seedUser creates a user with the given password and returns the
|
|
// stored hash so tests can assert whether it later changed.
|
|
func (e *testEnv) seedUser(
|
|
t *testing.T,
|
|
username, password string,
|
|
) (string, string) {
|
|
t.Helper()
|
|
|
|
hash, err := database.HashPassword(password)
|
|
require.NoError(t, err)
|
|
|
|
user := &database.User{Username: username, Password: hash}
|
|
require.NoError(t, e.db.DB().Create(user).Error)
|
|
|
|
return user.ID, hash
|
|
}
|
|
|
|
// seedWebhook creates a webhook owned by the given user.
|
|
func (e *testEnv) seedWebhook(
|
|
t *testing.T,
|
|
userID string,
|
|
) *database.Webhook {
|
|
t.Helper()
|
|
|
|
wh := &database.Webhook{UserID: userID, Name: "routed"}
|
|
|
|
require.NoError(
|
|
t,
|
|
e.db.DB().Omit(clause.Associations).Create(wh).Error,
|
|
)
|
|
|
|
return wh
|
|
}
|
|
|
|
// seedEvent records one event with the given body in a webhook's
|
|
// own database.
|
|
func (e *testEnv) seedEvent(
|
|
t *testing.T,
|
|
webhookID, body string,
|
|
) *database.Event {
|
|
t.Helper()
|
|
|
|
webhookDB, err := e.dbMgr.GetDB(webhookID)
|
|
require.NoError(t, err)
|
|
|
|
event := &database.Event{
|
|
WebhookID: webhookID,
|
|
Method: http.MethodPost,
|
|
Body: body,
|
|
ContentType: "application/octet-stream",
|
|
}
|
|
|
|
require.NoError(
|
|
t,
|
|
webhookDB.Omit(clause.Associations).Create(event).Error,
|
|
)
|
|
|
|
return event
|
|
}
|
|
|
|
// storedHash reads the current password hash for a username.
|
|
func (e *testEnv) storedHash(t *testing.T, username string) string {
|
|
t.Helper()
|
|
|
|
var user database.User
|
|
|
|
require.NoError(t,
|
|
e.db.DB().Where("username = ?", username).
|
|
First(&user).Error,
|
|
)
|
|
|
|
return user.Password
|
|
}
|
|
|
|
// --- /s static group ---
|
|
|
|
// TestStaticServesEveryMethod pins what the static mount actually
|
|
// answers. chi's Mount registers the handler for all methods and
|
|
// http.FileServer only special-cases HEAD (by suppressing the body),
|
|
// so a POST or a DELETE to an asset is served the file rather than
|
|
// refused. The README documents this; the test is what keeps the two
|
|
// from drifting.
|
|
func TestStaticServesEveryMethod(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
|
|
body, err := static.Static.ReadFile("js/app.js")
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, body)
|
|
|
|
for _, method := range []string{
|
|
http.MethodGet,
|
|
http.MethodHead,
|
|
http.MethodPost,
|
|
http.MethodPut,
|
|
http.MethodDelete,
|
|
} {
|
|
t.Run(method, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
req := httptest.NewRequestWithContext(
|
|
context.Background(), method,
|
|
"/s/js/app.js", nil,
|
|
)
|
|
w := httptest.NewRecorder()
|
|
env.router.ServeHTTP(w, req)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code,
|
|
"static mount answers every method")
|
|
|
|
if method == http.MethodHead {
|
|
assert.Empty(t, w.Body.Bytes(),
|
|
"HEAD must not carry a body")
|
|
|
|
return
|
|
}
|
|
|
|
assert.Equal(t, body, w.Body.Bytes(),
|
|
"the asset itself is returned")
|
|
})
|
|
}
|
|
}
|
|
|
|
// --- /pages group ---
|
|
|
|
// TestPagesLogin_OversizeBody_RejectedBeforeCSRF proves the cap runs
|
|
// ahead of gorilla/csrf: the response is a clean 413 and no CSRF
|
|
// cookie was issued, so neither the CSRF middleware nor the login
|
|
// handler ran.
|
|
func TestPagesLogin_OversizeBody_RejectedBeforeCSRF(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
|
|
form := url.Values{}
|
|
form.Set("username", oversizeValue())
|
|
form.Set("password", "irrelevant")
|
|
|
|
w := env.post("/pages/login", form, nil)
|
|
|
|
assert.Equal(
|
|
t, http.StatusRequestEntityTooLarge, w.Code,
|
|
)
|
|
assert.False(
|
|
t, csrfCookieSet(w),
|
|
"CSRF middleware must not run for an oversized body",
|
|
)
|
|
}
|
|
|
|
// TestPagesLogin_UnderLimit_NoToken_CSRFRejects is the control for
|
|
// the test above: an identically shaped but under-limit POST does
|
|
// reach gorilla/csrf, which rejects it and issues its cookie. Without
|
|
// this, the missing-cookie assertion above would prove nothing.
|
|
func TestPagesLogin_UnderLimit_NoToken_CSRFRejects(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
|
|
form := url.Values{}
|
|
form.Set("username", "someone")
|
|
form.Set("password", "irrelevant")
|
|
|
|
w := env.post("/pages/login", form, nil)
|
|
|
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
|
assert.True(
|
|
t, csrfCookieSet(w),
|
|
"CSRF middleware should run for an under-limit body",
|
|
)
|
|
}
|
|
|
|
// TestPagesLogin_UnderLimit_ValidToken_ReachesHandler proves the
|
|
// reorder did not break CSRF token handling: a token harvested from
|
|
// the rendered login form is still accepted and the request lands in
|
|
// the handler.
|
|
func TestPagesLogin_UnderLimit_ValidToken_ReachesHandler(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
|
|
token, cookies := env.csrfFrom(t, "/pages/login", nil)
|
|
|
|
form := url.Values{}
|
|
form.Set("csrf_token", token)
|
|
form.Set("username", "nosuchuser")
|
|
form.Set("password", "wrongpassword")
|
|
|
|
w := env.post("/pages/login", form, cookies)
|
|
|
|
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
|
assert.Contains(
|
|
t, w.Body.String(), "Invalid username or password",
|
|
"request should reach the login handler",
|
|
)
|
|
}
|
|
|
|
// TestPagesLogin_CorrectPasswordSurvivesASpentBudget pins the
|
|
// routing half of the fix, which every other login test misses by
|
|
// driving the handler directly: no pre-emptive limiter sits in front
|
|
// of POST /pages/login on the real route tree.
|
|
//
|
|
// A limiter registered there would answer the last request 429
|
|
// however correct its password is, because the wrong passwords
|
|
// before it have already spent the bucket — which is the lockout
|
|
// this endpoint exists to not have. CSRF and the body cap still run,
|
|
// since every request here carries a harvested token.
|
|
func TestPagesLogin_CorrectPasswordSurvivesASpentBudget(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
username = "operator"
|
|
password = "correct-horse-battery-staple"
|
|
)
|
|
|
|
env := newTestEnv(t)
|
|
env.seedUser(t, username, password)
|
|
|
|
submit := func(t *testing.T, pw string) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
|
|
token, cookies := env.csrfFrom(t, "/pages/login", nil)
|
|
|
|
form := url.Values{}
|
|
form.Set("csrf_token", token)
|
|
form.Set("username", username)
|
|
form.Set("password", pw)
|
|
|
|
return env.post("/pages/login", form, cookies)
|
|
}
|
|
|
|
// Spend the failure budget against this username. The exact
|
|
// limit belongs to the middleware; this waits for the throttle
|
|
// to appear rather than restating it, under a ceiling well
|
|
// above it so a broken limiter fails the test instead of
|
|
// looping.
|
|
const maxAttempts = 20
|
|
|
|
spent := false
|
|
|
|
for range maxAttempts {
|
|
code := submit(t, "wrong").Code
|
|
if code == http.StatusTooManyRequests {
|
|
spent = true
|
|
|
|
break
|
|
}
|
|
|
|
require.Equal(
|
|
t, http.StatusUnauthorized, code,
|
|
"a wrong password must be rejected, not accepted",
|
|
)
|
|
}
|
|
|
|
require.True(
|
|
t, spent,
|
|
"repeated wrong passwords must eventually be throttled",
|
|
)
|
|
|
|
assert.Equal(
|
|
t, http.StatusSeeOther, submit(t, password).Code,
|
|
"a correct password must be accepted on the routed "+
|
|
"endpoint even with the failure budget spent: the "+
|
|
"operator has no second administrative path",
|
|
)
|
|
}
|
|
|
|
// --- /user/{username} group ---
|
|
|
|
// TestPasswordChange_OversizeBody_RejectedAndPasswordUnchanged
|
|
// covers the route that previously had no middleware body cap at
|
|
// all. The request carries a valid session and a valid CSRF token,
|
|
// so the only thing that can stop it is the size cap; the unchanged
|
|
// password hash is the observable proof the handler never ran.
|
|
func TestPasswordChange_OversizeBody_RejectedAndPasswordUnchanged(
|
|
t *testing.T,
|
|
) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
|
|
userID, originalHash := env.seedUser(t, "pwuser", "oldpassword")
|
|
cookies := env.authCookies(t, userID, "pwuser")
|
|
token, cookies := env.csrfFrom(t, "/user/pwuser/", cookies)
|
|
|
|
form := url.Values{}
|
|
form.Set("csrf_token", token)
|
|
form.Set("current_password", "oldpassword")
|
|
form.Set("new_password", oversizeValue())
|
|
form.Set("confirm_password", oversizeValue())
|
|
|
|
w := env.post("/user/pwuser/password", form, cookies)
|
|
|
|
assert.Equal(
|
|
t, http.StatusRequestEntityTooLarge, w.Code,
|
|
)
|
|
assert.Equal(
|
|
t, originalHash, env.storedHash(t, "pwuser"),
|
|
"handler must not run, so the password must be unchanged",
|
|
)
|
|
}
|
|
|
|
// TestPasswordChange_UnderLimit_Succeeds proves that adding the cap
|
|
// to the /user/{username} group did not break the route it guards.
|
|
func TestPasswordChange_UnderLimit_Succeeds(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
|
|
userID, originalHash := env.seedUser(t, "okuser", "oldpassword")
|
|
cookies := env.authCookies(t, userID, "okuser")
|
|
token, cookies := env.csrfFrom(t, "/user/okuser/", cookies)
|
|
|
|
form := url.Values{}
|
|
form.Set("csrf_token", token)
|
|
form.Set("current_password", "oldpassword")
|
|
form.Set("new_password", "brandnewpassword")
|
|
form.Set("confirm_password", "brandnewpassword")
|
|
|
|
w := env.post("/user/okuser/password", form, cookies)
|
|
|
|
assert.Equal(t, http.StatusOK, w.Code)
|
|
assert.NotEqual(
|
|
t, originalHash, env.storedHash(t, "okuser"),
|
|
"an under-limit password change should still apply",
|
|
)
|
|
}
|
|
|
|
// --- /source/{sourceID} group ---
|
|
|
|
// TestSourceLogs_TruncationLinkDownloadsTheBody walks the whole
|
|
// feature the way a user does: render the event log page through
|
|
// the production router, take the download URL out of the markup
|
|
// the template emitted, and fetch that URL through the router
|
|
// again. Nothing here is hand-written, so a typo in either the
|
|
// route pattern or the template href fails this test — the
|
|
// handler-level tests cannot catch that, because they forge
|
|
// their own route context and assert a URL string they wrote
|
|
// themselves.
|
|
func TestSourceLogs_TruncationLinkDownloadsTheBody(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
|
|
userID, _ := env.seedUser(t, "loguser", "somepassword")
|
|
cookies := env.authCookies(t, userID, "loguser")
|
|
|
|
// Comfortably over the event log page's render cap, so the
|
|
// page truncates the body and renders the download link at
|
|
// all. The exact cap is the handlers package's business and
|
|
// is pinned by its own tests; this only needs to exceed it.
|
|
stored := strings.Repeat("Z", 64*1024)
|
|
|
|
wh := env.seedWebhook(t, userID)
|
|
env.seedEvent(t, wh.ID, stored)
|
|
|
|
page := env.get("/source/"+wh.ID+"/logs", cookies)
|
|
require.Equal(t, http.StatusOK, page.Code)
|
|
|
|
link := regexp.MustCompile(
|
|
`href="(/source/[^"]+/body)"`,
|
|
).FindStringSubmatch(page.Body.String())
|
|
require.Len(
|
|
t, link, 2,
|
|
"truncated body should render a download link",
|
|
)
|
|
|
|
w := env.get(html.UnescapeString(link[1]), cookies)
|
|
|
|
require.Equal(
|
|
t, http.StatusOK, w.Code,
|
|
"the link the page emits must be a live route",
|
|
)
|
|
assert.Equal(t, stored, w.Body.String())
|
|
assert.Equal(
|
|
t, strconv.Itoa(len(stored)),
|
|
w.Header().Get("Content-Length"),
|
|
)
|
|
assert.Equal(
|
|
t, "application/octet-stream",
|
|
w.Header().Get("Content-Type"),
|
|
)
|
|
assert.Contains(
|
|
t, w.Header().Get("Content-Disposition"), "attachment",
|
|
)
|
|
assert.Equal(
|
|
t, "nosniff", w.Header().Get("X-Content-Type-Options"),
|
|
)
|
|
}
|
|
|
|
// TestSourceLogsBody_OtherUser404s pins that the download route
|
|
// as registered is behind the auth the group provides and the
|
|
// ownership check the handler applies: another logged-in user
|
|
// asking the real router for the same URL gets a 404, and an
|
|
// unauthenticated request never reaches the handler at all.
|
|
func TestSourceLogsBody_OtherUser404s(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
env := newTestEnv(t)
|
|
|
|
ownerID, _ := env.seedUser(t, "owner", "somepassword")
|
|
wh := env.seedWebhook(t, ownerID)
|
|
|
|
const payload = "OWNERS-PAYLOAD-77c1"
|
|
|
|
evt := env.seedEvent(t, wh.ID, payload)
|
|
path := "/source/" + wh.ID + "/logs/" + evt.ID + "/body"
|
|
|
|
intruderID, _ := env.seedUser(t, "intruder", "somepassword")
|
|
intruder := env.authCookies(t, intruderID, "intruder")
|
|
|
|
w := env.get(path, intruder)
|
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
|
assert.NotContains(t, w.Body.String(), payload)
|
|
|
|
anon := env.get(path, nil)
|
|
assert.Equal(t, http.StatusSeeOther, anon.Code)
|
|
assert.Equal(t, "/pages/login", anon.Header().Get("Location"))
|
|
}
|