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 GraphQLUpload type (Content io.Reader, Filename, optional ContentType).
  • Placing a *GraphQLUpload anywhere inside GraphQLQuery.Variables, at any nesting depth, including inside lists, switches Do to a multipart/form-data request with the operations / map / file parts. Queries without uploads keep the existing JSON path untouched.
  • Variable paths are computed by mirroring encoding/json semantics (json tags, embedded struct flattening, sorted map keys) so the map field always matches the marshaled operations.
  • Content is buffered so the body stays replayable across retryablehttp retries; 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.Achievements registered in gitlab.go, service map entry, and generated mocks (testing/achievements_mock.go, testing/api_generated.go, testing/client_generated.go).
  • ImpersonateTestUser helper in gitlab_test/utils_test.go: userAchievementsUpdate (the showOnProfile flag) 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.go covering the happy path, mutation-level GraphQL errors and ErrNotFound for every method.
  • 13 unit tests in graphql_test.go for 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 → toggle showOnProfile as 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: newGIDStrings is now used in UpdateUserAchievementPriorities, the duplicated achievement field selection was extracted into achievementFields, and CreateAchievementOptions.Name is a *string for 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

Edited by BoxBoxJason

Merge request reports

Loading