feat: support achievements CRUD operations
What does this MR do and why?
This MR adds a new AchievementsServiceInterface covering the achievements GraphQL API, and adds the generic GraphQL file upload support that the achievement avatar endpoints require.
The scope has grown since the MR was opened (initially mutations only, in a single commit), so here is the current state.
1. Achievements service (achievements.go)
Mutations:
| Method | GraphQL mutation |
|---|---|
CreateAchievement |
achievementsCreate |
UpdateAchievement |
achievementsUpdate |
DeleteAchievement |
achievementsDelete |
AwardAchievement |
achievementsAward |
RevokeAchievement |
achievementsRevoke |
UpdateUserAchievement |
userAchievementsUpdate |
DeleteUserAchievement |
userAchievementsDelete |
UpdateUserAchievementPriorities |
userAchievementPrioritiesUpdate |
Queries (added after the first review round, the MR originally had no read endpoints):
| Method | GraphQL query |
|---|---|
ListUserAchievements |
user.userAchievements |
ListAchievements |
namespace.achievements |
ListAchievementRecipients |
achievement.userAchievements |
ListAchievementUniqueUsers |
achievement.uniqueUsers |
All list endpoints support the standard GraphQL cursor pagination arguments (After, Before, First, Last). GIDs are converted to/from int64 at the boundary so the public types expose plain IDs, consistent with the rest of the library (Achievement, UserAchievement, and []*BasicUser for unique users).
2. GraphQL file upload support (graphql.go)
Achievement avatars are an Upload scalar, which the client had no way to send. Rather than special-casing it in the achievements service, GraphQL.Do now implements the GraphQL multipart request specification that GitLab supports through apollo_upload_server:
- New exported
GraphQLUploadtype (Content io.Reader,Filename, optionalContentType). - Placing a
*GraphQLUploadanywhere insideGraphQLQuery.Variables, at any nesting depth, including inside lists, switchesDoto amultipart/form-datarequest with theoperations/map/ file parts. Queries without uploads keep the existing JSON path untouched. - Variable paths are computed by mirroring
encoding/jsonsemantics (json tags, embedded struct flattening, sorted map keys) so themapfield always matches the marshaled operations. - Content is buffered so the body stays replayable across
retryablehttpretries; uploads are validated before any reader is consumed; a single upload referenced from several variables is sent once.
This is usable by any future service that needs an Upload scalar, not just achievements. CreateAchievementOptions.Avatar and UpdateAchievementOptions.Avatar are the first consumers.
3. Plumbing
Client.Achievementsregistered ingitlab.go, service map entry, and generated mocks (testing/achievements_mock.go,testing/api_generated.go,testing/client_generated.go).ImpersonateTestUserhelper ingitlab_test/utils_test.go:userAchievementsUpdate(theshowOnProfileflag) is only permitted for the owning user, not for an admin acting on their behalf, so the integration test needs a client authenticated as the test user.
Testing
- 42 unit tests in
achievements_test.gocovering the happy path, mutation-level GraphQL errors andErrNotFoundfor every method. - 13 unit tests in
graphql_test.gofor the upload path: single file, multiple files, list-valued upload arguments, a shared upload, invalid uploads, reader errors, body replay on retry, and the path collection itself. - A runnable example (
example_graphql_upload_test.go) documenting the upload API. gitlab_test/achievements_integration_test.go(//go:build integration) walking the full lifecycle: create → update → award → list (user achievements, namespace achievements, recipients, unique users) → reprioritize → toggleshowOnProfileas the impersonated user → revoke → delete, with cleanup registered at each step.
Beyond the test suite, this service is fully exercised end to end by https://github.com/boxboxjason/gitlab-achievements, which drives every method in this MR (including avatar uploads) against a real GitLab instance.
Notes for reviewers
- Re. the earlier EE question: achievements are available in all tiers (docs), and there is no
ee/-only achievements code in the GitLab source, so no tier note was added to the interface. Happy to add one if that turns out to be wrong. - Duo's actionable review points were addressed:
newGIDStringsis now used inUpdateUserAchievementPriorities, the duplicated achievement field selection was extracted intoachievementFields, andCreateAchievementOptions.Nameis a*stringfor consistency with the rest of the library. - The work is split into reviewable commits (mutations → tests → error handling → list endpoints → upload support) and can be squashed on merge.
Closes #2285
Signed-off-by: BoxBoxJason contact@boxboxjason.dev