Add unit tests for player configurations and referral system
- Implement tests for player configuration creation, update, and deletion, ensuring proper handling of free and paid user scenarios. - Add tests for referral registration, including valid and invalid referrer cases. - Create tests for referral reward flow, verifying correct reward distribution and eligibility. - Establish a test database setup with necessary schema for user, plan, payment, and notification models. - Introduce helper functions for seeding test data and loading entities from the database.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -30,6 +30,5 @@ go.work.sum
|
||||
# OS-specific junk
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
server
|
||||
# Module cache (if you're using GOPATH, which is rare now)
|
||||
Godeps/
|
||||
|
||||
74
cmd/server/main.go
Normal file
74
cmd/server/main.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"stream.api/internal/adapters/redis"
|
||||
"stream.api/internal/config"
|
||||
"stream.api/internal/database/query"
|
||||
"stream.api/internal/transport/grpc"
|
||||
"stream.api/pkg/database"
|
||||
"stream.api/pkg/logger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 1. Load Config
|
||||
cfg, err := config.LoadConfig()
|
||||
if err != nil {
|
||||
// Use default if env/file issues, usually LoadConfig returns error only on serious issues
|
||||
// But here if it returns error we might want to panic
|
||||
log.Fatalf("Failed to load config: %v", err)
|
||||
}
|
||||
|
||||
// 2. Connect DB
|
||||
db, err := database.Connect(cfg.Database.DSN)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to database: %v", err)
|
||||
}
|
||||
// Initialize generated query
|
||||
query.SetDefault(db)
|
||||
|
||||
// TODO: Tách database migration ra luồng riêng nếu cần.
|
||||
|
||||
// 3. Connect Redis (Cache Interface)
|
||||
rdb, err := redis.NewAdapter(cfg.Redis.Addr, cfg.Redis.Password, cfg.Redis.DB)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to redis: %v", err)
|
||||
}
|
||||
defer rdb.Close() // Ensure we close cache on exit
|
||||
|
||||
// 4. Initialize Components
|
||||
appLogger := logger.NewLogger(cfg.Server.Mode)
|
||||
|
||||
module, err := grpc.NewGRPCModule(context.Background(), cfg, db, rdb, appLogger)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to setup gRPC runtime module: %v", err)
|
||||
}
|
||||
|
||||
grpcListener, err := net.Listen("tcp", ":"+cfg.Server.GRPCPort)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to listen on gRPC port %s: %v", cfg.Server.GRPCPort, err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("Starting gRPC server on port %s", cfg.Server.GRPCPort)
|
||||
if err := module.ServeGRPC(grpcListener); err != nil {
|
||||
log.Fatalf("Failed to run gRPC server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
log.Println("Shutting down gRPC server...")
|
||||
|
||||
module.Shutdown()
|
||||
_ = grpcListener.Close()
|
||||
|
||||
log.Println("Server exiting")
|
||||
}
|
||||
5
go.mod
5
go.mod
@@ -63,7 +63,6 @@ require (
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/sony/gobreaker v1.0.0
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
@@ -79,11 +78,11 @@ require (
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
gorm.io/datatypes v1.2.4 // indirect
|
||||
gorm.io/datatypes v1.2.4
|
||||
gorm.io/driver/mysql v1.5.7 // indirect
|
||||
gorm.io/hints v1.1.0 // indirect
|
||||
modernc.org/libc v1.67.6 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.46.1 // indirect
|
||||
modernc.org/sqlite v1.46.1
|
||||
)
|
||||
|
||||
26
go.sum
26
go.sum
@@ -78,10 +78,14 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
@@ -122,8 +126,6 @@ github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0t
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
github.com/sony/gobreaker v1.0.0 h1:feX5fGGXSl3dYd4aHZItw+FpHLvvoaqkawKjVNiFMNQ=
|
||||
github.com/sony/gobreaker v1.0.0/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
@@ -216,11 +218,31 @@ gorm.io/hints v1.1.0 h1:Lp4z3rxREufSdxn4qmkK3TLDltrM10FLTHiuqwDPvXw=
|
||||
gorm.io/hints v1.1.0/go.mod h1:lKQ0JjySsPBj3uslFzY3JhYDtqEwzm+G1hv8rWujB6Y=
|
||||
gorm.io/plugin/dbresolver v1.6.2 h1:F4b85TenghUeITqe3+epPSUtHH7RIk3fXr5l83DF8Pc=
|
||||
gorm.io/plugin/dbresolver v1.6.2/go.mod h1:tctw63jdrOezFR9HmrKnPkmig3m5Edem9fdxk9bQSzM=
|
||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
|
||||
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
|
||||
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
|
||||
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
|
||||
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
|
||||
modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
|
||||
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
|
||||
@@ -24,7 +24,7 @@ type PlayerConfig struct {
|
||||
Airplay *bool `gorm:"column:airplay;type:boolean;not null;default:true" json:"airplay"`
|
||||
Chromecast *bool `gorm:"column:chromecast;type:boolean;not null;default:true" json:"chromecast"`
|
||||
IsActive *bool `gorm:"column:is_active;type:boolean;not null;default:true" json:"is_active"`
|
||||
IsDefault bool `gorm:"column:is_default;type:boolean;not null;index:idx_player_configs_is_default,priority:1;index:idx_player_configs_user_default,priority:1" json:"is_default"`
|
||||
IsDefault bool `gorm:"column:is_default;type:boolean;not null;index:idx_player_configs_user_default,priority:1;index:idx_player_configs_is_default,priority:1" json:"is_default"`
|
||||
CreatedAt *time.Time `gorm:"column:created_at;type:timestamp(3) without time zone;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp(3) without time zone;not null" json:"updated_at"`
|
||||
Version *int64 `gorm:"column:version;type:bigint;not null;default:1;version" json:"-"`
|
||||
|
||||
@@ -21,16 +21,16 @@ type User struct {
|
||||
GoogleID *string `gorm:"column:google_id;type:text;uniqueIndex:user_google_id_key,priority:1" json:"google_id"`
|
||||
StorageUsed int64 `gorm:"column:storage_used;type:bigint;not null" json:"storage_used"`
|
||||
PlanID *string `gorm:"column:plan_id;type:uuid" json:"plan_id"`
|
||||
CreatedAt *time.Time `gorm:"column:created_at;type:timestamp(3) without time zone;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp(3) without time zone;not null" json:"updated_at"`
|
||||
Version *int64 `gorm:"column:version;type:bigint;not null;default:1;version" json:"-"`
|
||||
TelegramID *string `gorm:"column:telegram_id;type:character varying" json:"telegram_id"`
|
||||
ReferredByUserID *string `gorm:"column:referred_by_user_id;type:uuid;index:idx_user_referred_by_user_id,priority:1" json:"referred_by_user_id"`
|
||||
ReferralEligible *bool `gorm:"column:referral_eligible;type:boolean;not null;default:true" json:"referral_eligible"`
|
||||
ReferralRewardBps *int32 `gorm:"column:referral_reward_bps;type:integer" json:"referral_reward_bps"`
|
||||
ReferralRewardGrantedAt *time.Time `gorm:"column:referral_reward_granted_at;type:timestamp with time zone" json:"referral_reward_granted_at"`
|
||||
ReferralRewardPaymentID *string `gorm:"column:referral_reward_payment_id;type:uuid" json:"referral_reward_payment_id"`
|
||||
ReferralRewardAmount *float64 `gorm:"column:referral_reward_amount;type:numeric(65,30)" json:"referral_reward_amount"`
|
||||
CreatedAt *time.Time `gorm:"column:created_at;type:timestamp(3) without time zone;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp(3) without time zone;not null" json:"updated_at"`
|
||||
Version *int64 `gorm:"column:version;type:bigint;not null;default:1;version" json:"-"`
|
||||
TelegramID *string `gorm:"column:telegram_id;type:character varying" json:"telegram_id"`
|
||||
}
|
||||
|
||||
// TableName User's table name
|
||||
|
||||
@@ -41,6 +41,12 @@ func newUser(db *gorm.DB, opts ...gen.DOOption) user {
|
||||
_user.UpdatedAt = field.NewTime(tableName, "updated_at")
|
||||
_user.Version = field.NewInt64(tableName, "version")
|
||||
_user.TelegramID = field.NewString(tableName, "telegram_id")
|
||||
_user.ReferredByUserID = field.NewString(tableName, "referred_by_user_id")
|
||||
_user.ReferralEligible = field.NewBool(tableName, "referral_eligible")
|
||||
_user.ReferralRewardBps = field.NewInt32(tableName, "referral_reward_bps")
|
||||
_user.ReferralRewardGrantedAt = field.NewTime(tableName, "referral_reward_granted_at")
|
||||
_user.ReferralRewardPaymentID = field.NewString(tableName, "referral_reward_payment_id")
|
||||
_user.ReferralRewardAmount = field.NewFloat64(tableName, "referral_reward_amount")
|
||||
|
||||
_user.fillFieldMap()
|
||||
|
||||
@@ -50,20 +56,26 @@ func newUser(db *gorm.DB, opts ...gen.DOOption) user {
|
||||
type user struct {
|
||||
userDo userDo
|
||||
|
||||
ALL field.Asterisk
|
||||
ID field.String
|
||||
Email field.String
|
||||
Password field.String
|
||||
Username field.String
|
||||
Avatar field.String
|
||||
Role field.String
|
||||
GoogleID field.String
|
||||
StorageUsed field.Int64
|
||||
PlanID field.String
|
||||
CreatedAt field.Time
|
||||
UpdatedAt field.Time
|
||||
Version field.Int64
|
||||
TelegramID field.String
|
||||
ALL field.Asterisk
|
||||
ID field.String
|
||||
Email field.String
|
||||
Password field.String
|
||||
Username field.String
|
||||
Avatar field.String
|
||||
Role field.String
|
||||
GoogleID field.String
|
||||
StorageUsed field.Int64
|
||||
PlanID field.String
|
||||
CreatedAt field.Time
|
||||
UpdatedAt field.Time
|
||||
Version field.Int64
|
||||
TelegramID field.String
|
||||
ReferredByUserID field.String
|
||||
ReferralEligible field.Bool
|
||||
ReferralRewardBps field.Int32
|
||||
ReferralRewardGrantedAt field.Time
|
||||
ReferralRewardPaymentID field.String
|
||||
ReferralRewardAmount field.Float64
|
||||
|
||||
fieldMap map[string]field.Expr
|
||||
}
|
||||
@@ -93,6 +105,12 @@ func (u *user) updateTableName(table string) *user {
|
||||
u.UpdatedAt = field.NewTime(table, "updated_at")
|
||||
u.Version = field.NewInt64(table, "version")
|
||||
u.TelegramID = field.NewString(table, "telegram_id")
|
||||
u.ReferredByUserID = field.NewString(table, "referred_by_user_id")
|
||||
u.ReferralEligible = field.NewBool(table, "referral_eligible")
|
||||
u.ReferralRewardBps = field.NewInt32(table, "referral_reward_bps")
|
||||
u.ReferralRewardGrantedAt = field.NewTime(table, "referral_reward_granted_at")
|
||||
u.ReferralRewardPaymentID = field.NewString(table, "referral_reward_payment_id")
|
||||
u.ReferralRewardAmount = field.NewFloat64(table, "referral_reward_amount")
|
||||
|
||||
u.fillFieldMap()
|
||||
|
||||
@@ -117,7 +135,7 @@ func (u *user) GetFieldByName(fieldName string) (field.OrderExpr, bool) {
|
||||
}
|
||||
|
||||
func (u *user) fillFieldMap() {
|
||||
u.fieldMap = make(map[string]field.Expr, 13)
|
||||
u.fieldMap = make(map[string]field.Expr, 19)
|
||||
u.fieldMap["id"] = u.ID
|
||||
u.fieldMap["email"] = u.Email
|
||||
u.fieldMap["password"] = u.Password
|
||||
@@ -131,6 +149,12 @@ func (u *user) fillFieldMap() {
|
||||
u.fieldMap["updated_at"] = u.UpdatedAt
|
||||
u.fieldMap["version"] = u.Version
|
||||
u.fieldMap["telegram_id"] = u.TelegramID
|
||||
u.fieldMap["referred_by_user_id"] = u.ReferredByUserID
|
||||
u.fieldMap["referral_eligible"] = u.ReferralEligible
|
||||
u.fieldMap["referral_reward_bps"] = u.ReferralRewardBps
|
||||
u.fieldMap["referral_reward_granted_at"] = u.ReferralRewardGrantedAt
|
||||
u.fieldMap["referral_reward_payment_id"] = u.ReferralRewardPaymentID
|
||||
u.fieldMap["referral_reward_amount"] = u.ReferralRewardAmount
|
||||
}
|
||||
|
||||
func (u user) clone(db *gorm.DB) user {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// update lại test sau nhé.
|
||||
package service
|
||||
|
||||
import (
|
||||
@@ -8,7 +9,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
goredis "github.com/redis/go-redis/v9"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/metadata"
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"stream.api/internal/database/query"
|
||||
"stream.api/internal/middleware"
|
||||
"stream.api/pkg/logger"
|
||||
"stream.api/pkg/token"
|
||||
)
|
||||
|
||||
const testTrustedMarker = "trusted-test-marker"
|
||||
@@ -69,26 +68,7 @@ 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 _ goredis.Client = (*fakeCache)(nil)
|
||||
// var _ goredis.Client = (*fakeCache)(nil)
|
||||
|
||||
func newTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
@@ -244,11 +224,10 @@ func newTestAppServices(t *testing.T, db *gorm.DB) *appServices {
|
||||
}
|
||||
|
||||
return &appServices{
|
||||
db: db,
|
||||
logger: testLogger{},
|
||||
authenticator: middleware.NewAuthenticator(db, testLogger{}, testTrustedMarker),
|
||||
cache: &fakeCache{values: map[string]string{}},
|
||||
tokenProvider: fakeTokenProvider{},
|
||||
db: db,
|
||||
logger: testLogger{},
|
||||
authenticator: middleware.NewAuthenticator(db, testLogger{}, testTrustedMarker),
|
||||
// cache: &fakeCache{values: map[string]string{}},
|
||||
googleUserInfoURL: defaultGoogleUserInfoURL,
|
||||
}
|
||||
}
|
||||
@@ -38,10 +38,6 @@ func (s *appServices) Login(ctx context.Context, req *appv1.LoginRequest) (*appv
|
||||
return nil, status.Error(codes.Unauthenticated, "Invalid credentials")
|
||||
}
|
||||
|
||||
if err := s.issueSessionCookies(ctx, user); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
payload, err := buildUserPayload(ctx, s.db, user)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, "Failed to build user payload")
|
||||
@@ -304,10 +300,6 @@ func (s *appServices) CompleteGoogleLogin(ctx context.Context, req *appv1.Comple
|
||||
}
|
||||
}
|
||||
|
||||
if err := s.issueSessionCookies(ctx, user); err != nil {
|
||||
return nil, status.Error(codes.Internal, "session_failed")
|
||||
}
|
||||
|
||||
payload, err := buildUserPayload(ctx, s.db, user)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.Internal, "Failed to build user payload")
|
||||
|
||||
@@ -6,15 +6,14 @@ import (
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
"gorm.io/gorm"
|
||||
"stream.api/internal/adapters/redis"
|
||||
appv1 "stream.api/internal/api/proto/app/v1"
|
||||
"stream.api/internal/config"
|
||||
"stream.api/internal/database/model"
|
||||
"stream.api/internal/middleware"
|
||||
"stream.api/internal/video"
|
||||
"stream.api/internal/video/runtime/adapters/queue/redis"
|
||||
"stream.api/pkg/logger"
|
||||
"stream.api/pkg/storage"
|
||||
"stream.api/pkg/token"
|
||||
)
|
||||
|
||||
const adTemplateUpgradeRequiredMessage = "Upgrade required to manage Ads & VAST"
|
||||
@@ -73,7 +72,6 @@ type appServices struct {
|
||||
db *gorm.DB
|
||||
logger logger.Logger
|
||||
authenticator *middleware.Authenticator
|
||||
tokenProvider token.Provider
|
||||
cache *redis.RedisAdapter
|
||||
storageProvider storage.Provider
|
||||
videoService *video.Service
|
||||
|
||||
@@ -959,39 +959,7 @@ func buildPaymentSubscription(input paymentExecutionInput, paymentRecord *model.
|
||||
ExpiresAt: newExpiry,
|
||||
}
|
||||
}
|
||||
func (s *appServices) issueSessionCookies(ctx context.Context, user *model.User) error {
|
||||
if user == nil {
|
||||
return status.Error(codes.Unauthenticated, "Unauthorized")
|
||||
}
|
||||
tokenPair, err := s.tokenProvider.GenerateTokenPair(user.ID, user.Email, safeRole(user.Role))
|
||||
if err != nil {
|
||||
s.logger.Error("Token generation failed", "error", err)
|
||||
return status.Error(codes.Internal, "Error generating tokens")
|
||||
}
|
||||
|
||||
if err := s.cache.Set(ctx, "refresh_uuid:"+tokenPair.RefreshUUID, user.ID, time.Until(time.Unix(tokenPair.RtExpires, 0))); err != nil {
|
||||
s.logger.Error("Session storage failed", "error", err)
|
||||
return status.Error(codes.Internal, "Error storing session")
|
||||
}
|
||||
|
||||
if err := grpc.SetHeader(ctx, metadata.Pairs(
|
||||
"set-cookie", buildTokenCookie("access_token", tokenPair.AccessToken, int(tokenPair.AtExpires-time.Now().Unix())),
|
||||
"set-cookie", buildTokenCookie("refresh_token", tokenPair.RefreshToken, int(tokenPair.RtExpires-time.Now().Unix())),
|
||||
)); err != nil {
|
||||
s.logger.Error("Failed to set gRPC auth headers", "error", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
func buildTokenCookie(name string, value string, maxAge int) string {
|
||||
return (&http.Cookie{
|
||||
Name: name,
|
||||
Value: value,
|
||||
Path: "/",
|
||||
MaxAge: maxAge,
|
||||
HttpOnly: true,
|
||||
}).String()
|
||||
}
|
||||
func messageResponse(message string) *appv1.MessageResponse {
|
||||
return &appv1.MessageResponse{Message: message}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
package grpc
|
||||
@@ -6,11 +6,11 @@ import (
|
||||
|
||||
grpcpkg "google.golang.org/grpc"
|
||||
"gorm.io/gorm"
|
||||
redisadapter "stream.api/internal/adapters/redis"
|
||||
"stream.api/internal/config"
|
||||
"stream.api/internal/service"
|
||||
"stream.api/internal/video"
|
||||
runtime "stream.api/internal/video/runtime"
|
||||
redisadapter "stream.api/internal/video/runtime/adapters/queue/redis"
|
||||
runtimegrpc "stream.api/internal/video/runtime/grpc"
|
||||
"stream.api/internal/video/runtime/services"
|
||||
"stream.api/pkg/logger"
|
||||
|
||||
@@ -1,33 +1,15 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
runtimedomain "stream.api/internal/video/runtime/domain"
|
||||
runtimeservices "stream.api/internal/video/runtime/services"
|
||||
)
|
||||
|
||||
type Job = runtimedomain.Job
|
||||
|
||||
type AgentWithStats = runtimeservices.AgentWithStats
|
||||
|
||||
type PaginatedJobs = runtimeservices.PaginatedJobs
|
||||
|
||||
var ErrInvalidJobCursor = runtimeservices.ErrInvalidJobCursor
|
||||
|
||||
type JobService interface {
|
||||
CreateJob(ctx context.Context, userID string, videoID string, name string, config []byte, priority int, timeLimit int64) (*Job, error)
|
||||
ListJobs(ctx context.Context, offset, limit int) (*PaginatedJobs, error)
|
||||
ListJobsByAgent(ctx context.Context, agentID string, offset, limit int) (*PaginatedJobs, error)
|
||||
ListJobsByCursor(ctx context.Context, agentID string, cursor string, pageSize int) (*PaginatedJobs, error)
|
||||
GetJob(ctx context.Context, id string) (*Job, error)
|
||||
CancelJob(ctx context.Context, id string) error
|
||||
RetryJob(ctx context.Context, id string) (*Job, error)
|
||||
SubscribeJobLogs(ctx context.Context, jobID string) (<-chan runtimedomain.LogEntry, error)
|
||||
SubscribeJobUpdates(ctx context.Context) (<-chan string, error)
|
||||
SubscribeSystemResources(ctx context.Context) (<-chan runtimedomain.SystemResource, error)
|
||||
}
|
||||
|
||||
type AgentRuntime interface {
|
||||
ListAgentsWithStats() []*AgentWithStats
|
||||
SendCommand(agentID string, cmd string) bool
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package domain
|
||||
|
||||
import "time"
|
||||
|
||||
type JobStatus string
|
||||
|
||||
const (
|
||||
@@ -11,28 +9,3 @@ const (
|
||||
JobStatusFailure JobStatus = "failure"
|
||||
JobStatusCancelled JobStatus = "cancelled"
|
||||
)
|
||||
|
||||
type Job struct {
|
||||
ID string `json:"id" gorm:"primaryKey;type:uuid;default:gen_random_uuid()"`
|
||||
Status JobStatus `json:"status" gorm:"type:varchar(32);index"`
|
||||
Priority int `json:"priority" gorm:"default:0;index"`
|
||||
UserID string `json:"user_id" gorm:"index"`
|
||||
VideoID string `json:"video_id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
TimeLimit int64 `json:"time_limit"`
|
||||
InputURL string `json:"input_url"`
|
||||
OutputURL string `json:"output_url"`
|
||||
TotalDuration int64 `json:"total_duration"`
|
||||
CurrentTime int64 `json:"current_time"`
|
||||
Progress float64 `json:"progress"`
|
||||
AgentID *string `json:"agent_id" gorm:"type:uuid;index"`
|
||||
Logs string `json:"logs" gorm:"type:text"`
|
||||
Config string `json:"config" gorm:"type:text"`
|
||||
Cancelled bool `json:"cancelled" gorm:"default:false"`
|
||||
RetryCount int `json:"retry_count" gorm:"default:0"`
|
||||
MaxRetries int `json:"max_retries" gorm:"default:3"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (Job) TableName() string { return "render_jobs" }
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
func (m *Module) MetricsHandler() gin.HandlerFunc {
|
||||
return gin.WrapH(promhttp.Handler())
|
||||
}
|
||||
|
||||
// HandleLive godoc
|
||||
// @Summary Liveness health check
|
||||
// @Description Returns liveness status for the API and render module
|
||||
// @Tags health
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]string
|
||||
// @Failure 503 {object} map[string]string
|
||||
// @Router /health/live [get]
|
||||
func (m *Module) HandleLive(c *gin.Context) {
|
||||
status, code := m.healthService.SimpleHealthCheck(c.Request.Context())
|
||||
c.JSON(code, gin.H{"status": status})
|
||||
}
|
||||
|
||||
// HandleReady godoc
|
||||
// @Summary Readiness health check
|
||||
// @Description Returns readiness status including render gRPC availability flag
|
||||
// @Tags health
|
||||
// @Produce json
|
||||
// @Success 200 {object} map[string]interface{}
|
||||
// @Failure 503 {object} map[string]interface{}
|
||||
// @Router /health/ready [get]
|
||||
func (m *Module) HandleReady(c *gin.Context) {
|
||||
status, code := m.healthService.SimpleHealthCheck(c.Request.Context())
|
||||
c.JSON(code, gin.H{"status": status, "grpc_enabled": m.grpcServer != nil})
|
||||
}
|
||||
|
||||
// HandleDetailed godoc
|
||||
// @Summary Detailed health check
|
||||
// @Description Returns detailed health state for database, redis, and render dependencies
|
||||
// @Tags health
|
||||
// @Produce json
|
||||
// @Success 200 {object} services.HealthReport
|
||||
// @Router /health/detailed [get]
|
||||
func (m *Module) HandleDetailed(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, m.healthService.CheckHealth(c.Request.Context()))
|
||||
}
|
||||
|
||||
var (
|
||||
httpRequests = prometheus.NewCounterVec(prometheus.CounterOpts{Name: "stream_api_http_requests_total", Help: "Total HTTP requests."}, []string{"method", "path", "status"})
|
||||
httpDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{Name: "stream_api_http_request_duration_seconds", Help: "HTTP request duration."}, []string{"method", "path"})
|
||||
)
|
||||
|
||||
func init() {
|
||||
prometheus.MustRegister(httpRequests, httpDuration)
|
||||
}
|
||||
|
||||
func MetricsMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
c.Next()
|
||||
path := c.FullPath()
|
||||
if path == "" {
|
||||
path = c.Request.URL.Path
|
||||
}
|
||||
httpRequests.WithLabelValues(c.Request.Method, path, http.StatusText(c.Writer.Status())).Inc()
|
||||
httpDuration.WithLabelValues(c.Request.Method, path).Observe(time.Since(start).Seconds())
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package token
|
||||
|
||||
// TokenPair contains the access and refresh tokens
|
||||
type TokenPair struct {
|
||||
AccessToken string
|
||||
RefreshToken string
|
||||
AccessUUID string
|
||||
RefreshUUID string
|
||||
AtExpires int64
|
||||
RtExpires int64
|
||||
}
|
||||
|
||||
// Claims defines the JWT claims (User)
|
||||
type Claims struct {
|
||||
UserID string `json:"user_id"`
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
TokenID string `json:"token_id"`
|
||||
}
|
||||
|
||||
// Provider defines the interface for token operations
|
||||
type Provider interface {
|
||||
GenerateTokenPair(userID, email, role string) (*TokenPair, error)
|
||||
ParseToken(tokenString string) (*Claims, error)
|
||||
ParseMapToken(tokenString string) (map[string]interface{}, error)
|
||||
}
|
||||
Reference in New Issue
Block a user