diff --git a/.github/workflows/go_test.yml b/.github/workflows/go_test.yml index 1ea9a1d..9254497 100644 --- a/.github/workflows/go_test.yml +++ b/.github/workflows/go_test.yml @@ -10,13 +10,14 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v5 - name: Go setup - uses: actions/setup-go@v2 + uses: actions/setup-go@v6 + with: + go-version-file: "go.mod" + + - run: go version - - name: Add module - run: go mod init github.com/coaxial/tizinger - - name: Run tests run: make ci diff --git a/Makefile b/Makefile index 0634fb6..15b05f5 100644 --- a/Makefile +++ b/Makefile @@ -5,13 +5,14 @@ testwatch: watch -n 5 make test ci: - go get -t -d -v ./... && go test -race -coverprofile=coverage.out ./... && go tool cover -func=coverage.out + go test -race -coverprofile=coverage.out ./... && go tool cover -func=coverage.out lint: - golint ./... + go run github.com/golangci/golangci-lint/cmd/golangci-lint@latest run ./... gettools: - go get -u golang.org/x/lint/golint + go install golang.org/x/lint/golint@latest + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest testcovhtml: go test -coverprofile=coverage.out ./... && go tool cover -html=coverage.out diff --git a/fip/client.go b/fip/client.go index c2dfe27..498905f 100644 --- a/fip/client.go +++ b/fip/client.go @@ -6,7 +6,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "net/http" "strconv" "time" @@ -214,12 +214,10 @@ func makeRequest(req *http.Request, client *http.Client) (*http.Response, error) return nil, err } - logger.Info.Print( - fmt.Sprintf( - "received response %q, %d bytes", - response.Header.Get("content-type"), - response.ContentLength, - ), + logger.Info.Printf( + "received response %q, %d bytes", + response.Header.Get("content-type"), + response.ContentLength, ) return response, nil @@ -227,7 +225,7 @@ func makeRequest(req *http.Request, client *http.Client) (*http.Response, error) // unmarshalResponse parses the API response and unmarshals it to JSON. func unmarshalResponse(response *http.Response) (history historyResponse, err error) { - responseData, err := ioutil.ReadAll(response.Body) + responseData, err := io.ReadAll(response.Body) defer response.Body.Close() if err != nil { errMsg := fmt.Sprintf("error while reading response data: %v", err) @@ -242,7 +240,7 @@ func unmarshalResponse(response *http.Response) (history historyResponse, err er response.StatusCode, string(responseData), ) - logger.Error.Printf(errMsg) + logger.Error.Print(errMsg) return history, errors.New(errMsg) } @@ -281,6 +279,10 @@ func extractEndCursor(JSON *historyResponse) (timestamp int64, err error) { ec := JSON.Data.TimelineCursor.PageInfo.EndCursor logger.Trace.Printf("converting %q to int64 timestamp", ec) endCursorByte, err := base64.StdEncoding.DecodeString(ec) + if err != nil { + logger.Error.Printf("error decoding endCursor %q: %v", ec, err) + return timestamp, err + } timestamp, err = strconv.ParseInt(string(endCursorByte), 0, 64) if err != nil { logger.Error.Printf("error decoding endCursor %q to timestamp: %v", ec, err) diff --git a/fip/client_test.go b/fip/client_test.go index 4ea9f2c..0a070f9 100644 --- a/fip/client_test.go +++ b/fip/client_test.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "net/http" + "strconv" "testing" "time" @@ -20,8 +21,8 @@ func TestPlaylistErr(t *testing.T) { resp.WriteHeader(http.StatusBadRequest) resp.Header().Set("Content-Type", "application/html") length, badReqResp := mocks.LoadFixture("../fixtures/fip/bad_req.json") - resp.Header().Set("Content-Length", string(length)) - resp.Write(badReqResp) + resp.Header().Set("Content-Length", strconv.Itoa(length)) + _, _ = resp.Write(badReqResp) } server := mocks.Server(http.HandlerFunc(handler)) defer server.Close() @@ -39,8 +40,8 @@ func TestPlaylist(t *testing.T) { resp.WriteHeader(http.StatusOK) resp.Header().Set("Content-Type", "application/json; charset=utf-8") length, historyJSON := mocks.LoadFixture("../fixtures/fip/history_response.json") - resp.Header().Set("Content-Length", string(length)) - resp.Write(historyJSON) + resp.Header().Set("Content-Length", strconv.Itoa(length)) + _, _ = resp.Write(historyJSON) } server := mocks.Server(http.HandlerFunc(handler)) defer server.Close() @@ -71,8 +72,8 @@ func TestEmptyResponse(t *testing.T) { resp.WriteHeader(http.StatusOK) resp.Header().Set("Content-Type", "application/json; charset=utf-8") emptyResp := []byte("{}") - resp.Header().Set("Content-Length", string(len(emptyResp))) - resp.Write(emptyResp) + resp.Header().Set("Content-Length", strconv.Itoa(len(emptyResp))) + _, _ = resp.Write(emptyResp) } server := mocks.Server(http.HandlerFunc(handler)) defer server.Close() @@ -86,16 +87,28 @@ func TestEmptyResponse(t *testing.T) { } func ExampleAPIClient_Playlist() { + handler := func(resp http.ResponseWriter, req *http.Request) { + resp.WriteHeader(http.StatusOK) + resp.Header().Set("Content-Type", "application/json; charset=utf-8") + length, historyJSON := mocks.LoadFixture("../fixtures/fip/history_response.json") + resp.Header().Set("Content-Length", strconv.Itoa(length)) + _, _ = resp.Write(historyJSON) + } + server := mocks.Server(http.HandlerFunc(handler)) + defer server.Close() + SetEndpointURL(server.URL) + defer ResetEndpointURL() + var fipClient APIClient - // Get the list of 10 tracks played on FIP since 2020-07-25 00:30:00 GMT - tracks, err := fipClient.Playlist(1564014600, 10) + // Get the list of 10 tracks played on FIP since 2019-07-25 00:30:00 GMT (date + // doesn't matter as the fixture will return the same data for any timestamp) + tracks, err := fipClient.Playlist(1562284800, 10) if err != nil { log.Fatalf("Could not fetch FIP tracks: %v", err) } fmt.Println(tracks) - // Output: [{Riding the sun Howls Howls} {In the wake of adversity Dead Can Dance Within the realm of a dying sun} {Madame rêve Alain Bashung Osez Josephine} {The Planets op 32 : 3. Mercury, the Winged Messenger Orchestre Symphonique De Chicago Gustav Holst : Les Planètes} {Annie : The hard-knock life Alicia Morton BOF TV / Annie} {Bruce Lee Catastrophe Bruce Lee} {New comer 1 Walt Rockman Dusty fingers} {Cars Gary Numan The pleasure principle / Warriors} {Radio #1 Air 10000 hz legend} {Previsão do tempo Marcos Valle Previsao do tempo}] - + // Output: [{Scar tissue Red Hot Chili Peppers Greatest hits} {Off the wall Jil Is Lucky Off the wall} {Kalimba (Flute mix) Freakniks Electro tunes} {Tsukikaage no rendezvous Keiko Mari Nippon girls: Japanese pop, beat & bossa nova 1966-1970} {Un petit poisson, un petit oiseau Juliette Greco Déshabillez-moi 1965-1969} {I want to be happy Ray Brown Brown Ray trio / Some of my best friends are guitarists} {I'm so happy I can't stop crying Sting Mercury falling} {Sambarilove (feat. Roubinho Jacobina) Chiara Civello Eclipse} {Retiens l'été Double Francoise Les bijoux} {Serenade nº13 en Sol Maj K 525 ""une petite musique de nuit"" : I. Allegro I Musici Mozart, pachelbel, albinoni}] } func TestEndCursorConvert(t *testing.T) { @@ -124,8 +137,8 @@ func TestPlaylist200(t *testing.T) { length, historyJSON := mocks.LoadFixture(fixture) resp.WriteHeader(http.StatusOK) resp.Header().Set("Content-Type", "application/json; charset=utf-8") - resp.Header().Set("Content-Length", string(length)) - resp.Write(historyJSON) + resp.Header().Set("Content-Length", strconv.Itoa(length)) + _, _ = resp.Write(historyJSON) } server := mocks.Server(http.HandlerFunc(handler)) defer server.Close() diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7ae24cf --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/coaxial/tizinger + +go 1.25.3 + +require ( + github.com/bclicn/color v0.0.0-20180711051946-108f2023dc84 + github.com/gorilla/mux v1.8.1 + github.com/stretchr/testify v1.11.1 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..e1d67c0 --- /dev/null +++ b/go.sum @@ -0,0 +1,14 @@ +github.com/bclicn/color v0.0.0-20180711051946-108f2023dc84 h1:cutFptzj+ospnc1PETUqcSVTH3VQ44Bi0rpt3nE9gvo= +github.com/bclicn/color v0.0.0-20180711051946-108f2023dc84/go.mod h1:Va9ap1qxjAWkIVaW1E9rH0aNgE8SDI5A4n8Ds8P0fAA= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tidal/client.go b/tidal/client.go index 7a118ab..963c8ef 100644 --- a/tidal/client.go +++ b/tidal/client.go @@ -4,7 +4,7 @@ package tidal import ( "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "net/url" "regexp" @@ -24,9 +24,6 @@ type APIClient struct{} // baseURL can be overridden while testing to avoid live calls. var baseURL = "https://api.tidalhifi.com/v1" -// jar is the cookie jar for the tidal client. -var jar http.CookieJar - // tidalClient is the client making requests to the Tidal API. It is defined // here so that this client instance is reused. var tidalClient = &http.Client{} @@ -170,12 +167,12 @@ func queryTidal( logger.Error.Printf("error %v", err) return err } - debugBody, _ := ioutil.ReadAll(body) + debugBody, _ := io.ReadAll(body) debugyBodyString := string(debugBody) qs := req.URL.RawQuery h := req.Header // Remove password from logs - re := regexp.MustCompile(`(?P.*"password":")(?P.*?)(?P".*)`) + re := regexp.MustCompile(`(?P.*"password":")(?P.*?)(?P.*)`) debugyBodyString = re.ReplaceAllString(debugyBodyString, `$1$3`) logger.Trace.Printf("request: body: %#v", string(debugyBodyString)) logger.Trace.Printf("qs: %q", string(qs)) @@ -203,7 +200,7 @@ func queryTidal( // length. This is up to the server and there isn't much that can be // done about it. logger.Info.Printf("received response %q, %d bytes", resp.Header.Get("Content-Type"), resp.ContentLength) - contents, err := ioutil.ReadAll(resp.Body) + contents, err := io.ReadAll(resp.Body) // The request succeeds only for HTTP 200 OK or HTTP 201 Created (for // playlist creation) if resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusCreated { @@ -252,7 +249,7 @@ func setToken() (err error) { return err } - tokens, err := ioutil.ReadAll(resp.Body) + tokens, err := io.ReadAll(resp.Body) resp.Body.Close() if err != nil { logger.Error.Printf("error reading response: %v", err) diff --git a/tidal/client_test.go b/tidal/client_test.go index 9ce61b5..d687d71 100644 --- a/tidal/client_test.go +++ b/tidal/client_test.go @@ -2,6 +2,7 @@ package tidal import ( "net/http" + "strconv" "strings" "testing" @@ -15,8 +16,8 @@ func TestFetchingTokens(t *testing.T) { length, tokensJSON := mocks.LoadFixture("../fixtures/tidal/tokens.json") resp.WriteHeader(http.StatusOK) resp.Header().Set("Content-Type", "application/json; charset=utf-8") - resp.Header().Set("Content-Length", string(length)) - resp.Write(tokensJSON) + resp.Header().Set("Content-Length", strconv.Itoa(length)) + _, _ = resp.Write(tokensJSON) } server := mocks.Server(http.HandlerFunc(handler)) defer server.Close() @@ -33,8 +34,8 @@ func TestLogin(t *testing.T) { length, JSON := mocks.LoadFixture("../fixtures/tidal/login_response.json") resp.WriteHeader(http.StatusOK) resp.Header().Set("Content-Type", "application/json;charset=UTF-8") - resp.Header().Set("Content-Length", string(length)) - resp.Write(JSON) + resp.Header().Set("Content-Length", strconv.Itoa(length)) + _, _ = resp.Write(JSON) } server := mocks.Server(http.HandlerFunc(handler)) defer server.Close() @@ -85,8 +86,8 @@ func TestCreateEmptyPlaylist(t *testing.T) { length, JSON := mocks.LoadFixture("../fixtures/tidal/playlist-create_response.json") resp.WriteHeader(http.StatusCreated) resp.Header().Set("Content-Type", "application/json;charset=UTF-8") - resp.Header().Set("Content-Length", string(length)) - resp.Write(JSON) + resp.Header().Set("Content-Length", strconv.Itoa(length)) + _, _ = resp.Write(JSON) } server := mocks.Server(http.HandlerFunc(handler)) defer server.Close() @@ -110,8 +111,8 @@ func TestSearch(t *testing.T) { length, JSON := mocks.LoadFixture("../fixtures/tidal/search-track_result_response.json") resp.WriteHeader(http.StatusOK) resp.Header().Set("Content-Type", "application/json;charset=UTF-8") - resp.Header().Set("Content-Length", string(length)) - resp.Write(JSON) + resp.Header().Set("Content-Length", strconv.Itoa(length)) + _, _ = resp.Write(JSON) } server := mocks.Server(http.HandlerFunc(handler)) defer server.Close() @@ -131,8 +132,8 @@ func TestSearchNoResult(t *testing.T) { length, JSON := mocks.LoadFixture("../fixtures/tidal/search-track_noresult_response.json") resp.WriteHeader(http.StatusOK) resp.Header().Set("Content-Type", "application/json;charset=UTF-8") - resp.Header().Set("Content-Length", string(length)) - resp.Write(JSON) + resp.Header().Set("Content-Length", strconv.Itoa(length)) + _, _ = resp.Write(JSON) } server := mocks.Server(http.HandlerFunc(handler)) defer server.Close() @@ -168,15 +169,15 @@ func TestPopulatePlaylist(t *testing.T) { length, JSON := mocks.LoadFixture("../fixtures/tidal/playlist-add_success_response.json") resp.WriteHeader(http.StatusOK) resp.Header().Set("Content-Type", "application/json;charset=UTF-8") - resp.Header().Set("Content-Length", string(length)) - resp.Write(JSON) + resp.Header().Set("Content-Length", strconv.Itoa(length)) + _, _ = resp.Write(JSON) } getLastUpdatedHandler := func(resp http.ResponseWriter, req *http.Request) { length, JSON := mocks.LoadFixture("../fixtures/tidal/playlist-get_response.json") resp.WriteHeader(http.StatusOK) resp.Header().Set("Content-Type", "application/json;charset=UTF-8") - resp.Header().Set("Content-Length", string(length)) - resp.Write(JSON) + resp.Header().Set("Content-Length", strconv.Itoa(length)) + _, _ = resp.Write(JSON) } r := mux.NewRouter() r.HandleFunc("/playlists/mockUUID/items", addTrackHandler) @@ -216,8 +217,8 @@ func TestGetLastUpdated(t *testing.T) { length, JSON := mocks.LoadFixture("../fixtures/tidal/playlist-get_response.json") resp.WriteHeader(http.StatusOK) resp.Header().Set("Content-Type", "application/json;charset=UTF-8") - resp.Header().Set("Content-Length", string(length)) - resp.Write(JSON) + resp.Header().Set("Content-Length", strconv.Itoa(length)) + _, _ = resp.Write(JSON) } server := mocks.Server(http.HandlerFunc(handler)) defer server.Close() diff --git a/utils/credentials/credentials.go b/utils/credentials/credentials.go index 48a3037..282e33d 100644 --- a/utils/credentials/credentials.go +++ b/utils/credentials/credentials.go @@ -1,8 +1,7 @@ -// Package credentials abstracts away access to the data contained withing the credentials.yml file. package credentials import ( - "io/ioutil" + "os" "sync" "github.com/coaxial/tizinger/utils/logger" @@ -34,7 +33,7 @@ var once sync.Once // loadConfig reads and unmarshalls the credentials file. func loadConfig() { logger.Trace.Printf("reading credentials from %q", credentialsFile) - content, err := ioutil.ReadFile(credentialsFile) + content, err := os.ReadFile(credentialsFile) if err != nil { logger.Error.Fatalf("could not read %q: %v", credentialsFile, err) return diff --git a/utils/mocks/server.go b/utils/mocks/server.go index 4731158..773bb4d 100644 --- a/utils/mocks/server.go +++ b/utils/mocks/server.go @@ -1,9 +1,9 @@ package mocks import ( - "io/ioutil" "net/http" "net/http/httptest" + "os" "github.com/coaxial/tizinger/utils/logger" ) @@ -18,7 +18,7 @@ func Server(handler http.Handler) *httptest.Server { // LoadFixture is a helper for loading canned responses to use in handler // functions. func LoadFixture(path string) (length int, content []byte) { - content, err := ioutil.ReadFile(path) + content, err := os.ReadFile(path) length = len(content) if err != nil { logger.Error.Fatalf("Could not load %q: %v", path, err) diff --git a/utils/mocks/server_test.go b/utils/mocks/server_test.go index 619fa62..c660f46 100644 --- a/utils/mocks/server_test.go +++ b/utils/mocks/server_test.go @@ -1,14 +1,17 @@ package mocks -import "net/http" +import ( + "net/http" + "strconv" +) func ExampleServer() { handler := func(resp http.ResponseWriter, req *http.Request) { length, historyJSON := LoadFixture("../fixtures/fip/history_response.json") resp.WriteHeader(http.StatusOK) resp.Header().Set("Content-Type", "application/json; charset=utf-8") - resp.Header().Set("Content-Length", string(length)) - resp.Write(historyJSON) + resp.Header().Set("Content-Length", strconv.Itoa(length)) + _, _ = resp.Write(historyJSON) } server := Server(http.HandlerFunc(handler)) defer server.Close()