package middleware_test // T119 HTTP middleware cases: AU-05 / AU-06 / AU-12 / AU-13 / AD-13 import ( "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "apps/backend/internal/middleware" "apps/backend/internal/module/member/domain" tokenUC "apps/backend/internal/module/token/usecase" "apps/backend/internal/response" "github.com/stretchr/testify/require" ) // mwRepo is a minimal Repository for AuthMiddleware tests. type mwRepo struct { byUID map[int64]*domain.Member } func (m *mwRepo) FindByUID(_ context.Context, uid int64) (*domain.Member, error) { x, ok := m.byUID[uid] if !ok { return nil, domain.ErrNotFound } cp := *x return &cp, nil } func (m *mwRepo) NextUID(context.Context) (int64, error) { return 0, nil } func (m *mwRepo) CreateMember(context.Context, *domain.Member) error { return nil } func (m *mwRepo) FindByEmail(context.Context, string) (*domain.Member, error) { return nil, domain.ErrNotFound } func (m *mwRepo) FindByPhone(context.Context, string) (*domain.Member, error) { return nil, domain.ErrNotFound } func (m *mwRepo) FindByInviteCode(context.Context, string) (*domain.Member, error) { return nil, domain.ErrNotFound } func (m *mwRepo) UpdateMember(context.Context, *domain.Member) error { return nil } func (m *mwRepo) ListPage(context.Context, int, int, string, string) ([]*domain.Member, int64, error) { return nil, 0, nil } func (m *mwRepo) ListAllMembers(context.Context) ([]*domain.Member, error) { return nil, nil } func (m *mwRepo) CountActiveAdmins(context.Context) (int64, error) { return 0, nil } func (m *mwRepo) CreateIdentity(context.Context, *domain.Identity) error { return nil } func (m *mwRepo) FindIdentity(context.Context, string, string) (*domain.Identity, error) { return nil, domain.ErrIdentityNotFound } func (m *mwRepo) ListIdentitiesByUID(context.Context, int64) ([]*domain.Identity, error) { return nil, nil } func (m *mwRepo) DeleteIdentity(context.Context, string, string) error { return nil } func (m *mwRepo) UpdateIdentityPassword(context.Context, string, string, string) error { return nil } func (m *mwRepo) SetCode(string, string, string, time.Duration) bool { return true } func (m *mwRepo) CheckCode(string, string, string) bool { return false } func (m *mwRepo) PeekCode(string, string, string) bool { return false } func (m *mwRepo) SaveRefresh(string, int64, time.Time) {} func (m *mwRepo) ConsumeRefresh(string) (int64, bool) { return 0, false } func (m *mwRepo) RevokeRefresh(string) {} func (m *mwRepo) RevokeAllRefreshByUID(context.Context, int64) error { return nil } func (m *mwRepo) GetSettings(context.Context, int64) (*domain.UserSettings, error) { return nil, nil } func (m *mwRepo) SaveSettings(context.Context, int64, *domain.UserSettings) error { return nil } func decodeEnv(t *testing.T, rr *httptest.ResponseRecorder) response.Envelope { t.Helper() var env response.Envelope require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &env)) return env } func TestAU_05_MeMissingAuthorization(t *testing.T) { issuer := tokenUC.NewJWTIssuer("mw-a", "mw-r", 3600, 86400) repo := &mwRepo{byUID: map[int64]*domain.Member{}} h := middleware.AuthMiddleware(issuer, repo)(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(`{"code":102000}`)) }) rr := httptest.NewRecorder() h.ServeHTTP(rr, httptest.NewRequest(http.MethodGet, "/api/v1/auth/me", nil)) require.Equal(t, http.StatusUnauthorized, rr.Code) env := decodeEnv(t, rr) require.NotEqual(t, 102000, env.Code) require.NotEmpty(t, env.Message) } func TestAU_06_MeExpiredAccess(t *testing.T) { // access expire 1 second — issue then sleep issuer := tokenUC.NewJWTIssuer("mw-a2", "mw-r2", 1, 86400) uid := int64(1_000_100) repo := &mwRepo{byUID: map[int64]*domain.Member{ uid: {UID: uid, Email: "exp@test.local", Roles: []string{domain.RoleMember}, Status: domain.StatusActive}, }} pair, _, _, err := issuer.Issue(uid, []string{domain.RoleMember}) require.NoError(t, err) time.Sleep(1100 * time.Millisecond) h := middleware.AuthMiddleware(issuer, repo)(func(w http.ResponseWriter, r *http.Request) { t.Fatal("should not reach handler") }) req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/me", nil) req.Header.Set("Authorization", "Bearer "+pair.AccessToken) rr := httptest.NewRecorder() h.ServeHTTP(rr, req) require.Equal(t, http.StatusUnauthorized, rr.Code) env := decodeEnv(t, rr) require.NotEqual(t, 102000, env.Code) } func TestAU_12_NonAdminForbidden(t *testing.T) { issuer := tokenUC.NewJWTIssuer("mw-a3", "mw-r3", 3600, 86400) uid := int64(1_000_101) repo := &mwRepo{byUID: map[int64]*domain.Member{ uid: {UID: uid, Email: "mem@test.local", Roles: []string{domain.RoleMember}, Status: domain.StatusActive}, }} pair, _, _, err := issuer.Issue(uid, []string{domain.RoleMember}) require.NoError(t, err) inner := middleware.AdminMiddleware(func(w http.ResponseWriter, r *http.Request) { t.Fatal("member must not pass admin middleware") }) h := middleware.AuthMiddleware(issuer, repo)(inner) req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/members", nil) req.Header.Set("Authorization", "Bearer "+pair.AccessToken) rr := httptest.NewRecorder() h.ServeHTTP(rr, req) require.Equal(t, http.StatusForbidden, rr.Code) env := decodeEnv(t, rr) require.EqualValues(t, 403002, env.Code) } func TestAU_13_AdminAllowed(t *testing.T) { issuer := tokenUC.NewJWTIssuer("mw-a4", "mw-r4", 3600, 86400) uid := int64(1_000_102) repo := &mwRepo{byUID: map[int64]*domain.Member{ uid: {UID: uid, Email: "adm@test.local", Roles: []string{domain.RoleAdmin, domain.RoleMember}, Status: domain.StatusActive}, }} pair, _, _, err := issuer.Issue(uid, []string{domain.RoleAdmin, domain.RoleMember}) require.NoError(t, err) called := false inner := middleware.AdminMiddleware(func(w http.ResponseWriter, r *http.Request) { called = true response.OK(w, map[string]bool{"ok": true}) }) h := middleware.AuthMiddleware(issuer, repo)(inner) req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/members", nil) req.Header.Set("Authorization", "Bearer "+pair.AccessToken) rr := httptest.NewRecorder() h.ServeHTTP(rr, req) require.True(t, called) require.Equal(t, http.StatusOK, rr.Code) env := decodeEnv(t, rr) require.EqualValues(t, 102000, env.Code) } func TestAD_13_MemberCreateMemberForbidden(t *testing.T) { // Same stack as AU-12: member hits admin route → 403002 TestAU_12_NonAdminForbidden(t) }