package app import ( "net/http" "net/http/httptest" "net/url" "path/filepath" "regexp" "strings" "testing" ) func TestLoginAttendanceAndReportFlow(t *testing.T) { s, err := New(Config{ Addr: ":0", BaseURL: "http://example.test", DatabasePath: filepath.Join(t.TempDir(), "test.db"), }) if err != nil { t.Fatal(err) } defer s.Close() login := formRequest(t, s.http.Handler, "/login", url.Values{ "username": {"admin"}, "password": {"admin123"}, }, nil) if login.Code != http.StatusSeeOther { t.Fatalf("login status: got %d, body %s", login.Code, login.Body.String()) } cookies := login.Result().Cookies() if len(cookies) == 0 { t.Fatal("login did not set a session cookie") } session := cookies[0] dashboardReq := httptest.NewRequest(http.MethodGet, "/", nil) dashboardReq.AddCookie(session) dashboard := httptest.NewRecorder() s.http.Handler.ServeHTTP(dashboard, dashboardReq) if dashboard.Code != http.StatusOK || !strings.Contains(dashboard.Body.String(), "PERSIAN CALENDAR") { t.Fatalf("dashboard status/body: %d %s", dashboard.Code, dashboard.Body.String()) } if !strings.Contains(dashboard.Body.String(), "/day?date=") { t.Fatal("dashboard calendar days do not link to the team day view") } csrf := extractCSRF(t, dashboard.Body.String()) createUser := formRequest(t, s.http.Handler, "/admin/users", url.Values{ "csrf": {csrf}, "username": {"sara"}, "password": {"temporary-password"}, "display_name": {"Sara Ahmadi"}, "email": {"sara@example.test"}, "role": {"member"}, }, session) if createUser.Code != http.StatusSeeOther { t.Fatalf("create user status: got %d, body %s", createUser.Code, createUser.Body.String()) } if _, err := s.store.Authenticate("sara", "temporary-password"); err != nil { t.Fatalf("created teammate could not authenticate: %v", err) } checkIn := formRequest(t, s.http.Handler, "/attendance/check-in", url.Values{ "csrf": {csrf}, "mode": {"office"}, }, session) if checkIn.Code != http.StatusSeeOther { t.Fatalf("check-in status: got %d, body %s", checkIn.Code, checkIn.Body.String()) } reportReq := httptest.NewRequest(http.MethodGet, "/reports/attendance.csv?start=2020-01-01&end=2030-01-01", nil) reportReq.AddCookie(session) report := httptest.NewRecorder() s.http.Handler.ServeHTTP(report, reportReq) if report.Code != http.StatusOK { t.Fatalf("report status: got %d, body %s", report.Code, report.Body.String()) } if got := report.Header().Get("Content-Type"); !strings.Contains(got, "text/csv") { t.Fatalf("report content type: %s", got) } if !strings.Contains(report.Body.String(), "Workspace Admin") { t.Fatalf("report did not contain attendance row: %s", report.Body.String()) } } func TestPersianRequestDateParsing(t *testing.T) { got, ok := parseUserDate("1405-05-06") if !ok || got != "2026-07-28" { t.Fatalf("got %q, %v; want 2026-07-28, true", got, ok) } if _, ok := parseUserDate("1400-12-30"); ok { t.Fatal("accepted an invalid non-leap Esfand date") } } func TestReviewedRequestCanBeListed(t *testing.T) { s, err := New(Config{ Addr: ":0", BaseURL: "http://example.test", DatabasePath: filepath.Join(t.TempDir(), "review.db"), }) if err != nil { t.Fatal(err) } defer s.Close() if err := s.store.CreateRequest(1, "leave", "2026-07-28", "2026-07-29", "Personal"); err != nil { t.Fatal(err) } pending, err := s.store.Requests(1, true, "pending") if err != nil || len(pending) != 1 { t.Fatalf("pending requests: %#v, %v", pending, err) } if err := s.store.ReviewRequest(t.Context(), pending[0].ID, 1, "approved", "Approved"); err != nil { t.Fatal(err) } reviewed, err := s.store.Requests(1, true, "") if err != nil { t.Fatalf("listing reviewed request failed: %v", err) } if len(reviewed) != 1 || reviewed[0].Status != "approved" { t.Fatalf("unexpected reviewed requests: %#v", reviewed) } token, _, err := s.store.CreateSession(1) if err != nil { t.Fatal(err) } adminRequest := httptest.NewRequest(http.MethodGet, "/admin/requests?flash=Request+reviewed", nil) adminRequest.AddCookie(&http.Cookie{Name: "teammate_session", Value: token}) adminResponse := httptest.NewRecorder() s.http.Handler.ServeHTTP(adminResponse, adminRequest) if adminResponse.Code != http.StatusOK || !strings.Contains(adminResponse.Body.String(), "Request reviewed") { t.Fatalf("review redirect page: got %d, body %s", adminResponse.Code, adminResponse.Body.String()) } } func TestPublicRegistrationAndIdentifierLogin(t *testing.T) { s, err := New(Config{ Addr: ":0", BaseURL: "http://example.test", DatabasePath: filepath.Join(t.TempDir(), "registration.db"), }) if err != nil { t.Fatal(err) } defer s.Close() healthRequest := httptest.NewRequest(http.MethodGet, "/healthz", nil) healthResponse := httptest.NewRecorder() s.http.Handler.ServeHTTP(healthResponse, healthRequest) if healthResponse.Code != http.StatusOK || healthResponse.Body.String() != `{"status":"ok"}` { t.Fatalf("health endpoint: got %d, body %s", healthResponse.Code, healthResponse.Body.String()) } registerPage := httptest.NewRequest(http.MethodGet, "/register", nil) registerView := httptest.NewRecorder() s.http.Handler.ServeHTTP(registerView, registerPage) if registerView.Code != http.StatusOK || !strings.Contains(registerView.Body.String(), "Create your account") { t.Fatalf("register page: got %d, body %s", registerView.Code, registerView.Body.String()) } for _, expected := range []string{"Vazirmatn-font-face.css", "/static/theme.js", "data-theme-toggle"} { if !strings.Contains(registerView.Body.String(), expected) { t.Fatalf("register page does not include %q", expected) } } themeRequest := httptest.NewRequest(http.MethodGet, "/static/theme.js", nil) themeResponse := httptest.NewRecorder() s.http.Handler.ServeHTTP(themeResponse, themeRequest) if themeResponse.Code != http.StatusOK || !strings.Contains(themeResponse.Body.String(), "hamkar-theme") { t.Fatalf("theme asset: got %d, body %s", themeResponse.Code, themeResponse.Body.String()) } register := formRequest(t, s.http.Handler, "/register", url.Values{ "display_name": {"Neda Karimi"}, "email": {"neda@example.test"}, "password": {"secure-password"}, "password_confirm": {"secure-password"}, }, nil) if register.Code != http.StatusSeeOther || register.Header().Get("Location") != "/?flash=Welcome+to+Hamkar" { t.Fatalf("registration: got %d location %q body %s", register.Code, register.Header().Get("Location"), register.Body.String()) } if len(register.Result().Cookies()) == 0 { t.Fatal("registration did not sign the new member in") } user, err := s.store.Authenticate("neda@example.test", "secure-password") if err != nil { t.Fatalf("email login failed: %v", err) } if user.Username != "neda" || user.Role != "member" { t.Fatalf("unexpected registered user: %#v", user) } if _, err := s.store.Authenticate("neda", "secure-password"); err != nil { t.Fatalf("generated username login failed: %v", err) } } func TestRequestPageIncludesJalaliPicker(t *testing.T) { s, err := New(Config{ Addr: ":0", BaseURL: "http://example.test", DatabasePath: filepath.Join(t.TempDir(), "picker.db"), }) if err != nil { t.Fatal(err) } defer s.Close() token, _, err := s.store.CreateSession(1) if err != nil { t.Fatal(err) } pageRequest := httptest.NewRequest(http.MethodGet, "/requests", nil) pageRequest.AddCookie(&http.Cookie{Name: "teammate_session", Value: token}) pageResponse := httptest.NewRecorder() s.http.Handler.ServeHTTP(pageResponse, pageRequest) if pageResponse.Code != http.StatusOK { t.Fatalf("request page status: %d", pageResponse.Code) } for _, expected := range []string{"/static/jalali-picker.js", "data-jalali-picker", "jalali-trigger"} { if !strings.Contains(pageResponse.Body.String(), expected) { t.Fatalf("request page does not include %q", expected) } } assetRequest := httptest.NewRequest(http.MethodGet, "/static/jalali-picker.js", nil) assetResponse := httptest.NewRecorder() s.http.Handler.ServeHTTP(assetResponse, assetRequest) if assetResponse.Code != http.StatusOK || !strings.Contains(assetResponse.Body.String(), "gregorianToJalali") { t.Fatalf("picker asset: got %d, body %s", assetResponse.Code, assetResponse.Body.String()) } } func TestTeamDayShowsPresentRemoteAndAbsent(t *testing.T) { s, err := New(Config{ Addr: ":0", BaseURL: "http://example.test", DatabasePath: filepath.Join(t.TempDir(), "day.db"), }) if err != nil { t.Fatal(err) } defer s.Close() remoteID, err := s.store.CreateUser("remote-user", "secure-password", "Remote Teammate", "remote@example.test", "member") if err != nil { t.Fatal(err) } leaveID, err := s.store.CreateUser("leave-user", "secure-password", "Absent Teammate", "leave@example.test", "member") if err != nil { t.Fatal(err) } const selectedDay = "2030-01-02" if err := s.store.CheckIn(1, selectedDay, "office"); err != nil { t.Fatal(err) } if err := s.store.CreateRequest(remoteID, "remote", selectedDay, selectedDay, "Working from home"); err != nil { t.Fatal(err) } if err := s.store.CreateRequest(leaveID, "leave", selectedDay, selectedDay, "Holiday"); err != nil { t.Fatal(err) } for _, userID := range []int64{remoteID, leaveID} { requests, err := s.store.Requests(userID, false, "pending") if err != nil || len(requests) != 1 { t.Fatalf("pending request for %d: %#v, %v", userID, requests, err) } if err := s.store.ReviewRequest(t.Context(), requests[0].ID, 1, "approved", "Approved"); err != nil { t.Fatal(err) } } token, _, err := s.store.CreateSession(1) if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodGet, "/day?date="+selectedDay, nil) request.AddCookie(&http.Cookie{Name: "teammate_session", Value: token}) response := httptest.NewRecorder() s.http.Handler.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("day page status: %d, body %s", response.Code, response.Body.String()) } for _, expected := range []string{ "Workspace Admin", "Remote Teammate", "Absent Teammate", "Office ยท checked in", "Approved remote day", "Approved time off", `class="presence-badge present"`, `class="presence-badge remote"`, `class="presence-badge absent"`, } { if !strings.Contains(response.Body.String(), expected) { t.Fatalf("day page does not include %q", expected) } } } func formRequest(t *testing.T, handler http.Handler, path string, values url.Values, cookie *http.Cookie) *httptest.ResponseRecorder { t.Helper() req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(values.Encode())) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") if cookie != nil { req.AddCookie(cookie) } rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) return rec } func extractCSRF(t *testing.T, body string) string { t.Helper() re := regexp.MustCompile(`name="csrf" value="([^"]+)"`) match := re.FindStringSubmatch(body) if len(match) != 2 { t.Fatal("page did not contain a CSRF token") } return match[1] }