176 lines
8.5 KiB
Go
176 lines
8.5 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"google.golang.org/grpc/metadata"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
_ "modernc.org/sqlite"
|
|
appv1 "stream.api/internal/api/proto/app/v1"
|
|
"stream.api/internal/database/model"
|
|
"stream.api/internal/database/query"
|
|
"stream.api/internal/middleware"
|
|
"stream.api/internal/repository"
|
|
)
|
|
|
|
type serviceTestLogger struct{}
|
|
|
|
func (serviceTestLogger) Info(string, ...any) {}
|
|
func (serviceTestLogger) Error(string, ...any) {}
|
|
func (serviceTestLogger) Debug(string, ...any) {}
|
|
func (serviceTestLogger) Warn(string, ...any) {}
|
|
|
|
const notificationTestTrustedMarker = "trusted-notification-test-marker"
|
|
|
|
type publishedNotificationEvent struct {
|
|
notification *model.Notification
|
|
}
|
|
|
|
type fakeNotificationEventPublisher struct {
|
|
events []publishedNotificationEvent
|
|
}
|
|
|
|
func (f *fakeNotificationEventPublisher) PublishNotificationCreated(_ context.Context, notification *model.Notification) error {
|
|
copyNotification := *notification
|
|
f.events = append(f.events, publishedNotificationEvent{notification: ©Notification})
|
|
return nil
|
|
}
|
|
|
|
func newNotificationTestDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", uuid.NewString())
|
|
db, err := gorm.Open(sqlite.Dialector{DriverName: "sqlite", DSN: dsn}, &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open sqlite db: %v", err)
|
|
}
|
|
for _, stmt := range []string{
|
|
`CREATE TABLE user (id TEXT PRIMARY KEY,email TEXT NOT NULL,password TEXT,username TEXT,avatar TEXT,role TEXT NOT NULL,google_id TEXT,storage_used INTEGER NOT NULL DEFAULT 0,plan_id TEXT,referred_by_user_id TEXT,referral_eligible BOOLEAN NOT NULL DEFAULT 1,referral_reward_bps INTEGER,referral_reward_granted_at DATETIME,referral_reward_payment_id TEXT,referral_reward_amount REAL,created_at DATETIME DEFAULT CURRENT_TIMESTAMP,updated_at DATETIME,version INTEGER NOT NULL DEFAULT 1,telegram_id TEXT)`,
|
|
`CREATE TABLE plan (id TEXT PRIMARY KEY,name TEXT NOT NULL,description TEXT,price REAL NOT NULL,cycle TEXT NOT NULL,storage_limit INTEGER NOT NULL,upload_limit INTEGER NOT NULL,duration_limit INTEGER NOT NULL,quality_limit TEXT NOT NULL,features JSON,is_active BOOLEAN NOT NULL DEFAULT 1,version INTEGER NOT NULL DEFAULT 1)`,
|
|
`CREATE TABLE payment (id TEXT PRIMARY KEY,user_id TEXT NOT NULL,plan_id TEXT,amount REAL NOT NULL,currency TEXT,status TEXT,provider TEXT,transaction_id TEXT,created_at DATETIME DEFAULT CURRENT_TIMESTAMP,updated_at DATETIME,version INTEGER NOT NULL DEFAULT 1)`,
|
|
`CREATE TABLE plan_subscriptions (id TEXT PRIMARY KEY,user_id TEXT NOT NULL,payment_id TEXT NOT NULL,plan_id TEXT NOT NULL,term_months INTEGER NOT NULL,payment_method TEXT NOT NULL,wallet_amount REAL NOT NULL,topup_amount REAL NOT NULL,started_at DATETIME NOT NULL,expires_at DATETIME NOT NULL,reminder_7d_sent_at DATETIME,reminder_3d_sent_at DATETIME,reminder_1d_sent_at DATETIME,created_at DATETIME DEFAULT CURRENT_TIMESTAMP,updated_at DATETIME,version INTEGER NOT NULL DEFAULT 1)`,
|
|
`CREATE TABLE wallet_transactions (id TEXT PRIMARY KEY,user_id TEXT NOT NULL,type TEXT NOT NULL,amount REAL NOT NULL,currency TEXT,note TEXT,created_at DATETIME DEFAULT CURRENT_TIMESTAMP,updated_at DATETIME,payment_id TEXT,plan_id TEXT,term_months INTEGER,version INTEGER NOT NULL DEFAULT 1)`,
|
|
`CREATE TABLE notifications (id TEXT PRIMARY KEY,user_id TEXT NOT NULL,type TEXT NOT NULL,title TEXT NOT NULL,message TEXT NOT NULL,metadata TEXT,action_url TEXT,action_label TEXT,is_read BOOLEAN NOT NULL DEFAULT 0,created_at DATETIME DEFAULT CURRENT_TIMESTAMP,updated_at DATETIME,version INTEGER NOT NULL DEFAULT 1)`,
|
|
} {
|
|
if err := db.Exec(stmt).Error; err != nil {
|
|
t.Fatalf("create test schema: %v", err)
|
|
}
|
|
}
|
|
query.SetDefault(db)
|
|
return db
|
|
}
|
|
|
|
func newNotificationTestServices(t *testing.T, db *gorm.DB, publisher NotificationEventPublisher) *appServices {
|
|
t.Helper()
|
|
return &appServices{
|
|
db: db,
|
|
logger: serviceTestLogger{},
|
|
authenticator: middleware.NewAuthenticator(db, serviceTestLogger{}, notificationTestTrustedMarker, publisher),
|
|
googleUserInfoURL: defaultGoogleUserInfoURL,
|
|
userRepository: repository.NewUserRepository(db),
|
|
planRepository: repository.NewPlanRepository(db),
|
|
paymentRepository: repository.NewPaymentRepository(db),
|
|
billingRepository: repository.NewBillingRepository(db),
|
|
notificationRepo: repository.NewNotificationRepository(db),
|
|
notificationEvents: publisher,
|
|
}
|
|
}
|
|
|
|
func notificationTestContext(userID, role string) context.Context {
|
|
return metadata.NewIncomingContext(context.Background(), metadata.Pairs(
|
|
middleware.ActorMarkerMetadataKey, notificationTestTrustedMarker,
|
|
middleware.ActorIDMetadataKey, userID,
|
|
middleware.ActorRoleMetadataKey, role,
|
|
middleware.ActorEmailMetadataKey, "actor@example.com",
|
|
))
|
|
}
|
|
|
|
func notificationPtrString(v string) *string { return &v }
|
|
func notificationPtrBool(v bool) *bool { return &v }
|
|
func notificationPtrFloat64(v float64) *float64 { return &v }
|
|
|
|
func seedNotificationUser(t *testing.T, db *gorm.DB, user model.User) model.User {
|
|
t.Helper()
|
|
if user.Role == nil {
|
|
user.Role = notificationPtrString("USER")
|
|
}
|
|
if err := db.Create(&user).Error; err != nil {
|
|
t.Fatalf("create user: %v", err)
|
|
}
|
|
return user
|
|
}
|
|
|
|
func seedNotificationPlan(t *testing.T, db *gorm.DB, plan model.Plan) model.Plan {
|
|
t.Helper()
|
|
if plan.IsActive == nil {
|
|
plan.IsActive = notificationPtrBool(true)
|
|
}
|
|
if err := db.Create(&plan).Error; err != nil {
|
|
t.Fatalf("create plan: %v", err)
|
|
}
|
|
return plan
|
|
}
|
|
|
|
func seedNotificationWalletTransaction(t *testing.T, db *gorm.DB, tx model.WalletTransaction) model.WalletTransaction {
|
|
t.Helper()
|
|
if err := db.Create(&tx).Error; err != nil {
|
|
t.Fatalf("create wallet transaction: %v", err)
|
|
}
|
|
return tx
|
|
}
|
|
|
|
func TestExecutePaymentFlowPublishesNotificationEvent(t *testing.T) {
|
|
db := newNotificationTestDB(t)
|
|
publisher := &fakeNotificationEventPublisher{}
|
|
services := newNotificationTestServices(t, db, publisher)
|
|
|
|
user := seedNotificationUser(t, db, model.User{ID: uuid.NewString(), Email: "payer@example.com", Role: notificationPtrString("USER")})
|
|
plan := seedNotificationPlan(t, db, model.Plan{ID: uuid.NewString(), Name: "Pro", Price: 10, Cycle: "monthly", StorageLimit: 100, UploadLimit: 10, QualityLimit: "1080p", IsActive: notificationPtrBool(true)})
|
|
seedNotificationWalletTransaction(t, db, model.WalletTransaction{ID: uuid.NewString(), UserID: user.ID, Type: walletTransactionTypeTopup, Amount: 5, Currency: notificationPtrString("USD")})
|
|
|
|
_, err := services.executePaymentFlow(context.Background(), paymentExecutionInput{UserID: user.ID, Plan: &plan, TermMonths: 1, PaymentMethod: paymentMethodTopup, TopupAmount: notificationPtrFloat64(5)})
|
|
if err != nil {
|
|
t.Fatalf("executePaymentFlow() error = %v", err)
|
|
}
|
|
if len(publisher.events) != 1 {
|
|
t.Fatalf("published events = %d, want 1", len(publisher.events))
|
|
}
|
|
if publisher.events[0].notification == nil || publisher.events[0].notification.Type != "billing.subscription" {
|
|
t.Fatalf("published notification = %#v", publisher.events[0].notification)
|
|
}
|
|
}
|
|
|
|
func TestTopupWalletPublishesNotificationEvent(t *testing.T) {
|
|
db := newNotificationTestDB(t)
|
|
publisher := &fakeNotificationEventPublisher{}
|
|
services := newNotificationTestServices(t, db, publisher)
|
|
user := seedNotificationUser(t, db, model.User{ID: uuid.NewString(), Email: "user@example.com", Role: notificationPtrString("USER")})
|
|
|
|
_, err := (&paymentsAppService{appServices: services}).TopupWallet(notificationTestContext(user.ID, "USER"), &appv1.TopupWalletRequest{Amount: 12})
|
|
if err != nil {
|
|
t.Fatalf("TopupWallet() error = %v", err)
|
|
}
|
|
if len(publisher.events) != 1 {
|
|
t.Fatalf("published events = %d, want 1", len(publisher.events))
|
|
}
|
|
if publisher.events[0].notification == nil || publisher.events[0].notification.Type != "billing.topup" {
|
|
t.Fatalf("published notification = %#v", publisher.events[0].notification)
|
|
}
|
|
}
|
|
|
|
func TestBuildNotificationCreatedPayload(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
notification := &model.Notification{ID: uuid.NewString(), UserID: uuid.NewString(), Type: "billing.subscription", Title: "Subscription activated", Message: "Your subscription is active.", ActionURL: notificationPtrString("/settings/billing"), ActionLabel: notificationPtrString("Renew plan"), CreatedAt: &now}
|
|
payload := BuildNotificationCreatedPayload(notification)
|
|
if payload.ID != notification.ID || payload.UserID != notification.UserID || payload.Type != notification.Type {
|
|
t.Fatalf("payload = %#v", payload)
|
|
}
|
|
if payload.CreatedAt == "" {
|
|
t.Fatal("payload created_at should not be empty")
|
|
}
|
|
}
|