diff --git a/internal/mock_api/endpoints/endpoints.go b/internal/mock_api/endpoints/endpoints.go index 9b479987..882e4c7a 100644 --- a/internal/mock_api/endpoints/endpoints.go +++ b/internal/mock_api/endpoints/endpoints.go @@ -74,12 +74,10 @@ func All() []mock_api.MockEndpoint { schedule.ScheduleSettings{}, search.SearchCategories{}, search.SearchChannels{}, - streams.AllTags{}, streams.FollowedStreams{}, streams.Markers{}, streams.StreamKey{}, streams.Streams{}, - streams.StreamTags{}, subscriptions.BroadcasterSubscriptions{}, subscriptions.UserSubscriptions{}, teams.ChannelTeams{}, @@ -91,3 +89,17 @@ func All() []mock_api.MockEndpoint { whispers.Whispers{}, } } + +// All these endpoints return 410 Gone +func Gone() map[string][]string { + return map[string][]string{ + "/tags/streams": { + "GET", + }, + "/streams/tags": { + "GET", + "POST", + "PUT", + }, + } +} diff --git a/internal/mock_api/endpoints/streams/all_tags.go b/internal/mock_api/endpoints/streams/all_tags.go deleted file mode 100644 index bbd962de..00000000 --- a/internal/mock_api/endpoints/streams/all_tags.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -package streams - -import ( - "encoding/json" - "net/http" - - "github.com/twitchdev/twitch-cli/internal/database" - "github.com/twitchdev/twitch-cli/internal/mock_api/mock_errors" - "github.com/twitchdev/twitch-cli/internal/models" -) - -var allTagsMethodsSupported = map[string]bool{ - http.MethodGet: true, - http.MethodPost: false, - http.MethodDelete: false, - http.MethodPatch: false, - http.MethodPut: false, -} - -var allTagsScopesByMethod = map[string][]string{ - http.MethodGet: {}, - http.MethodPost: {}, - http.MethodDelete: {}, - http.MethodPatch: {}, - http.MethodPut: {}, -} - -type AllTags struct{} - -func (e AllTags) Path() string { return "/tags/streams" } - -func (e AllTags) GetRequiredScopes(method string) []string { - return allTagsScopesByMethod[method] -} - -func (e AllTags) ValidMethod(method string) bool { - return allTagsMethodsSupported[method] -} - -func (e AllTags) ServeHTTP(w http.ResponseWriter, r *http.Request) { - db = r.Context().Value("db").(database.CLIDatabase) - - switch r.Method { - case http.MethodGet: - getAllTags(w, r) - break - default: - w.WriteHeader(http.StatusMethodNotAllowed) - } -} - -func getAllTags(w http.ResponseWriter, r *http.Request) { - tagIDs := r.URL.Query()["tag_id"] - dbResponse := database.DBResponse{} - tags := []database.Tag{} - - if len(tagIDs) > 100 { - mock_errors.WriteBadRequest(w, "only 100 tag_ids can be provided at a time") - return - } - - if len(tagIDs) > 0 { - for _, id := range tagIDs { - t := database.Tag{ID: id} - dbr, err := db.NewQuery(r, 100).GetTags(t) - if err != nil { - mock_errors.WriteServerError(w, "error fetching tags") - return - } - - tagResponse := dbr.Data.([]database.Tag) - tags = append(tags, tagResponse...) - } - } else { - t := database.Tag{} - dbr, err := db.NewQuery(r, 100).GetTags(t) - if err != nil { - mock_errors.WriteServerError(w, "error fetching tags") - return - } - dbResponse = *dbr - tagResponse := dbr.Data.([]database.Tag) - tags = append(tags, tagResponse...) - } - - apiResponse := models.APIResponse{ - Data: convertTags(tags), - } - - if len(tagIDs) == 0 && len(tags) == dbResponse.Limit { - apiResponse.Pagination = &models.APIPagination{Cursor: dbResponse.Cursor} - } - - bytes, _ := json.Marshal(apiResponse) - w.Write(bytes) -} diff --git a/internal/mock_api/endpoints/streams/stream_tags.go b/internal/mock_api/endpoints/streams/stream_tags.go deleted file mode 100644 index d677937e..00000000 --- a/internal/mock_api/endpoints/streams/stream_tags.go +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -package streams - -import ( - "encoding/json" - "log" - "net/http" - - "github.com/mattn/go-sqlite3" - "github.com/twitchdev/twitch-cli/internal/database" - "github.com/twitchdev/twitch-cli/internal/mock_api/authentication" - "github.com/twitchdev/twitch-cli/internal/mock_api/mock_errors" - "github.com/twitchdev/twitch-cli/internal/models" -) - -var streamTagsMethodsSupported = map[string]bool{ - http.MethodGet: true, - http.MethodPost: false, - http.MethodDelete: false, - http.MethodPatch: false, - http.MethodPut: true, -} - -var streamTagsScopesByMethod = map[string][]string{ - http.MethodGet: {}, - http.MethodPost: {}, - http.MethodDelete: {}, - http.MethodPatch: {}, - http.MethodPut: {"channel:manage:broadcast"}, -} - -type StreamTags struct{} - -type PutBodyStreamTags struct { - TagIDs []string `json:"tag_ids"` -} - -func (e StreamTags) Path() string { return "/streams/tags" } - -func (e StreamTags) GetRequiredScopes(method string) []string { - return streamTagsScopesByMethod[method] -} - -func (e StreamTags) ValidMethod(method string) bool { - return streamTagsMethodsSupported[method] -} - -func (e StreamTags) ServeHTTP(w http.ResponseWriter, r *http.Request) { - db = r.Context().Value("db").(database.CLIDatabase) - - switch r.Method { - case http.MethodGet: - getStreamTags(w, r) - break - case http.MethodPut: - putStreamTags(w, r) - break - default: - w.WriteHeader(http.StatusMethodNotAllowed) - } -} - -func getStreamTags(w http.ResponseWriter, r *http.Request) { - userCtx := r.Context().Value("auth").(authentication.UserAuthentication) - - if !userCtx.MatchesBroadcasterIDParam(r) { - mock_errors.WriteUnauthorized(w, "broadcaster_id does not match token") - return - } - - dbr, err := db.NewQuery(r, 100).GetStreamTags(userCtx.UserID) - if err != nil { - log.Print(err) - mock_errors.WriteServerError(w, "error fetching tags") - return - } - tagResponse := dbr.Data.([]database.Tag) - - apiResponse := models.APIResponse{ - Data: convertTags(tagResponse), - } - - if len(tagResponse) == dbr.Limit { - apiResponse.Pagination = &models.APIPagination{Cursor: dbr.Cursor} - } - - bytes, _ := json.Marshal(apiResponse) - w.Write(bytes) -} - -func putStreamTags(w http.ResponseWriter, r *http.Request) { - userCtx := r.Context().Value("auth").(authentication.UserAuthentication) - - if !userCtx.MatchesBroadcasterIDParam(r) { - mock_errors.WriteUnauthorized(w, "broadcaster_id does not match token") - return - } - - body := PutBodyStreamTags{} - - err := json.NewDecoder(r.Body).Decode(&body) - if err != nil { - mock_errors.WriteBadRequest(w, "error parsing body") - return - } - - err = db.NewQuery(r, 100).DeleteAllStreamTags(userCtx.UserID) - if err != nil { - log.Print(err) - mock_errors.WriteServerError(w, err.Error()) - return - } - for _, tag := range body.TagIDs { - err = db.NewQuery(r, 100).InsertStreamTag(database.StreamTag{UserID: userCtx.UserID, TagID: tag}) - if err != nil { - if database.DatabaseErrorIs(err, sqlite3.ErrConstraintForeignKey) { - mock_errors.WriteBadRequest(w, "invalid tag provided") - return - } - mock_errors.WriteServerError(w, err.Error()) - return - } - } - - w.WriteHeader(http.StatusNoContent) -} diff --git a/internal/mock_api/endpoints/streams/streams_test.go b/internal/mock_api/endpoints/streams/streams_test.go index 602465c0..a9ce8cf5 100644 --- a/internal/mock_api/endpoints/streams/streams_test.go +++ b/internal/mock_api/endpoints/streams/streams_test.go @@ -12,25 +12,6 @@ import ( "github.com/twitchdev/twitch-cli/test_setup/test_server" ) -func TestAllTags(t *testing.T) { - a := test_setup.SetupTestEnv(t) - ts := test_server.SetupTestServer(AllTags{}) - - // get - req, _ := http.NewRequest(http.MethodGet, ts.URL+AllTags{}.Path(), nil) - q := req.URL.Query() - req.URL.RawQuery = q.Encode() - resp, err := http.DefaultClient.Do(req) - a.Nil(err) - a.Equal(200, resp.StatusCode) - - q.Set("tag_id", "1234") - req.URL.RawQuery = q.Encode() - resp, err = http.DefaultClient.Do(req) - a.Nil(err) - a.Equal(200, resp.StatusCode) -} - func TestFollowedStreams(t *testing.T) { a := test_setup.SetupTestEnv(t) ts := test_server.SetupTestServer(FollowedStreams{}) @@ -99,45 +80,6 @@ func TestMarkers(t *testing.T) { a.Equal(400, resp.StatusCode) } -func TestStreamTags(t *testing.T) { - a := test_setup.SetupTestEnv(t) - ts := test_server.SetupTestServer(StreamTags{}) - - // get - req, _ := http.NewRequest(http.MethodGet, ts.URL+StreamTags{}.Path(), nil) - q := req.URL.Query() - req.URL.RawQuery = q.Encode() - resp, err := http.DefaultClient.Do(req) - a.Nil(err) - a.Equal(401, resp.StatusCode) - - q.Set("broadcaster_id", "1") - req.URL.RawQuery = q.Encode() - resp, err = http.DefaultClient.Do(req) - a.Nil(err) - a.Equal(200, resp.StatusCode) - - // put - put := PutBodyStreamTags{ - TagIDs: []string{"1234"}, - } - b, _ := json.Marshal(put) - req, _ = http.NewRequest(http.MethodPut, ts.URL+StreamTags{}.Path(), bytes.NewBuffer(b)) - q.Del("broadcaster_id") - req.URL.RawQuery = q.Encode() - resp, err = http.DefaultClient.Do(req) - a.Nil(err) - a.Equal(401, resp.StatusCode) - - b, _ = json.Marshal(put) - req, _ = http.NewRequest(http.MethodPut, ts.URL+StreamTags{}.Path(), bytes.NewBuffer(b)) - q.Set("broadcaster_id", "1") - req.URL.RawQuery = q.Encode() - resp, err = http.DefaultClient.Do(req) - a.Nil(err) - a.Equal(400, resp.StatusCode) -} - func TestStreamKey(t *testing.T) { a := test_setup.SetupTestEnv(t) ts := test_server.SetupTestServer(StreamKey{}) diff --git a/internal/mock_api/mock_server/server.go b/internal/mock_api/mock_server/server.go index b2525049..e26573bd 100644 --- a/internal/mock_api/mock_server/server.go +++ b/internal/mock_api/mock_server/server.go @@ -4,12 +4,14 @@ package mock_server import ( "context" + "encoding/json" "fmt" "log" "net" "net/http" "os" "os/signal" + "strings" "syscall" "time" @@ -19,6 +21,7 @@ import ( "github.com/twitchdev/twitch-cli/internal/mock_api/generate" "github.com/twitchdev/twitch-cli/internal/mock_auth" "github.com/twitchdev/twitch-cli/internal/mock_units" + "github.com/twitchdev/twitch-cli/internal/models" ) const MOCK_NAMESPACE = "/mock" @@ -105,6 +108,11 @@ func RegisterHandlers(m *http.ServeMux) { for _, e := range mock_auth.All() { m.Handle(AUTH_NAMESPACE+e.Path(), loggerMiddleware(e)) } + + // For removed endpoints we don't have to worry about an actual handler, since its just gonna return 410 Gone + for e := range endpoints.Gone() { + m.Handle(MOCK_NAMESPACE+e, loggerMiddleware(nil)) + } } func loggerMiddleware(next http.Handler) http.Handler { @@ -120,6 +128,39 @@ func loggerMiddleware(next http.Handler) http.Handler { return } + // Check for removed endpoints, which will return 410 Gone + for goneEndpoint, methods := range endpoints.Gone() { + if r.URL.Path == MOCK_NAMESPACE+goneEndpoint { + validRemovedEndpoint := false + for _, m := range methods { + if strings.EqualFold(m, r.Method) { + validRemovedEndpoint = true + } + } + + // In production, removed API URLs with no previously existing method return 404 + // e.g., "GET helix/tags/streams" returns 410, but "DELETE helix/tags/streams" returns 404 + if !validRemovedEndpoint { + bytes, _ := json.Marshal(models.APIResponse{ + Error: "Not Found", + Status: 404, + Message: "", + }) + w.WriteHeader(http.StatusNotFound) + w.Write(bytes) + } else { + bytes, _ := json.Marshal(models.APIResponse{ + Error: "Gone", + Status: 410, + Message: "The API is deprecated.", + }) + w.WriteHeader(410) + w.Write(bytes) + } + return + } + } + next.ServeHTTP(w, r) }) }