thread-master/apps/backend/internal/middleware/m1_t119_auth_test.go

166 lines
6.2 KiB
Go
Raw Normal View History

2026-07-13 01:15:30 +00:00
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) 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) 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) {}
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)
}