You've already forked agentic-coding-workflow
102 lines
3.4 KiB
Go
102 lines
3.4 KiB
Go
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())
|
|
}
|