From fcc6efc81dc2e6d118216dc321d36142b5e01f43 Mon Sep 17 00:00:00 2001 From: Joshua Raphael Date: Fri, 22 Nov 2024 14:03:36 -0700 Subject: [PATCH] add GetAchievementUnlocks endpoint --- README.md | 8 +- achievement.go | 34 ++++ achievement_test.go | 186 ++++++++++++++++++ .../getachievementunlocks.go | 28 +++ leaderboards_test.go | 2 - models/achievement.go | 54 +++++ 6 files changed, 309 insertions(+), 3 deletions(-) create mode 100644 achievement.go create mode 100644 achievement_test.go create mode 100644 examples/achievement/getachievementunlocks/getachievementunlocks.go create mode 100644 models/achievement.go diff --git a/README.md b/README.md index 0170862..3c4e658 100644 --- a/README.md +++ b/README.md @@ -88,4 +88,10 @@ For convenience, the API docs and examples can be found in the tables below |Function|Description|Links| |-|-|-| |`GetConsoleIDs()`|Gets the complete list of all system ID and name pairs on the site.|[docs](https://api-docs.retroachievements.org/v1/get-console-ids.html) \| [example](examples/system/getconsoleids/getconsoleids.go)| -|`GetGameList()`|Gets the complete list of games for a specified console on the site.|[docs](https://api-docs.retroachievements.org/v1/get-game-list.html) \| [example](examples/system/getgamelist/getgamelist.go)| \ No newline at end of file +|`GetGameList()`|Gets the complete list of games for a specified console on the site.|[docs](https://api-docs.retroachievements.org/v1/get-game-list.html) \| [example](examples/system/getgamelist/getgamelist.go)| + +

Achievement

+ +|Function|Description|Links| +|-|-|-| +|`GetAchievementUnlocks()`|Gets a list of users who have earned an achievement.|[docs](https://api-docs.retroachievements.org/v1/get-achievement-unlocks.html) \| [example](examples/achievement/getachievementunlocks/getachievementunlocks.go)| \ No newline at end of file diff --git a/achievement.go b/achievement.go new file mode 100644 index 0000000..ca6458a --- /dev/null +++ b/achievement.go @@ -0,0 +1,34 @@ +package retroachievements + +import ( + "fmt" + "net/http" + + raHttp "github.com/joshraphael/go-retroachievements/http" + "github.com/joshraphael/go-retroachievements/models" +) + +// GetAchievementUnlocks gets a list of users who have earned an achievement. +func (c *Client) GetAchievementUnlocks(params models.GetAchievementUnlocksParameters) (*models.GetAchievementUnlocks, error) { + details := []raHttp.RequestDetail{ + raHttp.Method(http.MethodGet), + raHttp.Path("/API/API_GetAchievementUnlocks.php"), + raHttp.APIToken(c.Secret), + raHttp.A(params.AchievementID), + } + if params.Count != nil { + details = append(details, raHttp.C(*params.Count)) + } + if params.Offset != nil { + details = append(details, raHttp.O(*params.Offset)) + } + r, err := c.do(details...) + if err != nil { + return nil, fmt.Errorf("calling endpoint: %w", err) + } + resp, err := raHttp.ResponseObject[models.GetAchievementUnlocks](r) + if err != nil { + return nil, fmt.Errorf("parsing response object: %w", err) + } + return resp, nil +} diff --git a/achievement_test.go b/achievement_test.go new file mode 100644 index 0000000..1f7adfc --- /dev/null +++ b/achievement_test.go @@ -0,0 +1,186 @@ +package retroachievements_test + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/joshraphael/go-retroachievements" + "github.com/joshraphael/go-retroachievements/models" + "github.com/stretchr/testify/require" +) + +func TestGetAchievementUnlocks(tt *testing.T) { + count := 10 + offset := 10 + achievementType := "progression" + dateCreated, err := time.Parse(time.DateTime, "2012-11-02 00:03:12") + require.NoError(tt, err) + dateModified, err := time.Parse(time.DateTime, "2023-09-30 02:00:49") + require.NoError(tt, err) + dateAwarded, err := time.Parse(time.RFC3339Nano, "2024-11-22T17:25:17.000000Z") + require.NoError(tt, err) + tests := []struct { + name string + params models.GetAchievementUnlocksParameters + modifyURL func(url string) string + responseCode int + responseMessage models.GetAchievementUnlocks + responseError models.ErrorResponse + response func(messageBytes []byte, errorBytes []byte) []byte + assert func(t *testing.T, resp *models.GetAchievementUnlocks, err error) + }{ + { + name: "fail to call endpoint", + params: models.GetAchievementUnlocksParameters{ + AchievementID: 14402, + Count: &count, + Offset: &offset, + }, + modifyURL: func(url string) string { + return "" + }, + responseCode: http.StatusOK, + response: func(messageBytes []byte, errorBytes []byte) []byte { + return messageBytes + }, + assert: func(t *testing.T, resp *models.GetAchievementUnlocks, err error) { + require.Nil(t, resp) + require.EqualError(t, err, "calling endpoint: Get \"/API/API_GetAchievementUnlocks.php?a=14402&c=10&o=10&y=some_secret\": unsupported protocol scheme \"\"") + }, + }, + { + name: "error response", + params: models.GetAchievementUnlocksParameters{ + AchievementID: 14402, + Count: &count, + Offset: &offset, + }, + modifyURL: func(url string) string { + return url + }, + responseCode: http.StatusUnauthorized, + responseError: models.ErrorResponse{ + Message: "test", + Errors: []models.ErrorDetail{ + { + Status: http.StatusUnauthorized, + Code: "unauthorized", + Title: "Not Authorized", + }, + }, + }, + response: func(messageBytes []byte, errorBytes []byte) []byte { + return errorBytes + }, + assert: func(t *testing.T, resp *models.GetAchievementUnlocks, err error) { + require.Nil(t, resp) + require.EqualError(t, err, "parsing response object: error responses: [401] Not Authorized") + }, + }, + { + name: "success", + params: models.GetAchievementUnlocksParameters{ + AchievementID: 14402, + Count: &count, + Offset: &offset, + }, + modifyURL: func(url string) string { + return url + }, + responseCode: http.StatusOK, + responseMessage: models.GetAchievementUnlocks{ + UnlocksCount: 15906, + UnlocksHardcoreCount: 8223, + TotalPlayers: 38104, + Achievement: models.GetAchievementUnlocksAchievement{ + ID: 1, + Title: "Ring Collector", + Description: "Collect 100 rings", + Points: 5, + TrueRatio: 7, + Author: "Scott", + DateCreated: models.DateTime{ + Time: dateCreated, + }, + DateModified: models.DateTime{ + Time: dateModified, + }, + Type: &achievementType, + }, + Console: models.GetAchievementUnlocksConsole{ + ID: 1, + Title: "Genesis/Mega Drive", + }, + Game: models.GetAchievementUnlocksGame{ + ID: 1, + Title: "Sonic the Hedgehog", + }, + Unlocks: []models.GetAchievementUnlocksUnlock{ + { + User: "redjedia", + RAPoints: 524, + RASoftcorePoints: 1615, + DateAwarded: dateAwarded, + HardcoreMode: 0, + }, + }, + }, + response: func(messageBytes []byte, errorBytes []byte) []byte { + return messageBytes + }, + assert: func(t *testing.T, resp *models.GetAchievementUnlocks, err error) { + require.NotNil(t, resp) + require.Equal(t, 15906, resp.UnlocksCount) + require.Equal(t, 8223, resp.UnlocksHardcoreCount) + require.Equal(t, 38104, resp.TotalPlayers) + require.Equal(t, 1, resp.Achievement.ID) + require.Equal(t, "Ring Collector", resp.Achievement.Title) + require.Equal(t, "Collect 100 rings", resp.Achievement.Description) + require.Equal(t, 5, resp.Achievement.Points) + require.Equal(t, 7, resp.Achievement.TrueRatio) + require.Equal(t, "Scott", resp.Achievement.Author) + require.Equal(t, dateCreated, resp.Achievement.DateCreated.Time) + require.Equal(t, dateModified, resp.Achievement.DateModified.Time) + require.NotNil(t, resp.Achievement.Type) + require.Equal(t, achievementType, *resp.Achievement.Type) + require.Equal(t, 1, resp.Console.ID) + require.Equal(t, "Genesis/Mega Drive", resp.Console.Title) + require.Equal(t, 1, resp.Game.ID) + require.Equal(t, "Sonic the Hedgehog", resp.Game.Title) + require.Len(t, resp.Unlocks, 1) + require.Equal(t, "redjedia", resp.Unlocks[0].User) + require.Equal(t, 524, resp.Unlocks[0].RAPoints) + require.Equal(t, 1615, resp.Unlocks[0].RASoftcorePoints) + require.Equal(t, dateAwarded, resp.Unlocks[0].DateAwarded) + require.Equal(t, 0, resp.Unlocks[0].HardcoreMode) + require.NoError(t, err) + }, + }, + } + for _, test := range tests { + tt.Run(test.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + expectedPath := "/API/API_GetAchievementUnlocks.php" + if r.URL.Path != expectedPath { + t.Errorf("Expected to request '%s', got: %s", expectedPath, r.URL.Path) + } + w.WriteHeader(test.responseCode) + messageBytes, err := json.Marshal(test.responseMessage) + require.NoError(t, err) + errBytes, err := json.Marshal(test.responseError) + require.NoError(t, err) + resp := test.response(messageBytes, errBytes) + num, err := w.Write(resp) + require.NoError(t, err) + require.Equal(t, num, len(resp)) + })) + defer server.Close() + client := retroachievements.New(test.modifyURL(server.URL), "go-retroachievements/v0.0.0", "some_secret") + resp, err := client.GetAchievementUnlocks(test.params) + test.assert(t, resp, err) + }) + } +} diff --git a/examples/achievement/getachievementunlocks/getachievementunlocks.go b/examples/achievement/getachievementunlocks/getachievementunlocks.go new file mode 100644 index 0000000..64f9189 --- /dev/null +++ b/examples/achievement/getachievementunlocks/getachievementunlocks.go @@ -0,0 +1,28 @@ +// Package getachievementunlocks provides an example for getting a list of users who have earned an achievement +package main + +import ( + "fmt" + "os" + + "github.com/joshraphael/go-retroachievements" + "github.com/joshraphael/go-retroachievements/models" +) + +/* +Test script, add RA_API_KEY to your env and use `go run getachievementunlocks.go` +*/ +func main() { + secret := os.Getenv("RA_API_KEY") + + client := retroachievements.NewClient(secret) + + resp, err := client.GetAchievementUnlocks(models.GetAchievementUnlocksParameters{ + AchievementID: 1, + }) + if err != nil { + panic(err) + } + + fmt.Printf("%+v\n", resp) +} diff --git a/leaderboards_test.go b/leaderboards_test.go index 8dc766b..11316ed 100644 --- a/leaderboards_test.go +++ b/leaderboards_test.go @@ -141,7 +141,6 @@ func TestGetGameLeaderboards(tt *testing.T) { require.Equal(t, num, len(resp)) })) defer server.Close() - client := retroachievements.New(test.modifyURL(server.URL), "go-retroachievements/v0.0.0", "some_secret") resp, err := client.GetGameLeaderboards(test.params) test.assert(t, resp, err) @@ -265,7 +264,6 @@ func TestGetLeaderboardEntries(tt *testing.T) { require.Equal(t, num, len(resp)) })) defer server.Close() - client := retroachievements.New(test.modifyURL(server.URL), "go-retroachievements/v0.0.0", "some_secret") resp, err := client.GetLeaderboardEntries(test.params) test.assert(t, resp, err) diff --git a/models/achievement.go b/models/achievement.go new file mode 100644 index 0000000..cb41e7b --- /dev/null +++ b/models/achievement.go @@ -0,0 +1,54 @@ +package models + +import "time" + +type GetAchievementUnlocksParameters struct { + // The target achievement ID + AchievementID int + + // [Optional] The number of records to return (default: 50, max: 500). + Count *int + + // [Optional] The number of entries to skip (default: 0). + Offset *int +} + +type GetAchievementUnlocks struct { + Achievement GetAchievementUnlocksAchievement `json:"Achievement"` + Console GetAchievementUnlocksConsole `json:"Console"` + Game GetAchievementUnlocksGame `json:"Game"` + UnlocksCount int `json:"UnlocksCount"` + UnlocksHardcoreCount int `json:"UnlocksHardcoreCount"` + TotalPlayers int `json:"TotalPlayers"` + Unlocks []GetAchievementUnlocksUnlock `json:"Unlocks"` +} + +type GetAchievementUnlocksAchievement struct { + ID int `json:"ID"` + Title string `json:"Title"` + Description string `json:"Description"` + Points int `json:"Points"` + TrueRatio int `json:"TrueRatio"` + Author string `json:"Author"` + DateCreated DateTime `json:"DateCreated"` + DateModified DateTime `json:"DateModified"` + Type *string `json:"Type"` +} + +type GetAchievementUnlocksConsole struct { + ID int `json:"ID"` + Title string `json:"Title"` +} + +type GetAchievementUnlocksGame struct { + ID int `json:"ID"` + Title string `json:"Title"` +} + +type GetAchievementUnlocksUnlock struct { + User string `json:"User"` + RAPoints int `json:"RAPoints"` + RASoftcorePoints int `json:"RASoftcorePoints"` + DateAwarded time.Time `json:"DateAwarded"` + HardcoreMode int `json:"HardcoreMode"` +}