update cicd

This commit is contained in:
2026-04-02 11:01:30 +00:00
parent 863a0ea2f6
commit 5a7f29c116
54 changed files with 4298 additions and 473 deletions

View File

@@ -1,15 +1,17 @@
package service
import (
"context"
"fmt"
"testing"
"time"
"github.com/google/uuid"
"google.golang.org/grpc/codes"
"gorm.io/gorm"
redisadapter "stream.api/internal/adapters/redis"
appv1 "stream.api/internal/api/proto/app/v1"
"stream.api/internal/database/model"
runtimeservices "stream.api/internal/service/runtime/services"
renderworkflow "stream.api/internal/workflow/render"
)
@@ -18,7 +20,7 @@ func TestListAdminJobsCursorPagination(t *testing.T) {
ensureTestJobsTable(t, db)
services := newTestAppServices(t, db)
services.videoWorkflowService = renderworkflow.New(db, runtimeservices.NewJobService(db, nil, nil))
services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, nil, nil, nil))
admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
baseTime := time.Date(2026, 3, 22, 10, 0, 0, 0, time.UTC)
@@ -67,7 +69,7 @@ func TestListAdminJobsInvalidCursor(t *testing.T) {
ensureTestJobsTable(t, db)
services := newTestAppServices(t, db)
services.videoWorkflowService = renderworkflow.New(db, runtimeservices.NewJobService(db, nil, nil))
services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, nil, nil, nil))
admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
conn, cleanup := newTestGRPCServer(t, services)
@@ -86,7 +88,7 @@ func TestListAdminJobsCursorRejectsAgentMismatch(t *testing.T) {
ensureTestJobsTable(t, db)
services := newTestAppServices(t, db)
services.videoWorkflowService = renderworkflow.New(db, runtimeservices.NewJobService(db, nil, nil))
services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, nil, nil, nil))
admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
baseTime := time.Date(2026, 3, 22, 11, 0, 0, 0, time.UTC)
@@ -192,4 +194,212 @@ func assertAdminJobIDs(t *testing.T, jobs []*appv1.AdminJob, want []string) {
}
}
func ptrTime(v time.Time) *time.Time { return &v }
type fakeDLQ struct {
entries map[string]*redisadapter.DLQEntry
order []string
listErr error
getErr error
removeErr error
retryErr error
}
func newFakeDLQ(entries ...*redisadapter.DLQEntry) *fakeDLQ {
f := &fakeDLQ{entries: map[string]*redisadapter.DLQEntry{}, order: []string{}}
for _, entry := range entries {
if entry == nil || entry.Job == nil {
continue
}
f.entries[entry.Job.ID] = entry
f.order = append(f.order, entry.Job.ID)
}
return f
}
func (f *fakeDLQ) Add(_ context.Context, job *model.Job, reason string) error {
if f.entries == nil {
f.entries = map[string]*redisadapter.DLQEntry{}
}
entry := &redisadapter.DLQEntry{Job: job, FailureTime: time.Now().UTC(), Reason: reason}
f.entries[job.ID] = entry
f.order = append(f.order, job.ID)
return nil
}
func (f *fakeDLQ) Get(_ context.Context, jobID string) (*redisadapter.DLQEntry, error) {
if f.getErr != nil {
return nil, f.getErr
}
entry, ok := f.entries[jobID]
if !ok {
return nil, fmt.Errorf("job not found in DLQ")
}
return entry, nil
}
func (f *fakeDLQ) List(_ context.Context, offset, limit int64) ([]*redisadapter.DLQEntry, error) {
if f.listErr != nil {
return nil, f.listErr
}
if offset < 0 {
offset = 0
}
if limit <= 0 {
limit = int64(len(f.order))
}
items := make([]*redisadapter.DLQEntry, 0)
for i := offset; i < int64(len(f.order)) && int64(len(items)) < limit; i++ {
id := f.order[i]
if entry, ok := f.entries[id]; ok {
items = append(items, entry)
}
}
return items, nil
}
func (f *fakeDLQ) Count(_ context.Context) (int64, error) {
return int64(len(f.entries)), nil
}
func (f *fakeDLQ) Remove(_ context.Context, jobID string) error {
if f.removeErr != nil {
return f.removeErr
}
if _, ok := f.entries[jobID]; !ok {
return fmt.Errorf("job not found in DLQ")
}
delete(f.entries, jobID)
filtered := make([]string, 0, len(f.order))
for _, id := range f.order {
if id != jobID {
filtered = append(filtered, id)
}
}
f.order = filtered
return nil
}
func (f *fakeDLQ) Retry(ctx context.Context, jobID string) (*model.Job, error) {
if f.retryErr != nil {
return nil, f.retryErr
}
entry, err := f.Get(ctx, jobID)
if err != nil {
return nil, err
}
if err := f.Remove(ctx, jobID); err != nil {
return nil, err
}
return entry.Job, nil
}
type fakeQueue struct {
enqueueErr error
}
func (f *fakeQueue) Enqueue(_ context.Context, _ *model.Job) error { return f.enqueueErr }
func (f *fakeQueue) Dequeue(_ context.Context) (*model.Job, error) { return nil, nil }
func TestAdminDlqJobs(t *testing.T) {
t.Run("list happy path", func(t *testing.T) {
db := newTestDB(t)
ensureTestJobsTable(t, db)
admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
job1 := seedTestJob(t, db, model.Job{ID: "job-dlq-1", CreatedAt: ptrTime(time.Now().Add(-2 * time.Hour).UTC()), UpdatedAt: ptrTime(time.Now().Add(-2 * time.Hour).UTC())})
job2 := seedTestJob(t, db, model.Job{ID: "job-dlq-2", CreatedAt: ptrTime(time.Now().Add(-time.Hour).UTC()), UpdatedAt: ptrTime(time.Now().Add(-time.Hour).UTC())})
services := newTestAppServices(t, db)
services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, &fakeQueue{}, nil, newFakeDLQ(
&redisadapter.DLQEntry{Job: &job1, FailureTime: time.Now().Add(-30 * time.Minute).UTC(), Reason: "lease_expired", RetryCount: 2},
&redisadapter.DLQEntry{Job: &job2, FailureTime: time.Now().Add(-10 * time.Minute).UTC(), Reason: "invalid_config", RetryCount: 3},
)))
conn, cleanup := newTestGRPCServer(t, services)
defer cleanup()
client := newAdminClient(conn)
resp, err := client.ListAdminDlqJobs(testActorOutgoingContext(admin.ID, "ADMIN"), &appv1.ListAdminDlqJobsRequest{Offset: 0, Limit: 10})
if err != nil {
t.Fatalf("ListAdminDlqJobs error = %v", err)
}
if resp.GetTotal() != 2 {
t.Fatalf("total = %d, want 2", resp.GetTotal())
}
if len(resp.GetItems()) != 2 {
t.Fatalf("items len = %d, want 2", len(resp.GetItems()))
}
if resp.GetItems()[0].GetJob().GetId() != "job-dlq-1" {
t.Fatalf("first job id = %q", resp.GetItems()[0].GetJob().GetId())
}
if resp.GetItems()[0].GetReason() != "lease_expired" {
t.Fatalf("first reason = %q", resp.GetItems()[0].GetReason())
}
})
t.Run("get not found", func(t *testing.T) {
db := newTestDB(t)
ensureTestJobsTable(t, db)
admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
services := newTestAppServices(t, db)
services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, &fakeQueue{}, nil, newFakeDLQ()))
conn, cleanup := newTestGRPCServer(t, services)
defer cleanup()
client := newAdminClient(conn)
_, err := client.GetAdminDlqJob(testActorOutgoingContext(admin.ID, "ADMIN"), &appv1.GetAdminDlqJobRequest{Id: "missing"})
assertGRPCCode(t, err, codes.NotFound)
})
t.Run("retry happy path", func(t *testing.T) {
db := newTestDB(t)
ensureTestJobsTable(t, db)
admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
job := seedTestJob(t, db, model.Job{ID: "job-dlq-retry", Status: ptrString("failure"), CreatedAt: ptrTime(time.Now().Add(-time.Hour).UTC()), UpdatedAt: ptrTime(time.Now().Add(-time.Hour).UTC())})
services := newTestAppServices(t, db)
services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, &fakeQueue{}, nil, newFakeDLQ(
&redisadapter.DLQEntry{Job: &job, FailureTime: time.Now().UTC(), Reason: "lease_expired", RetryCount: 1},
)))
conn, cleanup := newTestGRPCServer(t, services)
defer cleanup()
client := newAdminClient(conn)
resp, err := client.RetryAdminDlqJob(testActorOutgoingContext(admin.ID, "ADMIN"), &appv1.RetryAdminDlqJobRequest{Id: job.ID})
if err != nil {
t.Fatalf("RetryAdminDlqJob error = %v", err)
}
if resp.GetJob().GetId() != job.ID {
t.Fatalf("job id = %q, want %q", resp.GetJob().GetId(), job.ID)
}
if resp.GetJob().GetStatus() != "pending" {
t.Fatalf("job status = %q, want pending", resp.GetJob().GetStatus())
}
})
t.Run("remove happy path", func(t *testing.T) {
db := newTestDB(t)
ensureTestJobsTable(t, db)
admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
job := seedTestJob(t, db, model.Job{ID: "job-dlq-remove", CreatedAt: ptrTime(time.Now().Add(-time.Hour).UTC()), UpdatedAt: ptrTime(time.Now().Add(-time.Hour).UTC())})
services := newTestAppServices(t, db)
services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, &fakeQueue{}, nil, newFakeDLQ(
&redisadapter.DLQEntry{Job: &job, FailureTime: time.Now().UTC(), Reason: "invalid_config", RetryCount: 3},
)))
conn, cleanup := newTestGRPCServer(t, services)
defer cleanup()
client := newAdminClient(conn)
resp, err := client.RemoveAdminDlqJob(testActorOutgoingContext(admin.ID, "ADMIN"), &appv1.RemoveAdminDlqJobRequest{Id: job.ID})
if err != nil {
t.Fatalf("RemoveAdminDlqJob error = %v", err)
}
if resp.GetStatus() != "removed" {
t.Fatalf("status = %q, want removed", resp.GetStatus())
}
})
t.Run("list permission denied", func(t *testing.T) {
db := newTestDB(t)
ensureTestJobsTable(t, db)
user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "user@example.com", Role: ptrString("USER")})
services := newTestAppServices(t, db)
services.videoWorkflowService = renderworkflow.New(db, NewJobService(db, &fakeQueue{}, nil, newFakeDLQ()))
conn, cleanup := newTestGRPCServer(t, services)
defer cleanup()
client := newAdminClient(conn)
_, err := client.ListAdminDlqJobs(testActorOutgoingContext(user.ID, "USER"), &appv1.ListAdminDlqJobsRequest{})
assertGRPCCode(t, err, codes.PermissionDenied)
})
}

View File

@@ -0,0 +1,245 @@
package service
import (
"context"
"testing"
"time"
"github.com/google/uuid"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
appv1 "stream.api/internal/api/proto/app/v1"
"stream.api/internal/database/model"
"stream.api/internal/middleware"
)
func testInternalMetadataContext() context.Context {
return metadata.NewOutgoingContext(context.Background(), metadata.Pairs(
middleware.ActorMarkerMetadataKey, testTrustedMarker,
))
}
func testInvalidInternalMetadataContext() context.Context {
return metadata.NewOutgoingContext(context.Background(), metadata.Pairs(
middleware.ActorMarkerMetadataKey, "wrong-marker",
))
}
func TestGetVideoMetadata(t *testing.T) {
t.Run("happy path with owner defaults", func(t *testing.T) {
db := newTestDB(t)
admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "paid@example.com", Role: ptrString("USER"), PlanID: ptrString("plan-paid")})
now := time.Now().UTC()
video := model.Video{ID: uuid.NewString(), UserID: user.ID, Title: "video", URL: "https://cdn.example.com/video.mp4", Status: ptrString("ready"), CreatedAt: &now, UpdatedAt: now, AdID: nil}
if err := db.Create(&video).Error; err != nil {
t.Fatalf("create video: %v", err)
}
if err := db.Create(&model.Domain{ID: uuid.NewString(), UserID: user.ID, Name: "video.example.com"}).Error; err != nil {
t.Fatalf("create domain: %v", err)
}
if err := db.Create(&model.AdTemplate{ID: uuid.NewString(), UserID: user.ID, Name: "default-ad", VastTagURL: "https://ads.example.com/vast", IsDefault: true, IsActive: ptrBool(true)}).Error; err != nil {
t.Fatalf("create ad template: %v", err)
}
if err := db.Create(&model.PopupAd{ID: uuid.NewString(), UserID: user.ID, Type: "banner", Label: "promo", Value: "https://ads.example.com/banner", IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error; err != nil {
t.Fatalf("create popup ad: %v", err)
}
if err := db.Create(&model.PlayerConfig{ID: uuid.NewString(), UserID: user.ID, Name: "default-player", IsDefault: true, IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error; err != nil {
t.Fatalf("create player config: %v", err)
}
services := newTestAppServices(t, db)
_ = admin
conn, cleanup := newTestGRPCServer(t, services)
defer cleanup()
client := newVideoMetadataClient(conn)
resp, err := client.GetVideoMetadata(testInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: video.ID})
if err != nil {
t.Fatalf("GetVideoMetadata error = %v", err)
}
if resp.GetVideo().GetId() != video.ID {
t.Fatalf("video id = %q, want %q", resp.GetVideo().GetId(), video.ID)
}
if resp.GetAdTemplate().GetName() != "default-ad" {
t.Fatalf("ad template name = %q", resp.GetAdTemplate().GetName())
}
if resp.GetActivePopupAd().GetLabel() != "promo" {
t.Fatalf("popup label = %q", resp.GetActivePopupAd().GetLabel())
}
if resp.GetDefaultPlayerConfig().GetName() != "default-player" {
t.Fatalf("player config name = %q", resp.GetDefaultPlayerConfig().GetName())
}
if len(resp.GetDomains()) != 1 {
t.Fatalf("domains len = %d, want 1", len(resp.GetDomains()))
}
})
t.Run("missing ad template returns failed precondition", func(t *testing.T) {
db := newTestDB(t)
user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "paid@example.com", Role: ptrString("USER"), PlanID: ptrString("plan-paid")})
now := time.Now().UTC()
video := model.Video{ID: uuid.NewString(), UserID: user.ID, Title: "video", URL: "https://cdn.example.com/video.mp4", Status: ptrString("ready"), CreatedAt: &now, UpdatedAt: now}
_ = db.Create(&video).Error
_ = db.Create(&model.Domain{ID: uuid.NewString(), UserID: user.ID, Name: "video.example.com"}).Error
_ = db.Create(&model.PopupAd{ID: uuid.NewString(), UserID: user.ID, Type: "banner", Label: "promo", Value: "https://ads.example.com/banner", IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error
_ = db.Create(&model.PlayerConfig{ID: uuid.NewString(), UserID: user.ID, Name: "default-player", IsDefault: true, IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error
services := newTestAppServices(t, db)
conn, cleanup := newTestGRPCServer(t, services)
defer cleanup()
client := newVideoMetadataClient(conn)
_, err := client.GetVideoMetadata(testInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: video.ID})
assertGRPCCode(t, err, codes.FailedPrecondition)
})
t.Run("missing popup ad returns failed precondition", func(t *testing.T) {
db := newTestDB(t)
user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "paid@example.com", Role: ptrString("USER"), PlanID: ptrString("plan-paid")})
now := time.Now().UTC()
video := model.Video{ID: uuid.NewString(), UserID: user.ID, Title: "video", URL: "https://cdn.example.com/video.mp4", Status: ptrString("ready"), CreatedAt: &now, UpdatedAt: now}
_ = db.Create(&video).Error
_ = db.Create(&model.Domain{ID: uuid.NewString(), UserID: user.ID, Name: "video.example.com"}).Error
_ = db.Create(&model.AdTemplate{ID: uuid.NewString(), UserID: user.ID, Name: "default-ad", VastTagURL: "https://ads.example.com/vast", IsDefault: true, IsActive: ptrBool(true)}).Error
_ = db.Create(&model.PlayerConfig{ID: uuid.NewString(), UserID: user.ID, Name: "default-player", IsDefault: true, IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error
services := newTestAppServices(t, db)
conn, cleanup := newTestGRPCServer(t, services)
defer cleanup()
client := newVideoMetadataClient(conn)
_, err := client.GetVideoMetadata(testInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: video.ID})
assertGRPCCode(t, err, codes.FailedPrecondition)
})
t.Run("missing player config returns failed precondition", func(t *testing.T) {
db := newTestDB(t)
user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "paid@example.com", Role: ptrString("USER"), PlanID: ptrString("plan-paid")})
now := time.Now().UTC()
video := model.Video{ID: uuid.NewString(), UserID: user.ID, Title: "video", URL: "https://cdn.example.com/video.mp4", Status: ptrString("ready"), CreatedAt: &now, UpdatedAt: now}
_ = db.Create(&video).Error
_ = db.Create(&model.Domain{ID: uuid.NewString(), UserID: user.ID, Name: "video.example.com"}).Error
_ = db.Create(&model.AdTemplate{ID: uuid.NewString(), UserID: user.ID, Name: "default-ad", VastTagURL: "https://ads.example.com/vast", IsDefault: true, IsActive: ptrBool(true)}).Error
_ = db.Create(&model.PopupAd{ID: uuid.NewString(), UserID: user.ID, Type: "banner", Label: "promo", Value: "https://ads.example.com/banner", IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error
services := newTestAppServices(t, db)
conn, cleanup := newTestGRPCServer(t, services)
defer cleanup()
client := newVideoMetadataClient(conn)
_, err := client.GetVideoMetadata(testInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: video.ID})
assertGRPCCode(t, err, codes.FailedPrecondition)
})
t.Run("missing domains returns failed precondition", func(t *testing.T) {
db := newTestDB(t)
user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "paid@example.com", Role: ptrString("USER"), PlanID: ptrString("plan-paid")})
now := time.Now().UTC()
video := model.Video{ID: uuid.NewString(), UserID: user.ID, Title: "video", URL: "https://cdn.example.com/video.mp4", Status: ptrString("ready"), CreatedAt: &now, UpdatedAt: now}
_ = db.Create(&video).Error
_ = db.Create(&model.AdTemplate{ID: uuid.NewString(), UserID: user.ID, Name: "default-ad", VastTagURL: "https://ads.example.com/vast", IsDefault: true, IsActive: ptrBool(true)}).Error
_ = db.Create(&model.PopupAd{ID: uuid.NewString(), UserID: user.ID, Type: "banner", Label: "promo", Value: "https://ads.example.com/banner", IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error
_ = db.Create(&model.PlayerConfig{ID: uuid.NewString(), UserID: user.ID, Name: "default-player", IsDefault: true, IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error
services := newTestAppServices(t, db)
conn, cleanup := newTestGRPCServer(t, services)
defer cleanup()
client := newVideoMetadataClient(conn)
_, err := client.GetVideoMetadata(testInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: video.ID})
assertGRPCCode(t, err, codes.FailedPrecondition)
})
t.Run("free user falls back to system admin config", func(t *testing.T) {
db := newTestDB(t)
admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "free@example.com", Role: ptrString("USER")})
now := time.Now().UTC()
video := model.Video{ID: uuid.NewString(), UserID: user.ID, Title: "video", URL: "https://cdn.example.com/video.mp4", Status: ptrString("ready"), CreatedAt: &now, UpdatedAt: now}
if err := db.Create(&video).Error; err != nil {
t.Fatalf("create video: %v", err)
}
if err := db.Create(&model.Domain{ID: uuid.NewString(), UserID: user.ID, Name: "free.example.com"}).Error; err != nil {
t.Fatalf("create domain: %v", err)
}
if err := db.Create(&model.AdTemplate{ID: uuid.NewString(), UserID: admin.ID, Name: "system-ad", VastTagURL: "https://ads.example.com/system", IsDefault: true, IsActive: ptrBool(true)}).Error; err != nil {
t.Fatalf("create system ad template: %v", err)
}
if err := db.Create(&model.PopupAd{ID: uuid.NewString(), UserID: admin.ID, Type: "banner", Label: "system-popup", Value: "https://ads.example.com/system-popup", IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error; err != nil {
t.Fatalf("create system popup ad: %v", err)
}
if err := db.Create(&model.PlayerConfig{ID: uuid.NewString(), UserID: admin.ID, Name: "system-player", IsDefault: true, IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error; err != nil {
t.Fatalf("create system player config: %v", err)
}
services := newTestAppServices(t, db)
conn, cleanup := newTestGRPCServer(t, services)
defer cleanup()
client := newVideoMetadataClient(conn)
resp, err := client.GetVideoMetadata(testInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: video.ID})
if err != nil {
t.Fatalf("GetVideoMetadata error = %v", err)
}
if resp.GetAdTemplate().GetName() != "system-ad" {
t.Fatalf("ad template = %q, want system-ad", resp.GetAdTemplate().GetName())
}
if resp.GetActivePopupAd().GetLabel() != "system-popup" {
t.Fatalf("popup label = %q, want system-popup", resp.GetActivePopupAd().GetLabel())
}
if resp.GetDefaultPlayerConfig().GetName() != "system-player" {
t.Fatalf("player config = %q, want system-player", resp.GetDefaultPlayerConfig().GetName())
}
if len(resp.GetDomains()) != 1 || resp.GetDomains()[0].GetName() != "free.example.com" {
t.Fatalf("domains = %#v, want owner domains", resp.GetDomains())
}
})
t.Run("video ad id takes precedence over fallback ad template", func(t *testing.T) {
db := newTestDB(t)
admin := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "admin@example.com", Role: ptrString("ADMIN")})
user := seedTestUser(t, db, model.User{ID: uuid.NewString(), Email: "free@example.com", Role: ptrString("USER")})
now := time.Now().UTC()
videoAd := model.AdTemplate{ID: uuid.NewString(), UserID: user.ID, Name: "video-ad", VastTagURL: "https://ads.example.com/video", IsDefault: false, IsActive: ptrBool(true)}
if err := db.Create(&videoAd).Error; err != nil {
t.Fatalf("create video ad template: %v", err)
}
video := model.Video{ID: uuid.NewString(), UserID: user.ID, Title: "video", URL: "https://cdn.example.com/video.mp4", Status: ptrString("ready"), CreatedAt: &now, UpdatedAt: now, AdID: &videoAd.ID}
if err := db.Create(&video).Error; err != nil {
t.Fatalf("create video: %v", err)
}
if err := db.Create(&model.Domain{ID: uuid.NewString(), UserID: user.ID, Name: "free.example.com"}).Error; err != nil {
t.Fatalf("create domain: %v", err)
}
if err := db.Create(&model.AdTemplate{ID: uuid.NewString(), UserID: admin.ID, Name: "system-ad", VastTagURL: "https://ads.example.com/system", IsDefault: true, IsActive: ptrBool(true)}).Error; err != nil {
t.Fatalf("create system ad template: %v", err)
}
if err := db.Create(&model.PopupAd{ID: uuid.NewString(), UserID: admin.ID, Type: "banner", Label: "system-popup", Value: "https://ads.example.com/system-popup", IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error; err != nil {
t.Fatalf("create system popup ad: %v", err)
}
if err := db.Create(&model.PlayerConfig{ID: uuid.NewString(), UserID: admin.ID, Name: "system-player", IsDefault: true, IsActive: ptrBool(true), CreatedAt: &now, UpdatedAt: now}).Error; err != nil {
t.Fatalf("create system player config: %v", err)
}
services := newTestAppServices(t, db)
_ = admin
conn, cleanup := newTestGRPCServer(t, services)
defer cleanup()
client := newVideoMetadataClient(conn)
resp, err := client.GetVideoMetadata(testInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: video.ID})
if err != nil {
t.Fatalf("GetVideoMetadata error = %v", err)
}
if resp.GetAdTemplate().GetName() != "video-ad" {
t.Fatalf("ad template = %q, want video-ad", resp.GetAdTemplate().GetName())
}
})
t.Run("invalid marker returns unauthenticated", func(t *testing.T) {
db := newTestDB(t)
services := newTestAppServices(t, db)
conn, cleanup := newTestGRPCServer(t, services)
defer cleanup()
client := newVideoMetadataClient(conn)
_, err := client.GetVideoMetadata(testInvalidInternalMetadataContext(), &appv1.GetVideoMetadataRequest{VideoId: uuid.NewString()})
assertGRPCCode(t, err, codes.Unauthenticated)
})
t.Run("missing marker returns unauthenticated", func(t *testing.T) {
db := newTestDB(t)
services := newTestAppServices(t, db)
conn, cleanup := newTestGRPCServer(t, services)
defer cleanup()
client := newVideoMetadataClient(conn)
_, err := client.GetVideoMetadata(context.Background(), &appv1.GetVideoMetadataRequest{VideoId: uuid.NewString()})
assertGRPCCode(t, err, codes.Unauthenticated)
})
}

View File

@@ -9,13 +9,13 @@ import (
"time"
"github.com/google/uuid"
_ "github.com/mattn/go-sqlite3"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/test/bufconn"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
_ "github.com/mattn/go-sqlite3"
appv1 "stream.api/internal/api/proto/app/v1"
"stream.api/internal/database/model"
"stream.api/internal/database/query"
@@ -210,14 +210,11 @@ func newTestDB(t *testing.T) *gorm.DB {
`CREATE TABLE popup_ads (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
title TEXT NOT NULL,
image_url TEXT NOT NULL,
target_url TEXT NOT NULL,
type TEXT NOT NULL,
label TEXT NOT NULL,
value TEXT NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT 1,
start_at DATETIME,
end_at DATETIME,
priority INTEGER NOT NULL DEFAULT 0,
close_cooldown_minutes INTEGER NOT NULL DEFAULT 60,
max_triggers_per_session INTEGER,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME,
version INTEGER NOT NULL DEFAULT 1
@@ -273,6 +270,7 @@ func newTestGRPCServer(t *testing.T, services *appServices) (*grpc.ClientConn, f
PlansServer: services,
PaymentsServer: services,
VideosServer: services,
VideoMetadataServer: services,
AdminServer: services,
})
@@ -407,6 +405,10 @@ func newAdminClient(conn *grpc.ClientConn) appv1.AdminClient {
return appv1.NewAdminClient(conn)
}
func newVideoMetadataClient(conn *grpc.ClientConn) appv1.VideoMetadataClient {
return appv1.NewVideoMetadataClient(conn)
}
func ptrTime(v time.Time) *time.Time { return &v }
func seedTestPopupAd(t *testing.T, db *gorm.DB, item model.PopupAd) model.PopupAd {