You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

192 lines
5.8 KiB

package auth
import (
"bytes"
"html/template"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestLoginPageRenders(t *testing.T) {
handler, _, _ := newTestHandler(t)
request := httptest.NewRequest(http.MethodGet, "/login", nil)
response := httptest.NewRecorder()
handler.LoginPage(response, request)
if response.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", response.Code)
}
if body := response.Body.String(); !strings.Contains(body, `<form`) || !strings.Contains(body, `name="username"`) {
t.Fatalf("login page missing expected form fields: %s", body)
}
if got, want := strings.TrimSpace(response.Body.String()), strings.TrimSpace(readTemplateFile(t, "login.html")); got != want {
t.Fatalf("login page was not rendered from template file\nwant:\n%s\n\ngot:\n%s", want, got)
}
}
func TestLoginValidCredentialsSetSessionCookieAndRedirect(t *testing.T) {
handler, store, _ := newTestHandler(t)
if err := store.CreateUser("alice", "secret", false); err != nil {
t.Fatalf("create user: %v", err)
}
form := url.Values{"username": {"alice"}, "password": {"secret"}}
request := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
response := httptest.NewRecorder()
handler.Login(response, request)
if response.Code != http.StatusSeeOther {
t.Fatalf("expected 303, got %d", response.Code)
}
if location := response.Header().Get("Location"); location != "/" {
t.Fatalf("expected redirect to /, got %q", location)
}
cookie := findCookie(response.Result().Cookies(), sessionCookieName)
if cookie == nil || cookie.Value == "" {
t.Fatalf("expected session cookie, got %#v", response.Result().Cookies())
}
if _, err := store.GetSession(cookie.Value); err != nil {
t.Fatalf("session cookie was not persisted: %v", err)
}
}
func TestLoginInvalidCredentialsReturnsUnauthorized(t *testing.T) {
handler, store, _ := newTestHandler(t)
if err := store.CreateUser("alice", "secret", false); err != nil {
t.Fatalf("create user: %v", err)
}
form := url.Values{"username": {"alice"}, "password": {"wrong"}}
request := httptest.NewRequest(http.MethodPost, "/login", strings.NewReader(form.Encode()))
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
response := httptest.NewRecorder()
handler.Login(response, request)
if response.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", response.Code)
}
}
func TestAdminUsersRequiresAdmin(t *testing.T) {
handler, store, mux := newTestHandler(t)
if err := store.CreateUser("alice", "secret", false); err != nil {
t.Fatalf("create regular user: %v", err)
}
if err := store.CreateUser("admin", "secret", true); err != nil {
t.Fatalf("create admin user: %v", err)
}
mux.Handle("/admin/users", RequireAuth(store)(RequireAdmin(http.HandlerFunc(handler.UsersPage))))
request := httptest.NewRequest(http.MethodGet, "/admin/users", nil)
response := httptest.NewRecorder()
mux.ServeHTTP(response, request)
if response.Code != http.StatusFound {
t.Fatalf("expected unauthenticated redirect, got %d", response.Code)
}
if location := response.Header().Get("Location"); location != "/login" {
t.Fatalf("expected redirect to /login, got %q", location)
}
regularCookie := sessionCookie(t, store, "alice", "secret")
request = httptest.NewRequest(http.MethodGet, "/admin/users", nil)
request.AddCookie(regularCookie)
response = httptest.NewRecorder()
mux.ServeHTTP(response, request)
if response.Code != http.StatusForbidden {
t.Fatalf("expected regular user forbidden, got %d", response.Code)
}
adminCookie := sessionCookie(t, store, "admin", "secret")
request = httptest.NewRequest(http.MethodGet, "/admin/users", nil)
request.AddCookie(adminCookie)
response = httptest.NewRecorder()
mux.ServeHTTP(response, request)
if response.Code != http.StatusOK {
t.Fatalf("expected admin page, got %d", response.Code)
}
if !strings.Contains(response.Body.String(), "admin") {
t.Fatalf("admin page missing user list: %s", response.Body.String())
}
users, err := store.ListUsers()
if err != nil {
t.Fatalf("list users: %v", err)
}
if got, want := strings.TrimSpace(response.Body.String()), strings.TrimSpace(renderTemplateFile(t, "admin_users.html", struct {
Users []User
}{Users: users})); got != want {
t.Fatalf("admin users page was not rendered from template file\nwant:\n%s\n\ngot:\n%s", want, got)
}
}
func newTestHandler(t *testing.T) (*Handler, *Store, *http.ServeMux) {
t.Helper()
database := openTestDB(t)
store := NewStore(database)
handler := NewHandler(store, time.Hour)
return handler, store, http.NewServeMux()
}
func sessionCookie(t *testing.T, store *Store, username, password string) *http.Cookie {
t.Helper()
user, err := store.Authenticate(username, password)
if err != nil {
t.Fatalf("authenticate %s: %v", username, err)
}
token, err := store.CreateSession(user.ID, time.Hour)
if err != nil {
t.Fatalf("create session: %v", err)
}
return &http.Cookie{Name: sessionCookieName, Value: token}
}
func findCookie(cookies []*http.Cookie, name string) *http.Cookie {
for _, cookie := range cookies {
if cookie.Name == name {
return cookie
}
}
return nil
}
func readTemplateFile(t *testing.T, name string) string {
t.Helper()
content, err := os.ReadFile(filepath.Join("..", "web", "templates", name))
if err != nil {
t.Fatalf("read template %s: %v", name, err)
}
return string(content)
}
func renderTemplateFile(t *testing.T, name string, data any) string {
t.Helper()
tmpl, err := template.ParseFiles(filepath.Join("..", "web", "templates", name))
if err != nil {
t.Fatalf("parse template %s: %v", name, err)
}
var output bytes.Buffer
if err := tmpl.ExecuteTemplate(&output, name, data); err != nil {
t.Fatalf("execute template %s: %v", name, err)
}
return output.String()
}