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:
2026-03-26 02:20:05 +07:00
parent bb7f7b0bb3
commit 4de6baee61
25 changed files with 152 additions and 245 deletions

1
.gitignore vendored
View File

@@ -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
View 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
View File

@@ -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
View File

@@ -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=

View File

@@ -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:"-"`

View File

@@ -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

View File

@@ -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 {

View File

@@ -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,
}
}

View File

@@ -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")

View File

@@ -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

View File

@@ -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}
}

View File

@@ -1 +0,0 @@
package grpc

View File

@@ -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"

View File

@@ -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

View File

@@ -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" }

View File

@@ -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())
}
}

View File

@@ -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)
}