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
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-13 08:59:13 +00:00
|
|
|
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 }
|
2026-07-13 01:15:30 +00:00
|
|
|
func (m *mwRepo) ListPage(context.Context, int, int, string, string) ([]*domain.Member, int64, error) {
|
|
|
|
|
return nil, 0, nil
|
|
|
|
|
}
|
2026-07-13 08:59:13 +00:00
|
|
|
func (m *mwRepo) ListAllMembers(context.Context) ([]*domain.Member, error) { return nil, nil }
|
|
|
|
|
func (m *mwRepo) CountActiveAdmins(context.Context) (int64, error) { return 0, nil }
|
2026-07-13 01:15:30 +00:00
|
|
|
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
|
|
|
|
|
}
|
2026-07-15 15:23:59 +00:00
|
|
|
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) {}
|
2026-07-13 01:15:30 +00:00
|
|
|
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)
|
|
|
|
|
}
|