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
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()
|
|
}
|
|
|