Test logging in, logging out, the URL generator and /v1/e/ tokens (closes #77)
check / check (push) Failing after 4s
check / check (push) Failing after 4s
New handler tests in internal/handlers, with no network: GET / shows the login form without a session; a wrong key shows it again with an error and sets no session cookie; the right key answers 303 with a session cookie marked Secure, HttpOnly and SameSite=Strict, with which GET / shows the generator page; GET /logout empties the cookie with Max-Age=0; POST /generate without a session answers 303 to /; /v1/e/ serves a valid token's image, answers 410 for an expired token and 400 for one changed, cut short or made with another signing key; a URL made on the generator page is served by /v1/e/. No code changes. Model: opus-5-5
This commit was merged in pull request #191.
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"image/jpeg"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"sneak.berlin/go/pixa/internal/encurl"
|
||||
)
|
||||
|
||||
// requireServedPhoto requires that rec answers 200 with the JPEG at photoPath
|
||||
// on signedHost at the 50x50 that encPhotoURL and the generator tests ask for.
|
||||
func requireServedPhoto(t *testing.T, rec *httptest.ResponseRecorder) {
|
||||
t.Helper()
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want %d; body %q",
|
||||
rec.Code, http.StatusOK, rec.Body.String())
|
||||
}
|
||||
|
||||
contentType := rec.Header().Get("Content-Type")
|
||||
if contentType != "image/jpeg" {
|
||||
t.Errorf("Content-Type = %q, want image/jpeg", contentType)
|
||||
}
|
||||
|
||||
img, err := jpeg.DecodeConfig(rec.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("body is not a JPEG: %v", err)
|
||||
}
|
||||
|
||||
if img.Width != 50 || img.Height != 50 {
|
||||
t.Errorf("image is %dx%d, want 50x50", img.Width, img.Height)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleImageEnc_ValidToken_ServesImage verifies that a token made with
|
||||
// the signing key serves the image it asks for.
|
||||
func TestHandleImageEnc_ValidToken_ServesImage(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
h, srv := newSignedHostServer(t, slog.New(slog.DiscardHandler))
|
||||
|
||||
rec := httptest.NewRecorder()
|
||||
srv.ServeHTTP(rec, httptest.NewRequestWithContext(
|
||||
t.Context(), http.MethodGet, encPhotoURL(t, h), nil))
|
||||
|
||||
requireServedPhoto(t, rec)
|
||||
}
|
||||
|
||||
// TestHandleImageEnc_RejectedToken verifies that a token that has expired
|
||||
// answers 410, and that a token with one character changed, a token cut
|
||||
// short, and a token made with another signing key answer 400. The server
|
||||
// would serve the photo for a token it accepted.
|
||||
func TestHandleImageEnc_RejectedToken(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
h, srv := newSignedHostServer(t, slog.New(slog.DiscardHandler))
|
||||
|
||||
photo := encurl.Payload{
|
||||
SourceHost: signedHost,
|
||||
SourcePath: photoPath,
|
||||
Width: 50,
|
||||
Height: 50,
|
||||
}
|
||||
|
||||
valid, err := h.encGen.Generate(&photo)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate() error = %v", err)
|
||||
}
|
||||
|
||||
expiredPhoto := photo
|
||||
expiredPhoto.ExpiresAt = time.Now().Add(-time.Minute).Unix()
|
||||
|
||||
expired, err := h.encGen.Generate(&expiredPhoto)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate() error = %v", err)
|
||||
}
|
||||
|
||||
otherGen, err := encurl.NewGenerator("another-signing-key-fedcba9876543210")
|
||||
if err != nil {
|
||||
t.Fatalf("encurl.NewGenerator() error = %v", err)
|
||||
}
|
||||
|
||||
otherKey, err := otherGen.Generate(&photo)
|
||||
if err != nil {
|
||||
t.Fatalf("Generate() error = %v", err)
|
||||
}
|
||||
|
||||
// Changing a character in the middle always changes the decoded bytes;
|
||||
// the last character of unpadded base64 can carry unused bits.
|
||||
middle := len(valid) / 2
|
||||
|
||||
replacement := "A"
|
||||
if valid[middle] == 'A' {
|
||||
replacement = "B"
|
||||
}
|
||||
|
||||
changed := valid[:middle] + replacement + valid[middle+1:]
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
token string
|
||||
wantStatus int
|
||||
}{
|
||||
{"expired", expired, http.StatusGone},
|
||||
{"one character changed", changed, http.StatusBadRequest},
|
||||
{"cut short", valid[:middle], http.StatusBadRequest},
|
||||
{"another signing key", otherKey, http.StatusBadRequest},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rec := getEncToken(srv, tt.token)
|
||||
t.Logf("GET /v1/e/%s/img.jpg: %d %s", tt.token, rec.Code, rec.Body)
|
||||
|
||||
if rec.Code != tt.wantStatus {
|
||||
t.Errorf("status = %d, want %d", rec.Code, tt.wantStatus)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user