统一会员门禁逻辑并补权限分支测试

This commit is contained in:
hello-dd-code
2026-02-28 16:27:48 +08:00
parent 28690e2477
commit 01dd0ccbbe
7 changed files with 229 additions and 28 deletions
+23
View File
@@ -0,0 +1,23 @@
package service
import (
"context"
"fmt"
"time"
usermodel "wx_service/internal/model"
"gorm.io/gorm"
)
func hasActiveMembership(ctx context.Context, db *gorm.DB, miniProgramID uint, userID uint, now time.Time) (bool, error) {
var count int64
if err := db.WithContext(ctx).
Model(&usermodel.UserMembership{}).
Where("mini_program_id = ? AND user_id = ? AND status = ? AND ends_at > ?",
miniProgramID, userID, "active", now).
Count(&count).Error; err != nil {
return false, fmt.Errorf("check membership: %w", err)
}
return count > 0, nil
}
@@ -0,0 +1,104 @@
package service
import (
"context"
"errors"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func newMockGormDB(t *testing.T) (*gorm.DB, sqlmock.Sqlmock, func()) {
t.Helper()
sqlDB, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("sqlmock.New: %v", err)
}
gdb, err := gorm.Open(mysql.New(mysql.Config{
Conn: sqlDB,
SkipInitializeWithVersion: true,
}), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
_ = sqlDB.Close()
t.Fatalf("gorm.Open: %v", err)
}
cleanup := func() {
_ = sqlDB.Close()
}
return gdb, mock, cleanup
}
func TestHasActiveMembership(t *testing.T) {
t.Parallel()
now := time.Date(2026, 2, 28, 16, 0, 0, 0, time.Local)
db, mock, cleanup := newMockGormDB(t)
defer cleanup()
mock.ExpectQuery("SELECT count\\(\\*\\) FROM `user_memberships`").
WithArgs(uint(100), uint(200), "active", now).
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
ok, err := hasActiveMembership(context.Background(), db, 100, 200, now)
if err != nil {
t.Fatalf("hasActiveMembership: %v", err)
}
if !ok {
t.Fatalf("hasActiveMembership got=false, want=true")
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("unmet expectations: %v", err)
}
}
func TestHasActiveMembershipNotFound(t *testing.T) {
t.Parallel()
now := time.Date(2026, 2, 28, 16, 0, 0, 0, time.Local)
db, mock, cleanup := newMockGormDB(t)
defer cleanup()
mock.ExpectQuery("SELECT count\\(\\*\\) FROM `user_memberships`").
WithArgs(uint(101), uint(201), "active", now).
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
ok, err := hasActiveMembership(context.Background(), db, 101, 201, now)
if err != nil {
t.Fatalf("hasActiveMembership: %v", err)
}
if ok {
t.Fatalf("hasActiveMembership got=true, want=false")
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("unmet expectations: %v", err)
}
}
func TestHasActiveMembershipDBError(t *testing.T) {
t.Parallel()
now := time.Date(2026, 2, 28, 16, 0, 0, 0, time.Local)
db, mock, cleanup := newMockGormDB(t)
defer cleanup()
mock.ExpectQuery("SELECT count\\(\\*\\) FROM `user_memberships`").
WithArgs(uint(102), uint(202), "active", now).
WillReturnError(errors.New("db unavailable"))
_, err := hasActiveMembership(context.Background(), db, 102, 202, now)
if err == nil {
t.Fatalf("expected error when query fails")
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("unmet expectations: %v", err)
}
}
@@ -189,7 +189,7 @@ func (s *SmokeAIAdviceService) getCached(ctx context.Context, uid int, adviceTyp
}
func (s *SmokeAIAdviceService) isAllowed(ctx context.Context, user *usermodel.User, adviceDate time.Time) (bool, error) {
isVIP, err := s.isVIP(ctx, user)
isVIP, err := hasActiveMembership(ctx, s.db, user.MiniProgramID, user.ID, time.Now())
if err != nil {
return false, err
}
@@ -199,19 +199,6 @@ func (s *SmokeAIAdviceService) isAllowed(ctx context.Context, user *usermodel.Us
return s.isUnlocked(ctx, int(user.ID), adviceDate)
}
func (s *SmokeAIAdviceService) isVIP(ctx context.Context, user *usermodel.User) (bool, error) {
now := time.Now()
var count int64
if err := s.db.WithContext(ctx).
Model(&usermodel.UserMembership{}).
Where("mini_program_id = ? AND user_id = ? AND status = ? AND ends_at > ?",
user.MiniProgramID, user.ID, "active", now).
Count(&count).Error; err != nil {
return false, fmt.Errorf("check vip: %w", err)
}
return count > 0, nil
}
func (s *SmokeAIAdviceService) isUnlocked(ctx context.Context, uid int, adviceDate time.Time) (bool, error) {
startOfDay := dateOnly(adviceDate)
var unlock smokemodel.SmokeAIAdviceUnlock
@@ -538,7 +538,7 @@ func (s *SmokeAINextSmokeService) loadRecent3Days(ctx context.Context, uid int,
}
func (s *SmokeAINextSmokeService) isAllowed(ctx context.Context, user *usermodel.User, planDate time.Time) (bool, error) {
isVIP, err := s.isVIP(ctx, user)
isVIP, err := hasActiveMembership(ctx, s.db, user.MiniProgramID, user.ID, time.Now())
if err != nil {
return false, err
}
@@ -548,19 +548,6 @@ func (s *SmokeAINextSmokeService) isAllowed(ctx context.Context, user *usermodel
return s.isUnlocked(ctx, int(user.ID), planDate)
}
func (s *SmokeAINextSmokeService) isVIP(ctx context.Context, user *usermodel.User) (bool, error) {
now := time.Now()
var count int64
if err := s.db.WithContext(ctx).
Model(&usermodel.UserMembership{}).
Where("mini_program_id = ? AND user_id = ? AND status = ? AND ends_at > ?",
user.MiniProgramID, user.ID, "active", now).
Count(&count).Error; err != nil {
return false, fmt.Errorf("check vip: %w", err)
}
return count > 0, nil
}
func (s *SmokeAINextSmokeService) isUnlocked(ctx context.Context, uid int, planDate time.Time) (bool, error) {
startOfDay := dateOnly(planDate)
var unlock smokemodel.SmokeAIAdviceUnlock
@@ -0,0 +1,96 @@
package service
import (
"context"
"testing"
"time"
"github.com/DATA-DOG/go-sqlmock"
usermodel "wx_service/internal/model"
)
func TestSmokeAIAdviceServiceIsAllowedMember(t *testing.T) {
t.Parallel()
db, mock, cleanup := newMockGormDB(t)
defer cleanup()
svc := &SmokeAIAdviceService{db: db}
user := &usermodel.User{ID: 200, MiniProgramID: 100}
adviceDate := time.Date(2026, 3, 1, 0, 0, 0, 0, time.Local)
mock.ExpectQuery("SELECT count\\(\\*\\) FROM `user_memberships`").
WithArgs(uint(100), uint(200), "active", sqlmock.AnyArg()).
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(1))
allowed, err := svc.isAllowed(context.Background(), user, adviceDate)
if err != nil {
t.Fatalf("isAllowed: %v", err)
}
if !allowed {
t.Fatalf("isAllowed got=false, want=true for active member")
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("unmet expectations: %v", err)
}
}
func TestSmokeAIAdviceServiceIsAllowedNonMemberLocked(t *testing.T) {
t.Parallel()
db, mock, cleanup := newMockGormDB(t)
defer cleanup()
svc := &SmokeAIAdviceService{db: db}
user := &usermodel.User{ID: 201, MiniProgramID: 101}
adviceDate := time.Date(2026, 3, 1, 0, 0, 0, 0, time.Local)
mock.ExpectQuery("SELECT count\\(\\*\\) FROM `user_memberships`").
WithArgs(uint(101), uint(201), "active", sqlmock.AnyArg()).
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
mock.ExpectQuery("SELECT \\* FROM `fa_smoke_ai_advice_unlocks`").
WithArgs(201, adviceDate.Format("2006-01-02"), sqlmock.AnyArg()).
WillReturnRows(sqlmock.NewRows([]string{"id", "uid", "unlock_date", "ad_watched_at"}))
allowed, err := svc.isAllowed(context.Background(), user, adviceDate)
if err != nil {
t.Fatalf("isAllowed: %v", err)
}
if allowed {
t.Fatalf("isAllowed got=true, want=false for non-member locked user")
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("unmet expectations: %v", err)
}
}
func TestSmokeAINextSmokeServiceIsAllowedNonMemberUnlocked(t *testing.T) {
t.Parallel()
db, mock, cleanup := newMockGormDB(t)
defer cleanup()
svc := &SmokeAINextSmokeService{db: db}
user := &usermodel.User{ID: 202, MiniProgramID: 102}
planDate := time.Date(2026, 3, 1, 0, 0, 0, 0, time.Local)
mock.ExpectQuery("SELECT count\\(\\*\\) FROM `user_memberships`").
WithArgs(uint(102), uint(202), "active", sqlmock.AnyArg()).
WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0))
mock.ExpectQuery("SELECT \\* FROM `fa_smoke_ai_advice_unlocks`").
WithArgs(202, planDate.Format("2006-01-02"), sqlmock.AnyArg()).
WillReturnRows(sqlmock.NewRows([]string{"id", "uid", "unlock_date", "ad_watched_at"}).
AddRow(1, 202, planDate, time.Now()))
allowed, err := svc.isAllowed(context.Background(), user, planDate)
if err != nil {
t.Fatalf("isAllowed: %v", err)
}
if !allowed {
t.Fatalf("isAllowed got=false, want=true for non-member unlocked user")
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Fatalf("unmet expectations: %v", err)
}
}