package user import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" "github.com/google/uuid" "github.com/stretchr/testify/require" ) // mountHandler builds a chi router with the user Handler attached. Helper // is shared by every handler test so the route prefix /api/v1/auth lives in // exactly one place; if the URL contract ever shifts the change is local. func mountHandler(svc Service) http.Handler { r := chi.NewRouter() h := NewHandler(svc) h.Mount(r) return r } // TestHandler_LoginThenMeThenLogout walks the full happy path of the auth // surface: log in, hit /me with the issued token, log out, and confirm the // token is no longer accepted. It exercises the integration between Handler, // Service, the in-memory fakeRepo and the Auth middleware. func TestHandler_LoginThenMeThenLogout(t *testing.T) { repo := newFakeRepo() hash, err := HashPassword("password123") require.NoError(t, err) repo.users["a@b.c"] = &User{ ID: uuid.New(), Email: "a@b.c", DisplayName: "T", PasswordHash: hash, } svc := NewService(repo, nil) srv := httptest.NewServer(mountHandler(svc)) defer srv.Close() // 1. login body, err := json.Marshal(map[string]string{"email": "a@b.c", "password": "password123"}) require.NoError(t, err) resp, err := http.Post(srv.URL+"/api/v1/auth/login", "application/json", bytes.NewReader(body)) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) var loginOut struct { Token string `json:"token"` User struct { ID string `json:"id"` Email string `json:"email"` } `json:"user"` } require.NoError(t, json.NewDecoder(resp.Body).Decode(&loginOut)) require.NoError(t, resp.Body.Close()) require.NotEmpty(t, loginOut.Token) require.Equal(t, "a@b.c", loginOut.User.Email) // 2. me req, err := http.NewRequest(http.MethodGet, srv.URL+"/api/v1/auth/me", nil) require.NoError(t, err) req.Header.Set("Authorization", "Bearer "+loginOut.Token) resp, err = http.DefaultClient.Do(req) require.NoError(t, err) require.Equal(t, http.StatusOK, resp.StatusCode) require.NoError(t, resp.Body.Close()) // 3. logout req, err = http.NewRequest(http.MethodPost, srv.URL+"/api/v1/auth/logout", nil) require.NoError(t, err) req.Header.Set("Authorization", "Bearer "+loginOut.Token) resp, err = http.DefaultClient.Do(req) require.NoError(t, err) require.Equal(t, http.StatusNoContent, resp.StatusCode) require.NoError(t, resp.Body.Close()) // 4. me again -> 401: the same bearer must be rejected after logout. req, err = http.NewRequest(http.MethodGet, srv.URL+"/api/v1/auth/me", nil) require.NoError(t, err) req.Header.Set("Authorization", "Bearer "+loginOut.Token) resp, err = http.DefaultClient.Do(req) require.NoError(t, err) require.Equal(t, http.StatusUnauthorized, resp.StatusCode) require.NoError(t, resp.Body.Close()) } // TestHandler_LoginInvalidPayload covers the validation branch: an empty // JSON object lacks both email and password and must be rejected with 400 // before the service layer is consulted. func TestHandler_LoginInvalidPayload(t *testing.T) { svc := NewService(newFakeRepo(), nil) srv := httptest.NewServer(mountHandler(svc)) defer srv.Close() resp, err := http.Post(srv.URL+"/api/v1/auth/login", "application/json", bytes.NewReader([]byte("{}"))) require.NoError(t, err) require.Equal(t, http.StatusBadRequest, resp.StatusCode) require.NoError(t, resp.Body.Close()) }