From aa808f74befc530e23d9970bcae78baae02a87b3 Mon Sep 17 00:00:00 2001 From: Wessie Date: Sun, 25 Feb 2024 01:25:45 +0000 Subject: [PATCH] website/api/v1: try and test the SSE handler These tests are pretty low quality right now but it atleast tests the very basic functionality of the code. --- generate.go | 2 +- go.mod | 1 + go.sum | 2 + streamer/streamer_test.go | 62 --------------- website/admin/pending.go | 4 +- website/api/php/api.go | 7 +- website/api/v1/sse.go | 10 ++- website/api/v1/sse_test.go | 152 +++++++++++++++++++++++++++++++++++++ 8 files changed, 166 insertions(+), 74 deletions(-) delete mode 100644 streamer/streamer_test.go create mode 100644 website/api/v1/sse_test.go diff --git a/generate.go b/generate.go index d2ac6b79..c8edbc04 100644 --- a/generate.go +++ b/generate.go @@ -2,5 +2,5 @@ package radio //go:generate go generate ./rpc/generate.go //go:generate moq -out mocks/radio.gen.go -pkg mocks . StorageService StorageTx TrackStorage SubmissionStorage UserStorage -//go:generate moq -out mocks/templates.gen.go -pkg mocks ./templates/ Executor +//go:generate moq -out mocks/templates.gen.go -pkg mocks ./templates/ Executor TemplateSelectable //go:generate moq -out mocks/util.gen.go -pkg mocks ./mocks/ FS File FileInfo diff --git a/go.mod b/go.mod index 268ece3e..193cf382 100644 --- a/go.mod +++ b/go.mod @@ -43,6 +43,7 @@ require ( github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Microsoft/hcsshim v0.11.4 // indirect + github.com/alevinval/sse v1.0.2 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/containerd/containerd v1.7.11 // indirect github.com/containerd/log v0.1.0 // indirect diff --git a/go.sum b/go.sum index c6b19617..a9fcc5b2 100644 --- a/go.sum +++ b/go.sum @@ -19,6 +19,8 @@ github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7 github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= github.com/XSAM/otelsql v0.27.0 h1:i9xtxtdcqXV768a5C6SoT/RkG+ue3JTOgkYInzlTOqs= github.com/XSAM/otelsql v0.27.0/go.mod h1:0mFB3TvLa7NCuhm/2nU7/b2wEtsczkj8Rey8ygO7V+A= +github.com/alevinval/sse v1.0.2 h1:ooc08hn9B5X/u7vOMpnYDkXxIKA0y5DOw9qBVVK3YKY= +github.com/alevinval/sse v1.0.2/go.mod h1:X4J1/nTNs4yKbvjXFWJB+NdF9gaYkoAC4sw9Z9h7ASk= github.com/alexedwards/scs/v2 v2.7.0 h1:DY4rqLCM7UIR9iwxFS0++z1NhTzQlKV30aMHkJCDWKw= github.com/alexedwards/scs/v2 v2.7.0/go.mod h1:ToaROZxyKukJKT/xLcVQAChi5k6+Pn1Gvmdl7h3RRj8= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= diff --git a/streamer/streamer_test.go b/streamer/streamer_test.go deleted file mode 100644 index e96fd9b9..00000000 --- a/streamer/streamer_test.go +++ /dev/null @@ -1,62 +0,0 @@ -package streamer - -import ( - "bytes" - "io" - "net/http" - "net/http/httptest" - "testing" -) - -func TestNewIcecastConn(t *testing.T) { - e := make(chan string, 1) - var statusCode int - var testData []byte - - s := httptest.NewServer(http.HandlerFunc( - func(rw http.ResponseWriter, r *http.Request) { - if r.Method != "SOURCE" { - e <- "method is not SOURCE" - return - } - - if r.Proto != "HTTP/1.0" { - e <- "using wrong HTTP version" - return - } - - auth := r.Header.Get("Authorization") - if auth == "" { - e <- "no authorization send" - return - } - - // TODO: check auth values - - if r.Header.Get("Content-Type") == "" { - e <- "no content-type set" - return - } - - rw.WriteHeader(statusCode) - - data := make([]byte, len(testData)) - n, err := io.ReadFull(r.Body, data) - if err != nil { - e <- err.Error() - return - } - if n != len(data) { - e <- "n not equal to data length" - return - } - - if !bytes.Equal(data, testData) { - e <- "data not equal to testData" - return - } - }, - )) - - _ = s -} diff --git a/website/admin/pending.go b/website/admin/pending.go index bd5d6610..91449005 100644 --- a/website/admin/pending.go +++ b/website/admin/pending.go @@ -8,6 +8,7 @@ import ( "path/filepath" "slices" "strconv" + "time" radio "github.com/R-a-dio/valkyrie" "github.com/R-a-dio/valkyrie/errors" @@ -427,8 +428,7 @@ func (pf *PendingForm) Update(form url.Values) { pf.ReplacementID = radio.TrackID(id) } pf.Reason = form.Get("reason") - // TODO: move reviewedat into db layer - // pf.ReviewedAt = time.Now() + pf.ReviewedAt = time.Now() pf.GoodUpload = form.Get("good") != "" } diff --git a/website/api/php/api.go b/website/api/php/api.go index bbf19c45..4f186dbc 100644 --- a/website/api/php/api.go +++ b/website/api/php/api.go @@ -177,9 +177,7 @@ func (a *API) getSearch(w http.ResponseWriter, r *http.Request) { rawLimit := values.Get("limit") parsedLimit, err := strconv.Atoi(rawLimit) if err == nil && parsedLimit < 20 { - // TODO: check if we just want to throw a fit if NaN - // only use the value if it's a number and it's - // not above the allowed limit + // if used defined limit isn't a number just return the standard 20 limit = parsedLimit } } @@ -188,8 +186,7 @@ func (a *API) getSearch(w http.ResponseWriter, r *http.Request) { rawPage := values.Get("page") parsedPage, err := strconv.Atoi(rawPage) if err == nil { - // TODO: check if we just want to throw a fit if NaN - // only use the value if it's a valid number + // if it's not a number just return the first page page = parsedPage } } diff --git a/website/api/v1/sse.go b/website/api/v1/sse.go index d6380fa8..1183c885 100644 --- a/website/api/v1/sse.go +++ b/website/api/v1/sse.go @@ -67,8 +67,8 @@ func (a *API) runStatusUpdates(ctx context.Context) error { continue } - // we send to the now playing sse stream and separately to - // a streamer sse stream + // only send events if the relevant data to said event has changed + // since our previous status if !status.Song.EqualTo(previous.Song) { log.Debug().Str("event", EventMetadata).Any("value", status).Msg("sending") a.sse.SendNowPlaying(status) @@ -79,6 +79,8 @@ func (a *API) runStatusUpdates(ctx context.Context) error { if status.User.ID != previous.User.ID { log.Debug().Str("event", EventStreamer).Any("value", status.User).Msg("sending") a.sse.SendStreamer(status.User) + // TODO(wessie): queue is technically only used for the automated streamer + // and should probably have an extra event trigger here to make it disappear } previous = status @@ -116,7 +118,7 @@ const ( ) const ( - EventPing = "ping" + EventTime = "time" EventMetadata = "metadata" EventStreamer = "streamer" EventQueue = "queue" @@ -171,7 +173,7 @@ func (s *Stream) ServeHTTP(w http.ResponseWriter, r *http.Request) { // send a sync timestamp now := strconv.FormatInt(time.Now().UnixMilli(), 10) - _, _ = w.Write(sse.Event{Name: "time", Data: []byte(now)}.Encode()) + _, _ = w.Write(sse.Event{Name: string(EventTime), Data: []byte(now)}.Encode()) // send events that have already happened, one for each event so that // we're certain the page is current diff --git a/website/api/v1/sse_test.go b/website/api/v1/sse_test.go new file mode 100644 index 00000000..08bc2f88 --- /dev/null +++ b/website/api/v1/sse_test.go @@ -0,0 +1,152 @@ +package v1 + +import ( + "context" + "encoding/json" + "net" + "net/http/httptest" + "reflect" + "sync" + "testing" + "time" + + radio "github.com/R-a-dio/valkyrie" + "github.com/R-a-dio/valkyrie/mocks" + "github.com/R-a-dio/valkyrie/templates" + "github.com/leanovate/gopter" + "github.com/leanovate/gopter/arbitrary" + "github.com/leanovate/gopter/gen" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/alevinval/sse/pkg/eventsource" +) + +func TestStream(t *testing.T) { + exec := &mocks.ExecutorMock{ + ExecuteAllFunc: func(input templates.TemplateSelectable) (map[string][]byte, error) { + jsonData, err := json.Marshal(input) + if err != nil { + return nil, err + } + + return map[string][]byte{ + "json": jsonData, + }, nil + }, + } + + var muExpected sync.Mutex + var expected radio.Status + + stream := NewStream(exec) + server := httptest.NewUnstartedServer(stream) + server.Config.ConnContext = func(ctx context.Context, c net.Conn) context.Context { + return templates.SetTheme(ctx, "json") + } + server.Start() + + t.Log(server.URL) + es1, err := eventsource.New(server.URL) + require.NoError(t, err) + + sync := make(chan struct{}) + go func() { + a := arbitrary.DefaultArbitraries() + timeGen := func(gp *gopter.GenParameters) *gopter.GenResult { + v := time.Date(2000, 10, 9, 8, 7, 6, 5, time.UTC) + res := gen.Time()(gp) + res.Result = v + return res + } + a.RegisterGen(timeGen) + a.RegisterGen(gen.PtrOf(timeGen)) + + genStatus := a.GenForType(reflect.TypeOf(radio.Status{})) + param := gopter.DefaultGenParameters() + + for i := 0; i < 100; i++ { + res := genStatus(param) + <-sync + sendStatus := res.Result.(radio.Status) + muExpected.Lock() + expected = sendStatus + muExpected.Unlock() + stream.SendNowPlaying(sendStatus) + } + }() + + ch1 := es1.MessageEvents() + + jsonEvent := <-ch1 + + assert.Equal(t, EventTime, jsonEvent.Name) + + for i := 0; i < 100; i++ { + sync <- struct{}{} + jsonEvent = <-ch1 + assert.Equal(t, EventMetadata, jsonEvent.Name) + + var status radio.Status + + // check the json version + err = json.Unmarshal([]byte(jsonEvent.Data), &status) + assert.NoError(t, err) + muExpected.Lock() + assert.Equal(t, expected, status) + muExpected.Unlock() + } +} + +func TestStreamSendInputs(t *testing.T) { + var name string + + exec := &mocks.ExecutorMock{ + ExecuteAllFunc: func(input templates.TemplateSelectable) (map[string][]byte, error) { + name = input.TemplateName() + return nil, nil + }, + } + + stream := NewStream(exec) + defer stream.Shutdown() + + t.Run("SendStreamer", func(t *testing.T) { + stream.SendStreamer(radio.User{}) + assert.Equal(t, "streamer", name) + }) + + t.Run("SendNowPlaying", func(t *testing.T) { + stream.SendNowPlaying(radio.Status{}) + assert.Equal(t, "nowplaying", name) + }) + + t.Run("SendQueue", func(t *testing.T) { + stream.SendQueue([]radio.QueueEntry{}) + assert.Equal(t, "queue", name) + }) + + t.Run("SendLastPlayed", func(t *testing.T) { + stream.SendLastPlayed([]radio.Song{}) + assert.Equal(t, "lastplayed", name) + }) +} + +func TestStreamSlowSub(t *testing.T) { + exec := &mocks.ExecutorMock{ + ExecuteAllFunc: func(input templates.TemplateSelectable) (map[string][]byte, error) { + return nil, nil + }, + } + + stream := NewStream(exec) + + ctx := templates.SetTheme(context.Background(), "default") + req := httptest.NewRequest("GET", "/", nil) + req = req.WithContext(ctx) + w := httptest.NewRecorder() + + go func() { + stream.ServeHTTP(w, req) + }() +}