feat: add test database setup and usage helpers
- Introduced a new test file for setting up an in-memory SQLite database for testing purposes. - Added helper functions for seeding test data, including users, plans, subscriptions, and wallet transactions. - Implemented usage helpers to load user video counts and storage usage. - Created user payload struct and functions to build user payloads with preferences and wallet balance. - Refactored gRPC server setup to include new services and handlers. - Updated proto files to simplify service definitions by removing redundant service prefixes.
This commit is contained in:
407
internal/service/testdb_setup_test.go
Normal file
407
internal/service/testdb_setup_test.go
Normal file
@@ -0,0 +1,407 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"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"
|
||||
_ "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/pkg/cache"
|
||||
"stream.api/pkg/logger"
|
||||
"stream.api/pkg/token"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
func (fakeTokenProvider) GenerateTokenPair(userID, _, _ string) (*token.TokenPair, error) {
|
||||
return &token.TokenPair{
|
||||
AccessToken: "access-" + userID,
|
||||
RefreshToken: "refresh-" + userID,
|
||||
AccessUUID: "access-uuid-" + userID,
|
||||
RefreshUUID: "refresh-uuid-" + userID,
|
||||
AtExpires: time.Now().Add(time.Hour).Unix(),
|
||||
RtExpires: time.Now().Add(24 * time.Hour).Unix(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (fakeTokenProvider) ParseToken(tokenString string) (*token.Claims, error) {
|
||||
return &token.Claims{UserID: tokenString}, nil
|
||||
}
|
||||
|
||||
func (fakeTokenProvider) ParseMapToken(tokenString string) (map[string]interface{}, error) {
|
||||
return map[string]interface{}{"token": tokenString}, nil
|
||||
}
|
||||
|
||||
var _ cache.Cache = (*fakeCache)(nil)
|
||||
var _ token.Provider = fakeTokenProvider{}
|
||||
|
||||
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: "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
|
||||
)`,
|
||||
`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
|
||||
)`,
|
||||
} {
|
||||
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),
|
||||
cache: &fakeCache{values: map[string]string{}},
|
||||
tokenProvider: fakeTokenProvider{},
|
||||
googleUserInfoURL: defaultGoogleUserInfoURL,
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
PlayerConfigsServer: services,
|
||||
PlansServer: services,
|
||||
PaymentsServer: services,
|
||||
VideosServer: 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)
|
||||
}
|
||||
|
||||
var _ logger.Logger = testLogger{}
|
||||
Reference in New Issue
Block a user