This commit is contained in:
+169
-2
@@ -1,6 +1,7 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
@@ -12,6 +13,62 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestExistingDatabaseAddsNewColumns(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "legacy.db")
|
||||
db, err := sql.Open("sqlite", path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = db.Exec(`CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
||||
password_hash TEXT,
|
||||
display_name TEXT NOT NULL,
|
||||
email TEXT UNIQUE COLLATE NOCASE,
|
||||
role TEXT NOT NULL DEFAULT 'member',
|
||||
avatar_url TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
INSERT INTO users(username,display_name,role) VALUES('legacy-admin','Legacy Admin','admin');
|
||||
CREATE TABLE board_tasks (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
status TEXT NOT NULL DEFAULT 'backlog',
|
||||
assignee_id INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
creator_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
due_date TEXT,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
INSERT INTO board_tasks(title,creator_id) VALUES('Legacy card',1)`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
store, err := OpenStore(path)
|
||||
if err != nil {
|
||||
t.Fatalf("open legacy database: %v", err)
|
||||
}
|
||||
defer store.Close()
|
||||
var active bool
|
||||
if err := store.db.QueryRow(`SELECT active FROM users WHERE username='legacy-admin'`).Scan(&active); err != nil {
|
||||
t.Fatalf("read migrated account state: %v", err)
|
||||
}
|
||||
if !active {
|
||||
t.Fatal("legacy account was not active after migration")
|
||||
}
|
||||
var archivedAt sql.NullString
|
||||
if err := store.db.QueryRow(`SELECT archived_at FROM board_tasks WHERE title='Legacy card'`).Scan(&archivedAt); err != nil {
|
||||
t.Fatalf("read migrated card archive state: %v", err)
|
||||
}
|
||||
if archivedAt.Valid {
|
||||
t.Fatalf("legacy card was unexpectedly archived: %q", archivedAt.String)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoginAttendanceAndReportFlow(t *testing.T) {
|
||||
s, err := New(Config{
|
||||
Addr: ":0",
|
||||
@@ -62,6 +119,67 @@ func TestLoginAttendanceAndReportFlow(t *testing.T) {
|
||||
if _, err := s.store.Authenticate("sara", "temporary-password"); err != nil {
|
||||
t.Fatalf("created teammate could not authenticate: %v", err)
|
||||
}
|
||||
users, err := s.store.Users()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var sara User
|
||||
for _, user := range users {
|
||||
if user.Username == "sara" {
|
||||
sara = user
|
||||
break
|
||||
}
|
||||
}
|
||||
if sara.ID == 0 || !sara.Active {
|
||||
t.Fatalf("created teammate missing or inactive: %#v", sara)
|
||||
}
|
||||
saraToken, _, err := s.store.CreateSession(sara.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lockUser := formRequest(t, s.http.Handler, "/admin/users/"+strconv.FormatInt(sara.ID, 10)+"/status", url.Values{
|
||||
"csrf": {csrf},
|
||||
"action": {"lock"},
|
||||
}, session)
|
||||
if lockUser.Code != http.StatusSeeOther || lockUser.Header().Get("Location") != "/admin/users?flash=Account+locked" {
|
||||
t.Fatalf("lock user: got %d location %q", lockUser.Code, lockUser.Header().Get("Location"))
|
||||
}
|
||||
if _, err := s.store.Authenticate("sara", "temporary-password"); err == nil || !strings.Contains(err.Error(), "locked") {
|
||||
t.Fatalf("locked teammate authentication result: %v", err)
|
||||
}
|
||||
if _, _, err := s.store.Session(saraToken); err == nil {
|
||||
t.Fatal("locking a teammate did not revoke the active session")
|
||||
}
|
||||
if activeUsers, err := s.store.ActiveUsers(); err != nil || len(activeUsers) != 1 {
|
||||
t.Fatalf("locked teammate was not hidden from active users: %#v, %v", activeUsers, err)
|
||||
}
|
||||
if roster, err := s.store.DayRoster(time.Now().Format("2006-01-02")); err != nil || len(roster) != 1 {
|
||||
t.Fatalf("locked teammate was not hidden from the team roster: %#v, %v", roster, err)
|
||||
}
|
||||
usersRequest := httptest.NewRequest(http.MethodGet, "/admin/users", nil)
|
||||
usersRequest.AddCookie(session)
|
||||
usersResponse := httptest.NewRecorder()
|
||||
s.http.Handler.ServeHTTP(usersResponse, usersRequest)
|
||||
if usersResponse.Code != http.StatusOK || !strings.Contains(usersResponse.Body.String(), "Locked") || !strings.Contains(usersResponse.Body.String(), "Unlock") {
|
||||
t.Fatalf("locked account state missing from admin directory: %d %s", usersResponse.Code, usersResponse.Body.String())
|
||||
}
|
||||
lockSelf := formRequest(t, s.http.Handler, "/admin/users/1/status", url.Values{
|
||||
"csrf": {csrf},
|
||||
"action": {"lock"},
|
||||
}, session)
|
||||
if lockSelf.Code != http.StatusSeeOther || !strings.Contains(lockSelf.Header().Get("Location"), "error=") {
|
||||
t.Fatalf("administrator could lock their own account: %d %q", lockSelf.Code, lockSelf.Header().Get("Location"))
|
||||
}
|
||||
unlockUser := formRequest(t, s.http.Handler, "/admin/users/"+strconv.FormatInt(sara.ID, 10)+"/status", url.Values{
|
||||
"csrf": {csrf},
|
||||
"action": {"unlock"},
|
||||
}, session)
|
||||
if unlockUser.Code != http.StatusSeeOther || unlockUser.Header().Get("Location") != "/admin/users?flash=Account+unlocked" {
|
||||
t.Fatalf("unlock user: got %d location %q", unlockUser.Code, unlockUser.Header().Get("Location"))
|
||||
}
|
||||
if _, err := s.store.Authenticate("sara", "temporary-password"); err != nil {
|
||||
t.Fatalf("unlocked teammate could not authenticate: %v", err)
|
||||
}
|
||||
|
||||
checkIn := formRequest(t, s.http.Handler, "/attendance/check-in", url.Values{
|
||||
"csrf": {csrf},
|
||||
@@ -564,6 +682,15 @@ func TestSharedBoardCreateAssignMoveAndPermissions(t *testing.T) {
|
||||
t.Fatalf("board page does not include %q", expected)
|
||||
}
|
||||
}
|
||||
adminBoardRequest := httptest.NewRequest(http.MethodGet, "/board", nil)
|
||||
adminBoardRequest.AddCookie(adminCookie)
|
||||
adminBoardResponse := httptest.NewRecorder()
|
||||
s.http.Handler.ServeHTTP(adminBoardResponse, adminBoardRequest)
|
||||
for _, expected := range []string{"Archive", "Delete", "Delete permanently"} {
|
||||
if !strings.Contains(adminBoardResponse.Body.String(), expected) {
|
||||
t.Fatalf("card owner controls do not include %q", expected)
|
||||
}
|
||||
}
|
||||
|
||||
tagPath := "/board/tasks/" + strconv.FormatInt(tasks[0].ID, 10) + "/tags"
|
||||
setTags := formRequest(t, s.http.Handler, tagPath, url.Values{
|
||||
@@ -609,13 +736,53 @@ func TestSharedBoardCreateAssignMoveAndPermissions(t *testing.T) {
|
||||
t.Fatal("non-creator removed a board task")
|
||||
}
|
||||
|
||||
archivePath := "/board/tasks/" + strconv.FormatInt(tasks[0].ID, 10) + "/archive"
|
||||
deniedArchive := formRequest(t, s.http.Handler, archivePath, url.Values{"csrf": {memberCSRF}}, memberCookie)
|
||||
if deniedArchive.Code != http.StatusSeeOther || !strings.Contains(deniedArchive.Header().Get("Location"), "error=") {
|
||||
t.Fatalf("non-creator archive was not rejected: %d %q", deniedArchive.Code, deniedArchive.Header().Get("Location"))
|
||||
}
|
||||
adminArchive := formRequest(t, s.http.Handler, archivePath, url.Values{"csrf": {adminCSRF}}, adminCookie)
|
||||
if adminArchive.Code != http.StatusSeeOther || adminArchive.Header().Get("Location") != "/board?flash=Task+archived" {
|
||||
t.Fatalf("admin archive: got %d location %q", adminArchive.Code, adminArchive.Header().Get("Location"))
|
||||
}
|
||||
if active, _ := s.store.BoardTasks(); len(active) != 0 {
|
||||
t.Fatalf("archived card remained on active board: %#v", active)
|
||||
}
|
||||
archived, err := s.store.ArchivedBoardTasks()
|
||||
if err != nil || len(archived) != 1 || archived[0].Title != "Ship the onboarding flow" {
|
||||
t.Fatalf("archived cards: %#v, %v", archived, err)
|
||||
}
|
||||
archivedPageRequest := httptest.NewRequest(http.MethodGet, "/board", nil)
|
||||
archivedPageRequest.AddCookie(adminCookie)
|
||||
archivedPage := httptest.NewRecorder()
|
||||
s.http.Handler.ServeHTTP(archivedPage, archivedPageRequest)
|
||||
for _, expected := range []string{"Archived cards", "Ship the onboarding flow", "Restore", "Delete permanently"} {
|
||||
if !strings.Contains(archivedPage.Body.String(), expected) {
|
||||
t.Fatalf("archived card panel does not include %q", expected)
|
||||
}
|
||||
}
|
||||
restorePath := "/board/tasks/" + strconv.FormatInt(tasks[0].ID, 10) + "/restore"
|
||||
adminRestore := formRequest(t, s.http.Handler, restorePath, url.Values{"csrf": {adminCSRF}}, adminCookie)
|
||||
if adminRestore.Code != http.StatusSeeOther || adminRestore.Header().Get("Location") != "/board?flash=Task+restored" {
|
||||
t.Fatalf("admin restore: got %d location %q", adminRestore.Code, adminRestore.Header().Get("Location"))
|
||||
}
|
||||
if active, _ := s.store.BoardTasks(); len(active) != 1 {
|
||||
t.Fatalf("restored card did not return to board: %#v", active)
|
||||
}
|
||||
adminArchive = formRequest(t, s.http.Handler, archivePath, url.Values{"csrf": {adminCSRF}}, adminCookie)
|
||||
if adminArchive.Code != http.StatusSeeOther {
|
||||
t.Fatalf("second admin archive status: %d", adminArchive.Code)
|
||||
}
|
||||
adminDelete := formRequest(t, s.http.Handler, deletePath, url.Values{"csrf": {adminCSRF}}, adminCookie)
|
||||
if adminDelete.Code != http.StatusSeeOther {
|
||||
t.Fatalf("admin delete status: %d", adminDelete.Code)
|
||||
if adminDelete.Code != http.StatusSeeOther || adminDelete.Header().Get("Location") != "/board?flash=Task+permanently+deleted" {
|
||||
t.Fatalf("admin delete: got %d location %q", adminDelete.Code, adminDelete.Header().Get("Location"))
|
||||
}
|
||||
if remaining, _ := s.store.BoardTasks(); len(remaining) != 0 {
|
||||
t.Fatal("admin could not remove the board task")
|
||||
}
|
||||
if remaining, _ := s.store.ArchivedBoardTasks(); len(remaining) != 0 {
|
||||
t.Fatal("permanently deleted card remained in archive")
|
||||
}
|
||||
}
|
||||
|
||||
func formRequest(t *testing.T, handler http.Handler, path string, values url.Values, cookie *http.Cookie) *httptest.ResponseRecorder {
|
||||
|
||||
Reference in New Issue
Block a user