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()
@@ -64,6 +70,12 @@ type user struct {
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()
@@ -247,8 +227,7 @@ func newTestAppServices(t *testing.T, db *gorm.DB) *appServices {
db: db,
logger: testLogger{},
authenticator: middleware.NewAuthenticator(db, testLogger{}, testTrustedMarker),
cache: &fakeCache{values: map[string]string{}},
tokenProvider: fakeTokenProvider{},
// 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)
}