package handler import ( "bytes" "context" "encoding/json" "log/slog" "errors" "net/http" "net/http/httptest" "testing" "gitea.michaelthomson.dev/mthomson/habits/internal/auth/service" ) type MockLoginer struct { LoginFunc func(ctx context.Context, email string, password string) (string, error) } func (m *MockLoginer) Login(ctx context.Context, email string, password string) (string, error) { return m.LoginFunc(ctx, email, password) } func TestLogin(t *testing.T) { logger := slog.Default() t.Run("returns 200 for existing user with correct credentials", func(t *testing.T) { loginRequest := LoginRequest{Email: "test@test.com", Password: "password"} token := "examplejwt" service := MockLoginer{ LoginFunc: func(ctx context.Context, email string, password string) (string, error) { return token, nil }, } handler := HandleLogin(logger, &service) requestBody, err := json.Marshal(loginRequest) if err != nil { t.Fatalf("Failed to marshal request %+v: %v", loginRequest, err) } req := httptest.NewRequest(http.MethodPost, "/login", bytes.NewBuffer(requestBody)) res := httptest.NewRecorder() handler(res, req) AssertStatusCodes(t, res.Code, http.StatusOK) AssertToken(t, res, token) }) t.Run("returns 401 for existing user with incorrect credentials", func(t *testing.T) { loginRequest := LoginRequest{Email: "test@test.com", Password: "password"} service := MockLoginer{ LoginFunc: func(ctx context.Context, email string, password string) (string, error) { return "", service.ErrUnauthorized }, } handler := HandleLogin(logger, &service) requestBody, err := json.Marshal(loginRequest) if err != nil { t.Fatalf("Failed to marshal request %+v: %v", loginRequest, err) } req := httptest.NewRequest(http.MethodPost, "/login", bytes.NewBuffer(requestBody)) res := httptest.NewRecorder() handler(res, req) AssertStatusCodes(t, res.Code, http.StatusUnauthorized) }) t.Run("returns 401 for non-existing user", func(t *testing.T) { loginRequest := LoginRequest{Email: "test@test.com", Password: "password"} service := MockLoginer{ LoginFunc: func(ctx context.Context, email string, password string) (string, error) { return "", service.ErrNotFound }, } handler := HandleLogin(logger, &service) requestBody, err := json.Marshal(loginRequest) if err != nil { t.Fatalf("Failed to marshal request %+v: %v", loginRequest, err) } req := httptest.NewRequest(http.MethodPost, "/login", bytes.NewBuffer(requestBody)) res := httptest.NewRecorder() handler(res, req) AssertStatusCodes(t, res.Code, http.StatusUnauthorized) }) t.Run("returns 400 with bad json", func(t *testing.T) { handler := HandleLogin(logger, nil) badStruct := struct { Foo string }{ Foo: "bar", } requestBody, err := json.Marshal(badStruct) if err != nil { t.Fatalf("Failed to marshal request %+v: %v", badStruct, err) } req := httptest.NewRequest(http.MethodPost, "/login", bytes.NewBuffer(requestBody)) res := httptest.NewRecorder() handler(res, req) AssertStatusCodes(t, res.Code, http.StatusBadRequest) }) t.Run("returns 500 arbitrary errors", func(t *testing.T) { loginRequest := LoginRequest{Email: "test@test.com", Password: "password"} service := MockLoginer{ LoginFunc: func(ctx context.Context, email string, password string) (string, error) { return "", errors.New("foo bar") }, } handler := HandleLogin(logger, &service) requestBody, err := json.Marshal(loginRequest) if err != nil { t.Fatalf("Failed to marshal request %+v: %v", loginRequest, err) } req := httptest.NewRequest(http.MethodPost, "/login", bytes.NewBuffer(requestBody)) res := httptest.NewRecorder() handler(res, req) AssertStatusCodes(t, res.Code, http.StatusInternalServerError) }) } func AssertStatusCodes(t testing.TB, got, want int) { t.Helper() if got != want { t.Errorf("got status code: %v, want status code: %v", want, got) } } func AssertToken(t testing.TB, res *httptest.ResponseRecorder, want string) { t.Helper() got := res.Result().Cookies()[0].Value if got != want { t.Errorf("got cookie: %q, want cookie: %q", got, want) } }