Files
stream.api/internal/service/__test__/testdb_setup_test.go
2026-04-02 11:01:30 +00:00

442 lines
12 KiB
Go

// update lại test sau nhé.
package service
import (
"context"
"fmt"
"net"
"testing"
"time"
"github.com/google/uuid"
_ "github.com/mattn/go-sqlite3"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/test/bufconn"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
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"
"stream.api/pkg/logger"
)
const testTrustedMarker = "trusted-test-marker"
var testBufDialerListenerSize = 1024 * 1024
type testLogger struct{}
type fakeCache struct {
values map[string]string
}
type fakeTokenProvider struct{}
func (testLogger) Info(string, ...any) {}
func (testLogger) Error(string, ...any) {}
func (testLogger) Debug(string, ...any) {}
func (testLogger) Warn(string, ...any) {}
func (f *fakeCache) Set(_ context.Context, key string, value interface{}, _ time.Duration) error {
if f.values == nil {
f.values = map[string]string{}
}
f.values[key] = fmt.Sprint(value)
return nil
}
func (f *fakeCache) Get(_ context.Context, key string) (string, error) {
if f.values == nil {
return "", fmt.Errorf("cache miss")
}
value, ok := f.values[key]
if !ok {
return "", fmt.Errorf("cache miss")
}
return value, nil
}
func (f *fakeCache) Del(_ context.Context, key string) error {
delete(f.values, key)
return nil
}
func (f *fakeCache) Close() error {
return nil
}
// var _ goredis.Client = (*fakeCache)(nil)
func newTestDB(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: "sqlite3", 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
)`,
`CREATE TABLE user_preferences (
user_id TEXT PRIMARY KEY,
language TEXT NOT NULL DEFAULT 'en',
locale TEXT NOT NULL DEFAULT 'en',
email_notifications BOOLEAN NOT NULL DEFAULT 1,
push_notifications BOOLEAN NOT NULL DEFAULT 1,
marketing_notifications BOOLEAN NOT NULL DEFAULT 0,
telegram_notifications BOOLEAN NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME,
version INTEGER NOT NULL DEFAULT 1
)`,
`CREATE TABLE player_configs (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
description TEXT,
autoplay BOOLEAN NOT NULL DEFAULT 0,
loop BOOLEAN NOT NULL DEFAULT 0,
muted BOOLEAN NOT NULL DEFAULT 0,
show_controls BOOLEAN NOT NULL DEFAULT 1,
pip BOOLEAN NOT NULL DEFAULT 1,
airplay BOOLEAN NOT NULL DEFAULT 1,
chromecast BOOLEAN NOT NULL DEFAULT 1,
is_active BOOLEAN NOT NULL DEFAULT 1,
is_default BOOLEAN NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME,
version INTEGER NOT NULL DEFAULT 1,
encrytion_m3u8 BOOLEAN NOT NULL DEFAULT 1,
logo_url TEXT
)`,
`CREATE TABLE popup_ads (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
type TEXT NOT NULL,
label TEXT NOT NULL,
value TEXT NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 1,
max_triggers_per_session INTEGER,
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 newTestAppServices(t *testing.T, db *gorm.DB) *appServices {
t.Helper()
if db == nil {
db = newTestDB(t)
}
return &appServices{
db: db,
logger: testLogger{},
authenticator: middleware.NewAuthenticator(db, testLogger{}, testTrustedMarker, nil),
// cache: &fakeCache{values: map[string]string{}},
googleUserInfoURL: defaultGoogleUserInfoURL,
userRepository: repository.NewUserRepository(db),
planRepository: repository.NewPlanRepository(db),
paymentRepository: repository.NewPaymentRepository(db),
notificationRepo: repository.NewNotificationRepository(db),
domainRepository: repository.NewDomainRepository(db),
adTemplateRepository: repository.NewAdTemplateRepository(db),
popupAdRepository: repository.NewPopupAdRepository(db),
playerConfigRepo: repository.NewPlayerConfigRepository(db),
}
}
func newTestGRPCServer(t *testing.T, services *appServices) (*grpc.ClientConn, func()) {
t.Helper()
lis := bufconn.Listen(testBufDialerListenerSize)
server := grpc.NewServer()
Register(server, &Services{
AuthServer: services,
AccountServer: services,
UsageServer: services,
NotificationsServer: services,
DomainsServer: services,
AdTemplatesServer: services,
PopupAdsServer: services,
PlayerConfigsServer: services,
PlansServer: services,
PaymentsServer: services,
VideosServer: services,
VideoMetadataServer: services,
AdminServer: services,
})
go func() {
_ = server.Serve(lis)
}()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
conn, err := grpc.DialContext(ctx, "bufnet",
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
return lis.Dial()
}),
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
cancel()
if err != nil {
server.Stop()
_ = lis.Close()
t.Fatalf("dial bufconn: %v", err)
}
cleanup := func() {
_ = conn.Close()
server.Stop()
_ = lis.Close()
}
return conn, cleanup
}
func ptrFloat64(v float64) *float64 { return &v }
func ptrString(v string) *string { return &v }
func ptrBool(v bool) *bool { return &v }
func ptrInt64(v int64) *int64 { return &v }
func firstTestMetadataValue(md metadata.MD, key string) string {
values := md.Get(key)
if len(values) == 0 {
return ""
}
return values[0]
}
func seedTestUser(t *testing.T, db *gorm.DB, user model.User) model.User {
t.Helper()
if user.Role == nil {
user.Role = ptrString("USER")
}
if err := db.Create(&user).Error; err != nil {
t.Fatalf("create user: %v", err)
}
return user
}
func seedTestPlan(t *testing.T, db *gorm.DB, plan model.Plan) model.Plan {
t.Helper()
if plan.IsActive == nil {
plan.IsActive = ptrBool(true)
}
if err := db.Create(&plan).Error; err != nil {
t.Fatalf("create plan: %v", err)
}
return plan
}
func seedWalletTransaction(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 seedSubscription(t *testing.T, db *gorm.DB, subscription model.PlanSubscription) model.PlanSubscription {
t.Helper()
if err := db.Create(&subscription).Error; err != nil {
t.Fatalf("create subscription: %v", err)
}
return subscription
}
func mustLoadUser(t *testing.T, db *gorm.DB, userID string) model.User {
t.Helper()
var user model.User
if err := db.First(&user, "id = ?", userID).Error; err != nil {
t.Fatalf("load user %s: %v", userID, err)
}
return user
}
func mustLoadPayment(t *testing.T, db *gorm.DB, paymentID string) model.Payment {
t.Helper()
var payment model.Payment
if err := db.First(&payment, "id = ?", paymentID).Error; err != nil {
t.Fatalf("load payment %s: %v", paymentID, err)
}
return payment
}
func mustLoadSubscriptionByPayment(t *testing.T, db *gorm.DB, paymentID string) model.PlanSubscription {
t.Helper()
var subscription model.PlanSubscription
if err := db.First(&subscription, "payment_id = ?", paymentID).Error; err != nil {
t.Fatalf("load subscription for payment %s: %v", paymentID, err)
}
return subscription
}
func mustListWalletTransactionsByPayment(t *testing.T, db *gorm.DB, paymentID string) []model.WalletTransaction {
t.Helper()
var items []model.WalletTransaction
if err := db.Order("amount DESC").Find(&items, "payment_id = ?", paymentID).Error; err != nil {
t.Fatalf("list wallet transactions for payment %s: %v", paymentID, err)
}
return items
}
func mustListNotificationsByUser(t *testing.T, db *gorm.DB, userID string) []model.Notification {
t.Helper()
var items []model.Notification
if err := db.Order("created_at ASC, id ASC").Find(&items, "user_id = ?", userID).Error; err != nil {
t.Fatalf("list notifications for user %s: %v", userID, err)
}
return items
}
func newPaymentsClient(conn *grpc.ClientConn) appv1.PaymentsClient {
return appv1.NewPaymentsClient(conn)
}
func newAdminClient(conn *grpc.ClientConn) appv1.AdminClient {
return appv1.NewAdminClient(conn)
}
func newVideoMetadataClient(conn *grpc.ClientConn) appv1.VideoMetadataClient {
return appv1.NewVideoMetadataClient(conn)
}
func ptrTime(v time.Time) *time.Time { return &v }
func seedTestPopupAd(t *testing.T, db *gorm.DB, item model.PopupAd) model.PopupAd {
t.Helper()
if item.IsActive == nil {
item.IsActive = ptrBool(true)
}
if item.CreatedAt == nil {
now := time.Now().UTC()
item.CreatedAt = &now
}
if item.CloseCooldownMinutes == 0 {
item.CloseCooldownMinutes = 60
}
if err := db.Create(&item).Error; err != nil {
t.Fatalf("create popup ad: %v", err)
}
return item
}
func mustListPopupAdsByUser(t *testing.T, db *gorm.DB, userID string) []model.PopupAd {
t.Helper()
var items []model.PopupAd
if err := db.Order("priority DESC, created_at DESC").Find(&items, "user_id = ?", userID).Error; err != nil {
t.Fatalf("list popup ads for user %s: %v", userID, err)
}
return items
}
var _ logger.Logger = testLogger{}