✨ Add liveBroadcast resource package

This commit is contained in:
OpenWaygate committed 2026-07-13 23:53:47 +08:00
1 parent 9d05ba30e0
commit a3a627d13f
3 files changed
+1177

No files matched your search

+23
View File
@@ -0,0 +1,23 @@
load("@rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "liveBroadcast",
srcs = ["liveBroadcast.go"],
importpath = "github.com/eat-pray-ai/yutu/pkg/liveBroadcast",
visibility = ["//visibility:public"],
deps = [
"//pkg/common",
"@com_github_jedib0t_go_pretty_v6//table",
"@org_golang_google_api//youtube/v3:youtube",
],
)
go_test(
name = "liveBroadcast_test",
srcs = ["liveBroadcast_test.go"],
embed = [":liveBroadcast"],
deps = [
"//pkg/common",
"@org_golang_google_api//youtube/v3:youtube",
],
)
+430
View File
@@ -0,0 +1,430 @@
// Copyright 2026 eat-pray-ai & OpenWaygate
// SPDX-License-Identifier: Apache-2.0
package liveBroadcast
import (
"errors"
"fmt"
"io"
"github.com/eat-pray-ai/yutu/pkg/common"
"github.com/jedib0t/go-pretty/v6/table"
"google.golang.org/api/youtube/v3"
)
var (
errGetLiveBroadcast = errors.New("failed to get live broadcast")
errInsertLiveBroadcast = errors.New("failed to insert live broadcast")
errUpdateLiveBroadcast = errors.New("failed to update live broadcast")
errDeleteLiveBroadcast = errors.New("failed to delete live broadcast")
errBindLiveBroadcast = errors.New("failed to bind live broadcast")
errTransitionLiveBroadcast = errors.New("failed to transition live broadcast")
errInsertCuepointLiveBroadcast = errors.New("failed to insert cuepoint")
)
type LiveBroadcast struct {
common.Fields
Title string `yaml:"title" json:"title,omitempty"`
Description string `yaml:"description" json:"description,omitempty"`
Mine *bool `yaml:"mine" json:"mine,omitempty"`
BroadcastStatus string `yaml:"broadcast_status" json:"broadcast_status,omitempty"`
BroadcastType string `yaml:"broadcast_type" json:"broadcast_type,omitempty"`
PrivacyStatus string `yaml:"privacy_status" json:"privacy_status,omitempty"`
ScheduledStartTime string `yaml:"scheduled_start_time" json:"scheduled_start_time,omitempty"`
ScheduledEndTime string `yaml:"scheduled_end_time" json:"scheduled_end_time,omitempty"`
StreamId string `yaml:"stream_id" json:"stream_id,omitempty"`
CueType string `yaml:"cue_type" json:"cue_type,omitempty"`
CueDurationSecs int64 `yaml:"cue_duration_secs" json:"cue_duration_secs,omitempty"`
CueInsertionOffsetMs int64 `yaml:"cue_insertion_offset_ms" json:"cue_insertion_offset_ms,omitempty"`
CueWalltimeMs uint64 `yaml:"cue_walltime_ms" json:"cue_walltime_ms,omitempty"`
OnBehalfOfContentOwnerChannel string `yaml:"on_behalf_of_content_owner_channel" json:"on_behalf_of_content_owner_channel,omitempty"`
}
type ILiveBroadcast[T any] interface {
List(io.Writer) error
Get() ([]*T, error)
Insert(io.Writer) error
Update(io.Writer) error
Delete(io.Writer) error
Bind(io.Writer) error
Transition(io.Writer) error
InsertCuepoint(io.Writer) error
}
type Option func(*LiveBroadcast)
func NewLiveBroadcast(opts ...Option) ILiveBroadcast[youtube.LiveBroadcast] {
b := &LiveBroadcast{Fields: common.Fields{}}
for _, opt := range opts {
opt(b)
}
return b
}
func (b *LiveBroadcast) Get() ([]*youtube.LiveBroadcast, error) {
if err := b.EnsureService(); err != nil {
return nil, err
}
call := b.Service.LiveBroadcasts.List(b.Parts)
if len(b.Ids) > 0 {
call = call.Id(b.Ids...)
}
if b.Mine != nil && *b.Mine {
call = call.Mine(true)
}
if b.BroadcastStatus != "" {
call = call.BroadcastStatus(b.BroadcastStatus)
}
if b.BroadcastType != "" {
call = call.BroadcastType(b.BroadcastType)
}
if b.OnBehalfOfContentOwner != "" {
call = call.OnBehalfOfContentOwner(b.OnBehalfOfContentOwner)
}
if b.OnBehalfOfContentOwnerChannel != "" {
call = call.OnBehalfOfContentOwnerChannel(b.OnBehalfOfContentOwnerChannel)
}
return common.Paginate(
&b.Fields, call,
func(r *youtube.LiveBroadcastListResponse) ([]*youtube.LiveBroadcast, string) {
return r.Items, r.NextPageToken
}, errGetLiveBroadcast,
)
}
func (b *LiveBroadcast) List(writer io.Writer) error {
broadcasts, err := b.Get()
if err != nil && broadcasts == nil {
return err
}
common.PrintList(
b.Output, broadcasts, writer,
table.Row{"ID", "Title", "Status", "Privacy"},
func(bc *youtube.LiveBroadcast) table.Row {
title := ""
status := ""
privacy := ""
if bc.Snippet != nil {
title = bc.Snippet.Title
}
if bc.Status != nil {
status = bc.Status.LifeCycleStatus
privacy = bc.Status.PrivacyStatus
}
return table.Row{bc.Id, title, status, privacy}
},
)
return err
}
func (b *LiveBroadcast) Insert(writer io.Writer) error {
if err := b.EnsureService(); err != nil {
return err
}
broadcast := &youtube.LiveBroadcast{
Snippet: &youtube.LiveBroadcastSnippet{
Title: b.Title,
Description: b.Description,
ScheduledStartTime: b.ScheduledStartTime,
ScheduledEndTime: b.ScheduledEndTime,
},
Status: &youtube.LiveBroadcastStatus{
PrivacyStatus: b.PrivacyStatus,
},
}
call := b.Service.LiveBroadcasts.Insert(b.Parts, broadcast)
if b.OnBehalfOfContentOwner != "" {
call = call.OnBehalfOfContentOwner(b.OnBehalfOfContentOwner)
}
if b.OnBehalfOfContentOwnerChannel != "" {
call = call.OnBehalfOfContentOwnerChannel(b.OnBehalfOfContentOwnerChannel)
}
res, err := call.Do()
if err != nil {
return errors.Join(errInsertLiveBroadcast, err)
}
common.PrintResult(
b.Output, res, writer, "Live broadcast inserted: %s\n", res.Id,
)
return nil
}
func (b *LiveBroadcast) Update(writer io.Writer) error {
if err := b.EnsureService(); err != nil {
return err
}
b.Parts = []string{"id", "snippet", "status"}
broadcasts, err := b.Get()
if err != nil {
return errors.Join(errUpdateLiveBroadcast, err)
}
if len(broadcasts) == 0 {
return errGetLiveBroadcast
}
original := broadcasts[0]
broadcast := &youtube.LiveBroadcast{
Id: original.Id,
Snippet: &youtube.LiveBroadcastSnippet{
Title: original.Snippet.Title,
Description: original.Snippet.Description,
ScheduledStartTime: original.Snippet.ScheduledStartTime,
ScheduledEndTime: original.Snippet.ScheduledEndTime,
},
Status: &youtube.LiveBroadcastStatus{},
}
if original.Status != nil {
broadcast.Status.PrivacyStatus = original.Status.PrivacyStatus
}
if b.Title != "" {
broadcast.Snippet.Title = b.Title
}
if b.Description != "" {
broadcast.Snippet.Description = b.Description
}
if b.ScheduledStartTime != "" {
broadcast.Snippet.ScheduledStartTime = b.ScheduledStartTime
}
if b.ScheduledEndTime != "" {
broadcast.Snippet.ScheduledEndTime = b.ScheduledEndTime
}
if b.PrivacyStatus != "" {
broadcast.Status.PrivacyStatus = b.PrivacyStatus
}
call := b.Service.LiveBroadcasts.Update(
[]string{"snippet", "status"}, broadcast,
)
if b.OnBehalfOfContentOwner != "" {
call = call.OnBehalfOfContentOwner(b.OnBehalfOfContentOwner)
}
if b.OnBehalfOfContentOwnerChannel != "" {
call = call.OnBehalfOfContentOwnerChannel(b.OnBehalfOfContentOwnerChannel)
}
res, err := call.Do()
if err != nil {
return errors.Join(errUpdateLiveBroadcast, err)
}
common.PrintResult(
b.Output, res, writer, "Live broadcast updated: %s\n", res.Id,
)
return nil
}
func (b *LiveBroadcast) Delete(writer io.Writer) error {
if err := b.EnsureService(); err != nil {
return err
}
for _, id := range b.Ids {
call := b.Service.LiveBroadcasts.Delete(id)
if b.OnBehalfOfContentOwner != "" {
call = call.OnBehalfOfContentOwner(b.OnBehalfOfContentOwner)
}
if b.OnBehalfOfContentOwnerChannel != "" {
call = call.OnBehalfOfContentOwnerChannel(b.OnBehalfOfContentOwnerChannel)
}
err := call.Do()
if err != nil {
return errors.Join(errDeleteLiveBroadcast, err)
}
_, _ = fmt.Fprintf(writer, "Live broadcast %s deleted\n", id)
}
return nil
}
func (b *LiveBroadcast) Bind(writer io.Writer) error {
if err := b.EnsureService(); err != nil {
return err
}
for _, id := range b.Ids {
call := b.Service.LiveBroadcasts.Bind(id, b.Parts)
if b.StreamId != "" {
call = call.StreamId(b.StreamId)
}
if b.OnBehalfOfContentOwner != "" {
call = call.OnBehalfOfContentOwner(b.OnBehalfOfContentOwner)
}
if b.OnBehalfOfContentOwnerChannel != "" {
call = call.OnBehalfOfContentOwnerChannel(b.OnBehalfOfContentOwnerChannel)
}
res, err := call.Do()
if err != nil {
return errors.Join(errBindLiveBroadcast, err)
}
common.PrintResult(
b.Output, res, writer, "Live broadcast %s bound to stream %s\n", res.Id,
b.StreamId,
)
}
return nil
}
func (b *LiveBroadcast) Transition(writer io.Writer) error {
if err := b.EnsureService(); err != nil {
return err
}
for _, id := range b.Ids {
call := b.Service.LiveBroadcasts.Transition(b.BroadcastStatus, id, b.Parts)
if b.OnBehalfOfContentOwner != "" {
call = call.OnBehalfOfContentOwner(b.OnBehalfOfContentOwner)
}
if b.OnBehalfOfContentOwnerChannel != "" {
call = call.OnBehalfOfContentOwnerChannel(b.OnBehalfOfContentOwnerChannel)
}
res, err := call.Do()
if err != nil {
return errors.Join(errTransitionLiveBroadcast, err)
}
common.PrintResult(
b.Output, res, writer,
"Live broadcast %s transitioned to %s\n", id, b.BroadcastStatus,
)
}
return nil
}
func (b *LiveBroadcast) InsertCuepoint(writer io.Writer) error {
if err := b.EnsureService(); err != nil {
return err
}
cuepoint := &youtube.Cuepoint{
CueType: b.CueType,
DurationSecs: b.CueDurationSecs,
InsertionOffsetTimeMs: b.CueInsertionOffsetMs,
WalltimeMs: b.CueWalltimeMs,
}
for _, id := range b.Ids {
call := b.Service.LiveBroadcasts.InsertCuepoint(cuepoint).Id(id)
if b.OnBehalfOfContentOwner != "" {
call = call.OnBehalfOfContentOwner(b.OnBehalfOfContentOwner)
}
if b.OnBehalfOfContentOwnerChannel != "" {
call = call.OnBehalfOfContentOwnerChannel(b.OnBehalfOfContentOwnerChannel)
}
res, err := call.Do()
if err != nil {
return errors.Join(errInsertCuepointLiveBroadcast, err)
}
common.PrintResult(
b.Output, res, writer, "Cuepoint inserted for broadcast %s: %s\n", id,
res.Id,
)
}
return nil
}
func WithTitle(title string) Option {
return func(b *LiveBroadcast) {
b.Title = title
}
}
func WithDescription(description string) Option {
return func(b *LiveBroadcast) {
b.Description = description
}
}
func WithMine(mine *bool) Option {
return func(b *LiveBroadcast) {
if mine != nil {
b.Mine = mine
}
}
}
func WithBroadcastStatus(broadcastStatus string) Option {
return func(b *LiveBroadcast) {
b.BroadcastStatus = broadcastStatus
}
}
func WithBroadcastType(broadcastType string) Option {
return func(b *LiveBroadcast) {
b.BroadcastType = broadcastType
}
}
func WithPrivacyStatus(privacyStatus string) Option {
return func(b *LiveBroadcast) {
b.PrivacyStatus = privacyStatus
}
}
func WithScheduledStartTime(scheduledStartTime string) Option {
return func(b *LiveBroadcast) {
b.ScheduledStartTime = scheduledStartTime
}
}
func WithScheduledEndTime(scheduledEndTime string) Option {
return func(b *LiveBroadcast) {
b.ScheduledEndTime = scheduledEndTime
}
}
func WithStreamId(streamId string) Option {
return func(b *LiveBroadcast) {
b.StreamId = streamId
}
}
func WithCueType(cueType string) Option {
return func(b *LiveBroadcast) {
b.CueType = cueType
}
}
func WithCueDurationSecs(durationSecs int64) Option {
return func(b *LiveBroadcast) {
b.CueDurationSecs = durationSecs
}
}
func WithCueInsertionOffsetMs(offsetMs int64) Option {
return func(b *LiveBroadcast) {
b.CueInsertionOffsetMs = offsetMs
}
}
func WithCueWalltimeMs(walltimeMs uint64) Option {
return func(b *LiveBroadcast) {
b.CueWalltimeMs = walltimeMs
}
}
func WithOnBehalfOfContentOwnerChannel(channel string) Option {
return func(b *LiveBroadcast) {
b.OnBehalfOfContentOwnerChannel = channel
}
}
var (
WithMaxResults = common.WithMaxResults[*LiveBroadcast]
WithParts = common.WithParts[*LiveBroadcast]
WithOutput = common.WithOutput[*LiveBroadcast]
WithService = common.WithService[*LiveBroadcast]
WithIds = common.WithIds[*LiveBroadcast]
WithOnBehalfOfContentOwner = common.WithOnBehalfOfContentOwner[*LiveBroadcast]
)
+724
View File
@@ -0,0 +1,724 @@
// Copyright 2026 eat-pray-ai & OpenWaygate
// SPDX-License-Identifier: Apache-2.0
package liveBroadcast
import (
"bytes"
"encoding/json"
"io"
"math"
"net/http"
"reflect"
"testing"
"github.com/eat-pray-ai/yutu/pkg/common"
"google.golang.org/api/youtube/v3"
)
func TestNewLiveBroadcast(t *testing.T) {
type args struct {
opts []Option
}
svc := &youtube.Service{}
mine := true
tests := []struct {
name string
args args
want ILiveBroadcast[youtube.LiveBroadcast]
}{
{
name: "with all options",
args: args{
opts: []Option{
WithIds([]string{"broadcast1", "broadcast2"}),
WithTitle("Test Broadcast"),
WithDescription("A test broadcast"),
WithMine(&mine),
WithBroadcastStatus("active"),
WithBroadcastType("event"),
WithPrivacyStatus("public"),
WithScheduledStartTime("2026-01-01T00:00:00Z"),
WithScheduledEndTime("2026-01-01T01:00:00Z"),
WithStreamId("stream123"),
WithCueType("cueTypeAd"),
WithCueDurationSecs(30),
WithCueInsertionOffsetMs(5000),
WithCueWalltimeMs(1234567890),
WithMaxResults(50),
WithParts([]string{"snippet", "status", "contentDetails"}),
WithOutput("json"),
WithService(svc),
WithOnBehalfOfContentOwner("owner123"),
WithOnBehalfOfContentOwnerChannel("channel123"),
},
},
want: &LiveBroadcast{
Fields: common.Fields{
Service: svc,
Parts: []string{"snippet", "status", "contentDetails"},
Output: "json",
MaxResults: 50,
Ids: []string{"broadcast1", "broadcast2"},
OnBehalfOfContentOwner: "owner123",
},
Title: "Test Broadcast",
Description: "A test broadcast",
Mine: &mine,
BroadcastStatus: "active",
BroadcastType: "event",
PrivacyStatus: "public",
ScheduledStartTime: "2026-01-01T00:00:00Z",
ScheduledEndTime: "2026-01-01T01:00:00Z",
StreamId: "stream123",
CueType: "cueTypeAd",
CueDurationSecs: 30,
CueInsertionOffsetMs: 5000,
CueWalltimeMs: 1234567890,
OnBehalfOfContentOwnerChannel: "channel123",
},
},
{
name: "with no options",
args: args{
opts: []Option{},
},
want: &LiveBroadcast{Fields: common.Fields{}},
},
{
name: "with zero max results",
args: args{
opts: []Option{
WithMaxResults(0),
},
},
want: &LiveBroadcast{
Fields: common.Fields{MaxResults: math.MaxInt64},
},
},
{
name: "with negative max results",
args: args{
opts: []Option{
WithMaxResults(-10),
},
},
want: &LiveBroadcast{
Fields: common.Fields{MaxResults: 1},
},
},
{
name: "with empty string values",
args: args{
opts: []Option{
WithTitle(""),
WithDescription(""),
WithBroadcastStatus(""),
WithBroadcastType(""),
WithPrivacyStatus(""),
WithScheduledStartTime(""),
WithScheduledEndTime(""),
WithStreamId(""),
},
},
want: &LiveBroadcast{
Fields: common.Fields{},
Title: "",
Description: "",
BroadcastStatus: "",
BroadcastType: "",
PrivacyStatus: "",
ScheduledStartTime: "",
ScheduledEndTime: "",
StreamId: "",
},
},
{
name: "with partial options",
args: args{
opts: []Option{
WithTitle("My Broadcast"),
WithPrivacyStatus("private"),
WithMaxResults(25),
WithParts([]string{"snippet"}),
},
},
want: &LiveBroadcast{
Fields: common.Fields{
Parts: []string{"snippet"},
MaxResults: 25,
},
Title: "My Broadcast",
PrivacyStatus: "private",
},
},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
if got := NewLiveBroadcast(tt.args.opts...); !reflect.DeepEqual(
got, tt.want,
) {
t.Errorf("%s\nNewLiveBroadcast() = %v\nwant %v", tt.name, got, tt.want)
}
},
)
}
}
func TestLiveBroadcast_Get(t *testing.T) {
tests := []struct {
name string
opts []Option
verify func(*http.Request)
wantLen int
wantErr bool
}{
{
name: "get live broadcasts by id",
opts: []Option{
WithIds([]string{"broadcast-1"}),
WithMaxResults(1),
},
verify: func(r *http.Request) {
q := r.URL.Query()
if q.Get("id") != "broadcast-1" {
t.Errorf(
"expected id=broadcast-1, got %s",
q.Get("id"),
)
}
},
wantLen: 1,
wantErr: false,
},
{
name: "get live broadcasts with mine",
opts: func() []Option {
mine := true
return []Option{
WithMine(&mine),
WithMaxResults(1),
}
}(),
verify: func(r *http.Request) {
q := r.URL.Query()
if q.Get("mine") != "true" {
t.Errorf(
"expected mine=true, got %s",
q.Get("mine"),
)
}
},
wantLen: 1,
wantErr: false,
},
{
name: "get live broadcasts with status filter",
opts: []Option{
WithBroadcastStatus("active"),
WithMaxResults(1),
},
verify: func(r *http.Request) {
q := r.URL.Query()
if q.Get("broadcastStatus") != "active" {
t.Errorf(
"expected broadcastStatus=active, got %s",
q.Get("broadcastStatus"),
)
}
},
wantLen: 1,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
svc := common.NewTestService(
t, http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
if tt.verify != nil {
tt.verify(r)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(
[]byte(`{
"items": [
{
"id": "broadcast-1",
"snippet": {
"title": "Test Broadcast",
"description": "A test broadcast"
},
"status": {
"lifeCycleStatus": "ready",
"privacyStatus": "public"
}
}
]
}`),
)
},
),
)
opts := append([]Option{WithService(svc)}, tt.opts...)
b := NewLiveBroadcast(opts...)
got, err := b.Get()
if (err != nil) != tt.wantErr {
t.Errorf("LiveBroadcast.Get() error = %v, wantErr %v", err, tt.wantErr)
return
}
if len(got) != tt.wantLen {
t.Errorf(
"LiveBroadcast.Get() got length = %v, want %v", len(got), tt.wantLen,
)
}
},
)
}
}
func TestLiveBroadcast_Get_Pagination(t *testing.T) {
svc := common.NewTestService(t, common.PaginationHandler("broadcast"))
mine := true
b := NewLiveBroadcast(
WithService(svc),
WithMine(&mine),
WithMaxResults(22),
)
got, err := b.Get()
if err != nil {
t.Errorf("LiveBroadcast.Get() error = %v", err)
}
if len(got) != 22 {
t.Errorf("LiveBroadcast.Get() got length = %v, want 22", len(got))
}
}
func TestLiveBroadcast_List(t *testing.T) {
mockResponse := `{
"items": [
{
"id": "broadcast-1",
"snippet": {
"title": "Test Broadcast",
"description": "A test broadcast"
},
"status": {
"lifeCycleStatus": "ready",
"privacyStatus": "public"
}
}
]
}`
mine := true
common.RunListTest(
t, mockResponse,
func(svc *youtube.Service, output string) func(io.Writer) error {
b := NewLiveBroadcast(
WithService(svc),
WithMine(&mine),
WithOutput(output),
WithMaxResults(1),
)
return b.List
},
)
}
func TestLiveBroadcast_Insert(t *testing.T) {
tests := []struct {
name string
opts []Option
verify func(*http.Request)
wantErr bool
}{
{
name: "insert live broadcast",
opts: []Option{
WithTitle("New Broadcast"),
WithDescription("A new live broadcast"),
WithPrivacyStatus("public"),
WithScheduledStartTime("2026-01-01T00:00:00Z"),
WithScheduledEndTime("2026-01-01T01:00:00Z"),
WithParts([]string{"snippet", "status"}),
},
verify: func(r *http.Request) {
if r.Method != "POST" {
t.Errorf("expected POST, got %s", r.Method)
}
defer func() { _ = r.Body.Close() }()
var body struct {
Snippet struct {
Title string `json:"title"`
Description string `json:"description"`
ScheduledStartTime string `json:"scheduledStartTime"`
ScheduledEndTime string `json:"scheduledEndTime"`
} `json:"snippet"`
Status struct {
PrivacyStatus string `json:"privacyStatus"`
} `json:"status"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("failed to decode request body: %v", err)
}
if body.Snippet.Title != "New Broadcast" {
t.Errorf(
"expected snippet.title=New Broadcast, got %s", body.Snippet.Title,
)
}
if body.Snippet.Description != "A new live broadcast" {
t.Errorf(
"expected snippet.description=A new live broadcast, got %s",
body.Snippet.Description,
)
}
if body.Status.PrivacyStatus != "public" {
t.Errorf(
"expected status.privacyStatus=public, got %s",
body.Status.PrivacyStatus,
)
}
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
svc := common.NewTestService(
t, http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
if tt.verify != nil {
tt.verify(r)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id": "new-broadcast-id"}`))
},
),
)
opts := append([]Option{WithService(svc)}, tt.opts...)
b := NewLiveBroadcast(opts...)
var buf bytes.Buffer
if err := b.Insert(&buf); (err != nil) != tt.wantErr {
t.Errorf(
"LiveBroadcast.Insert() error = %v, wantErr %v", err, tt.wantErr,
)
}
},
)
}
}
func TestLiveBroadcast_Update(t *testing.T) {
tests := []struct {
name string
opts []Option
wantErr bool
}{
{
name: "update live broadcast",
opts: []Option{
WithIds([]string{"broadcast-1"}),
WithTitle("Updated Broadcast"),
WithDescription("Updated description"),
WithMaxResults(1),
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
callCount := 0
svc := common.NewTestService(
t, http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
callCount++
if callCount == 1 {
_, _ = w.Write(
[]byte(`{
"items": [{
"id": "broadcast-1",
"snippet": {
"title": "Old Broadcast",
"description": "Old description",
"scheduledStartTime": "2026-01-01T00:00:00Z",
"scheduledEndTime": "2026-01-01T01:00:00Z"
},
"status": {"privacyStatus": "public", "lifeCycleStatus": "ready"}
}]
}`),
)
} else {
_, _ = w.Write([]byte(`{"id": "broadcast-1"}`))
}
},
),
)
opts := append([]Option{WithService(svc)}, tt.opts...)
b := NewLiveBroadcast(opts...)
var buf bytes.Buffer
if err := b.Update(&buf); (err != nil) != tt.wantErr {
t.Errorf(
"LiveBroadcast.Update() error = %v, wantErr %v", err, tt.wantErr,
)
}
},
)
}
}
func TestLiveBroadcast_Delete(t *testing.T) {
tests := []struct {
name string
opts []Option
verify func(*http.Request)
wantErr bool
}{
{
name: "delete live broadcast",
opts: []Option{
WithIds([]string{"broadcast-id"}),
},
verify: func(r *http.Request) {
if r.Method != "DELETE" {
t.Errorf("expected DELETE, got %s", r.Method)
}
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
svc := common.NewTestService(
t, http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
if tt.verify != nil {
tt.verify(r)
}
w.WriteHeader(http.StatusNoContent)
},
),
)
opts := append([]Option{WithService(svc)}, tt.opts...)
b := NewLiveBroadcast(opts...)
var buf bytes.Buffer
if err := b.Delete(&buf); (err != nil) != tt.wantErr {
t.Errorf(
"LiveBroadcast.Delete() error = %v, wantErr %v", err, tt.wantErr,
)
}
},
)
}
}
func TestLiveBroadcast_Bind(t *testing.T) {
tests := []struct {
name string
opts []Option
verify func(*http.Request)
wantErr bool
}{
{
name: "bind live broadcast to stream",
opts: []Option{
WithIds([]string{"broadcast-id"}),
WithStreamId("stream-id"),
WithParts([]string{"id", "snippet", "contentDetails", "status"}),
},
verify: func(r *http.Request) {
if r.Method != "POST" {
t.Errorf("expected POST, got %s", r.Method)
}
q := r.URL.Query()
if q.Get("id") != "broadcast-id" {
t.Errorf("expected id=broadcast-id, got %s", q.Get("id"))
}
if q.Get("streamId") != "stream-id" {
t.Errorf("expected streamId=stream-id, got %s", q.Get("streamId"))
}
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
svc := common.NewTestService(
t, http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
if tt.verify != nil {
tt.verify(r)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id": "broadcast-id"}`))
},
),
)
opts := append([]Option{WithService(svc)}, tt.opts...)
b := NewLiveBroadcast(opts...)
var buf bytes.Buffer
if err := b.Bind(&buf); (err != nil) != tt.wantErr {
t.Errorf(
"LiveBroadcast.Bind() error = %v, wantErr %v", err, tt.wantErr,
)
}
},
)
}
}
func TestLiveBroadcast_Transition(t *testing.T) {
tests := []struct {
name string
opts []Option
verify func(*http.Request)
wantErr bool
}{
{
name: "transition live broadcast",
opts: []Option{
WithIds([]string{"broadcast-id"}),
WithBroadcastStatus("live"),
WithParts([]string{"id", "snippet", "status"}),
},
verify: func(r *http.Request) {
if r.Method != "POST" {
t.Errorf("expected POST, got %s", r.Method)
}
q := r.URL.Query()
if q.Get("id") != "broadcast-id" {
t.Errorf("expected id=broadcast-id, got %s", q.Get("id"))
}
if q.Get("broadcastStatus") != "live" {
t.Errorf(
"expected broadcastStatus=live, got %s",
q.Get("broadcastStatus"),
)
}
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
svc := common.NewTestService(
t, http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
if tt.verify != nil {
tt.verify(r)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id": "broadcast-id"}`))
},
),
)
opts := append([]Option{WithService(svc)}, tt.opts...)
b := NewLiveBroadcast(opts...)
var buf bytes.Buffer
if err := b.Transition(&buf); (err != nil) != tt.wantErr {
t.Errorf(
"LiveBroadcast.Transition() error = %v, wantErr %v", err, tt.wantErr,
)
}
},
)
}
}
func TestLiveBroadcast_InsertCuepoint(t *testing.T) {
tests := []struct {
name string
opts []Option
verify func(*http.Request)
wantErr bool
}{
{
name: "insert cuepoint",
opts: []Option{
WithIds([]string{"broadcast-id"}),
WithCueType("cueTypeAd"),
WithCueDurationSecs(30),
WithCueInsertionOffsetMs(5000),
},
verify: func(r *http.Request) {
if r.Method != "POST" {
t.Errorf("expected POST, got %s", r.Method)
}
q := r.URL.Query()
if q.Get("id") != "broadcast-id" {
t.Errorf("expected id=broadcast-id, got %s", q.Get("id"))
}
defer func() { _ = r.Body.Close() }()
var body struct {
CueType string `json:"cueType"`
DurationSecs int64 `json:"durationSecs"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("failed to decode request body: %v", err)
}
if body.CueType != "cueTypeAd" {
t.Errorf("expected cueType=cueTypeAd, got %s", body.CueType)
}
if body.DurationSecs != 30 {
t.Errorf("expected durationSecs=30, got %d", body.DurationSecs)
}
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(
tt.name, func(t *testing.T) {
svc := common.NewTestService(
t, http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
if tt.verify != nil {
tt.verify(r)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id": "cuepoint-id"}`))
},
),
)
opts := append([]Option{WithService(svc)}, tt.opts...)
b := NewLiveBroadcast(opts...)
var buf bytes.Buffer
if err := b.InsertCuepoint(&buf); (err != nil) != tt.wantErr {
t.Errorf(
"LiveBroadcast.InsertCuepoint() error = %v, wantErr %v", err,
tt.wantErr,
)
}
},
)
}
}