From 5694b172322c8d9e2b85c7c175e077b909d6001b Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Sat, 25 Jan 2025 20:28:32 +0000 Subject: [PATCH 01/11] Added Health API Method, with successful unit test. Added relevant structs to model data. Signed-off-by: SolarFactories --- about.go | 21 +++++++++++++++++++++ about_test.go | 16 ++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/about.go b/about.go index 2a2bf49..1d1bf3f 100644 --- a/about.go +++ b/about.go @@ -23,6 +23,17 @@ type AboutFramework struct { Timestamp string `json:"timestamp"` } +type HealthCheck struct { + Name string `json:"name"` + Status string `json:"status"` + Data interface{} `json:"data,omitempty"` +} + +type Health struct { + Status string `json:"status"` + Checks []HealthCheck `json:"checks"` +} + type AboutService struct { client *Client } @@ -36,3 +47,13 @@ func (as AboutService) Get(ctx context.Context) (a About, err error) { _, err = as.client.doRequest(req, &a) return } + +func (as AboutService) Health(ctx context.Context) (h Health, err error) { + req, err := as.client.newRequest(ctx, http.MethodGet, "/health", withoutAuth()) + if err != nil { + return + } + + _, err = as.client.doRequest(req, &h) + return +} diff --git a/about_test.go b/about_test.go index 0970e09..d71c45f 100644 --- a/about_test.go +++ b/about_test.go @@ -31,6 +31,22 @@ func TestAboutService_Get(t *testing.T) { require.Equal(t, "Alpine", about.Framework.Name) } +func TestHealth(t *testing.T) { + client := setUpContainer(t, testContainerOptions{}) + + health, err := client.About.Health(context.TODO()) + require.NoError(t, err) + require.NotNil(t, health) + + require.Equal(t, "UP", health.Status) + require.Equal(t, 1, len(health.Checks)) + require.Equal(t, "database", health.Checks[0].Name) + require.Equal(t, "UP", health.Checks[0].Status) + require.NotNil(t, health.Checks[0].Data) + require.Equal(t, "UP", health.Checks[0].Data.(map[string]interface{})["nontx_connection_pool"]) + require.Equal(t, "UP", health.Checks[0].Data.(map[string]interface{})["tx_connection_pool"]) +} + type testContainerOptions struct { Version string APIPermissions []string From f786ba2aead12a9f47277a443c7d4ef582f967c0 Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Sat, 25 Jan 2025 21:46:02 +0000 Subject: [PATCH 02/11] Added CRUD methods for managed users, along with unit tests. Signed-off-by: SolarFactories --- user.go | 50 ++++++++++++++++++ user_test.go | 142 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 user_test.go diff --git a/user.go b/user.go index 83ba1a4..c60bf36 100644 --- a/user.go +++ b/user.go @@ -10,6 +10,20 @@ type UserService struct { client *Client } +type ManagedUser struct { + Username string `json:"username"` + LastPasswordChange int `json:"lastPasswordChange"` + Fullname string `json:"fullname,omitempty"` + Email string `json:"email,omitempty"` + Suspended bool `json:"suspended,omitempty"` + ForcePasswordChange bool `json:"forcePasswordChange,omitempty"` + NonExpiryPassword bool `json:"nonExpiryPassword,omitempty"` + Teams []Team `json:"teams,omitempty"` + Permissions []Permission `json:"permissions,omitempty"` + NewPassword string `json:"newPassword,omitempty"` + ConfirmPassword string `json:"confirmPassword,omitempty"` +} + func (us UserService) Login(ctx context.Context, username, password string) (token string, err error) { body := url.Values{} body.Set("username", username) @@ -43,3 +57,39 @@ func (us UserService) ForceChangePassword(ctx context.Context, username, passwor _, err = us.client.doRequest(req, nil) return } + +func (us UserService) GetAllManaged(ctx context.Context, po PageOptions) (p Page[ManagedUser], err error) { + req, err := us.client.newRequest(ctx, http.MethodGet, "/api/v1/user/managed", withPageOptions(po)) + if err != nil { + return + } + _, err = us.client.doRequest(req, &p.Items) + return +} + +func (us UserService) CreateManaged(ctx context.Context, usr ManagedUser) (user ManagedUser, err error) { + req, err := us.client.newRequest(ctx, http.MethodPut, "/api/v1/user/managed", withBody(usr)) + if err != nil { + return + } + _, err = us.client.doRequest(req, &user) + return +} + +func (us UserService) UpdateManaged(ctx context.Context, usr ManagedUser) (user ManagedUser, err error) { + req, err := us.client.newRequest(ctx, http.MethodPost, "/api/v1/user/managed", withBody(usr)) + if err != nil { + return + } + _, err = us.client.doRequest(req, &user) + return +} + +func (us UserService) DeleteManaged(ctx context.Context, user ManagedUser) (err error) { + req, err := us.client.newRequest(ctx, http.MethodDelete, "/api/v1/user/managed", withBody(user)) + if err != nil { + return + } + _, err = us.client.doRequest(req, nil) + return +} diff --git a/user_test.go b/user_test.go new file mode 100644 index 0000000..47aadb5 --- /dev/null +++ b/user_test.go @@ -0,0 +1,142 @@ +package dtrack + +import ( + "context" + "github.com/stretchr/testify/require" + "testing" +) + +func TestCreateManagedUser(t *testing.T) { + client := setUpContainer(t, testContainerOptions{ + APIPermissions: []string{ + PermissionAccessManagement, + }, + }) + + user, err := client.User.CreateManaged(context.Background(), ManagedUser{ + Username: "test-managed", + Fullname: "test-managed-full-name", + Email: "test-managed-email@localhost", + NewPassword: "test-managed-password", + ConfirmPassword: "test-managed-password", + }) + require.NoError(t, err) + + require.Equal(t, user.Username, "test-managed") + require.NotNil(t, user.LastPasswordChange) + require.Equal(t, user.Fullname, "test-managed-full-name") + require.Equal(t, user.Email, "test-managed-email@localhost") + require.Equal(t, user.Teams, []Team{}) + require.Equal(t, user.Permissions, []Permission{}) + require.Equal(t, user.Suspended, false) + require.Equal(t, user.ForcePasswordChange, false) + require.Equal(t, user.NonExpiryPassword, false) +} + +func TestUpdateManagedUser(t *testing.T) { + client := setUpContainer(t, testContainerOptions{ + APIPermissions: []string{ + PermissionAccessManagement, + }, + }) + + user, err := client.User.CreateManaged(context.Background(), ManagedUser{ + Username: "test-managed", + Fullname: "test-managed-full-name", + Email: "test-managed-email@localhost", + NewPassword: "test-managed-password", + ConfirmPassword: "test-managed-password", + }) + require.NoError(t, err) + require.Equal(t, user.Username, "test-managed") + require.Equal(t, user.Fullname, "test-managed-full-name") + require.Equal(t, user.Email, "test-managed-email@localhost") + + updated, err := client.User.UpdateManaged(context.Background(), ManagedUser{ + Username: user.Username, + Fullname: "test-managed-full-name-updated", + Email: user.Email, + }) + + require.NoError(t, err) + require.Equal(t, updated.Username, user.Username) + require.Equal(t, updated.Fullname, "test-managed-full-name-updated") + require.Equal(t, updated.Email, user.Email) +} + +func TestGetAllManagedUsers(t *testing.T) { + client := setUpContainer(t, testContainerOptions{ + APIPermissions: []string{ + PermissionAccessManagement, + }, + }) + + user, err := client.User.CreateManaged(context.Background(), ManagedUser{ + Username: "test-managed", + Fullname: "test-managed-full-name", + Email: "test-managed-email@localhost", + NewPassword: "test-managed-password", + ConfirmPassword: "test-managed-password", + }) + require.NoError(t, err) + require.Equal(t, user.Username, "test-managed") + require.Equal(t, user.Fullname, "test-managed-full-name") + require.Equal(t, user.Email, "test-managed-email@localhost") + + users, err := FetchAll(func(po PageOptions) (Page[ManagedUser], error) { + return client.User.GetAllManaged(context.Background(), po) + }) + + require.NoError(t, err) + require.Equal(t, len(users), 2) + require.Equal(t, users[0].Username, "admin") + require.Equal(t, len(users[0].Teams), 1) + require.Equal(t, users[0].Teams[0].Name, "Administrators") + require.Equal(t, len(users[0].Permissions), 14) + + require.Equal(t, users[1].Username, "test-managed") +} + +func TestDeleteManagedUser(t *testing.T) { + // Create User + // Validate creation of user + // Delete user + // Validate deletion of user + client := setUpContainer(t, testContainerOptions{ + APIPermissions: []string{ + PermissionAccessManagement, + }, + }) + + user, err := client.User.CreateManaged(context.Background(), ManagedUser{ + Username: "test-managed", + Fullname: "test-managed-full-name", + Email: "test-managed-email@localhost", + NewPassword: "test-managed-password", + ConfirmPassword: "test-managed-password", + }) + require.NoError(t, err) + require.Equal(t, user.Username, "test-managed") + require.Equal(t, user.Fullname, "test-managed-full-name") + require.Equal(t, user.Email, "test-managed-email@localhost") + + users, err := FetchAll(func(po PageOptions) (Page[ManagedUser], error) { + return client.User.GetAllManaged(context.Background(), po) + }) + + require.NoError(t, err) + require.Equal(t, len(users), 2) + + err = client.User.DeleteManaged(context.Background(), ManagedUser{ + Username: user.Username, + }) + + require.NoError(t, err) + + users, err = FetchAll(func(po PageOptions) (Page[ManagedUser], error) { + return client.User.GetAllManaged(context.Background(), po) + }) + + require.NoError(t, err) + require.Equal(t, len(users), 1) +} From 744f77436a8697d5a090ffc068d974dc9385e7c5 Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Sun, 14 Dec 2025 13:53:59 +0000 Subject: [PATCH 03/11] Added series of methods for user management for managed user teams, and OIDC user management, and current user. Signed-off-by: SolarFactories --- oidc.go | 73 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ user.go | 61 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) diff --git a/oidc.go b/oidc.go index 501aac4..2a68178 100644 --- a/oidc.go +++ b/oidc.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "net/url" "strconv" "github.com/google/uuid" @@ -28,6 +29,19 @@ type OIDCMapping struct { UUID uuid.UUID `json:"uuid"` } +type OIDCUser struct { + Username string `json:"username"` + SubjectIdentifier string `json:"subjectIdentifier"` + Email string `json:"email"` + Teams []Team `json:"teams"` + Permissions []Permission `json:"permissions"` +} + +type OIDCTokens struct { + ID string `json:"idToken"` + Access string `json:"accessToken,omitempty"` +} + func (s OIDCService) Available(ctx context.Context) (available bool, err error) { req, err := s.client.newRequest(ctx, http.MethodGet, "/api/v1/oidc/available", withAcceptContentType("text/plain")) if err != nil { @@ -122,3 +136,62 @@ func (s OIDCService) RemoveTeamMapping(ctx context.Context, mappingID uuid.UUID) _, err = s.client.doRequest(req, nil) return } + +func (s OIDCService) RemoveTeamMapping2(ctx context.Context, groupID, teamID uuid.UUID) (err error) { + req, err := s.client.newRequest(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/oidc/group/%s/team/%s/mapping", groupID.String(), teamID.String())) + if err != nil { + return + } + + _, err = s.client.doRequest(req, nil) + return +} + +func (s OIDCService) GetAllUsers(ctx context.Context) (p Page[OIDCUser], err error) { + req, err := s.client.newRequest(ctx, http.MethodGet, "/api/v1/user/oidc") + if err != nil { + return + } + + res, err := s.client.doRequest(req, &p.Items) + if err != nil { + return + } + + p.TotalCount = res.TotalCount + return +} + +func (s OIDCService) CreateUser(ctx context.Context, userReq OIDCUser) (userRes OIDCUser, err error) { + req, err := s.client.newRequest(ctx, http.MethodPut, "/api/v1/user/oidc", withBody(userReq)) + if err != nil { + return + } + + _, err = s.client.doRequest(req, &userRes) + return +} + +func (s OIDCService) DeleteUser(ctx context.Context, user OIDCUser) (err error) { + req, err := s.client.newRequest(ctx, http.MethodDelete, "/api/v1/user/oidc", withBody(user)) + if err != nil { + return + } + + _, err = s.client.doRequest(req, nil) + return +} + +func (s OIDCService) Login(ctx context.Context, tokens OIDCTokens) (token string, err error) { + body := url.Values{} + body.Set("idToken", tokens.ID) + body.Set("accessToken", tokens.Access) + + req, err := s.client.newRequest(ctx, http.MethodPost, "/api/v1/user/oidc/login", withBody(body)) + if err != nil { + return + } + + _, err = s.client.doRequest(req, &token) + return +} diff --git a/user.go b/user.go index c60bf36..93372ac 100644 --- a/user.go +++ b/user.go @@ -2,8 +2,11 @@ package dtrack import ( "context" + "fmt" "net/http" "net/url" + + "github.com/google/uuid" ) type UserService struct { @@ -24,6 +27,19 @@ type ManagedUser struct { ConfirmPassword string `json:"confirmPassword,omitempty"` } +type UserPrincipal struct { + Teams []Team `json:"teams"` + Username string `json:"username"` + Email string `json:"email"` + Id int64 `json:"id,omitempty"` + Permissions []Permission `json:"permissions"` + Name string `json:"name"` +} + +type IdentifiableObject struct { + UUID uuid.UUID `json:"uuid"` +} + func (us UserService) Login(ctx context.Context, username, password string) (token string, err error) { body := url.Values{} body.Set("username", username) @@ -58,6 +74,7 @@ func (us UserService) ForceChangePassword(ctx context.Context, username, passwor return } +// TODO: Add minimum API version checks func (us UserService) GetAllManaged(ctx context.Context, po PageOptions) (p Page[ManagedUser], err error) { req, err := us.client.newRequest(ctx, http.MethodGet, "/api/v1/user/managed", withPageOptions(po)) if err != nil { @@ -93,3 +110,47 @@ func (us UserService) DeleteManaged(ctx context.Context, user ManagedUser) (err _, err = us.client.doRequest(req, nil) return } + +func (us UserService) AddTeamToUser(ctx context.Context, username string, team uuid.UUID) (user UserPrincipal, err error) { + req, err := us.client.newRequest(ctx, http.MethodPost, fmt.Sprintf("/api/v1/user/%s/membership", username), withBody(IdentifiableObject{ + UUID: team, + })) + if err != nil { + return + } + + _, err = us.client.doRequest(req, &user) + return +} + +func (us UserService) RemoveTeamFromUser(ctx context.Context, username string, team uuid.UUID) (user UserPrincipal, err error) { + req, err := us.client.newRequest(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/user/%s/membership", username), withBody(IdentifiableObject{ + UUID: team, + })) + if err != nil { + return + } + + _, err = us.client.doRequest(req, &user) + return +} + +func (us UserService) GetSelf(ctx context.Context) (user UserPrincipal, err error) { + req, err := us.client.newRequest(ctx, http.MethodGet, "/api/v1/user/self") + if err != nil { + return + } + + _, err = us.client.doRequest(req, &user) + return +} + +func (us UserService) UpdateSelf(ctx context.Context, userReq ManagedUser) (userRes ManagedUser, err error) { + req, err := us.client.newRequest(ctx, http.MethodPost, "/api/v1/user/self", withBody(userReq)) + if err != nil { + return + } + + _, err = us.client.doRequest(req, &userRes) + return +} From 8ed21765514c0db4c7597f310ce383786bcf49d9 Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Sun, 14 Dec 2025 14:34:11 +0000 Subject: [PATCH 04/11] Add minimum API version checks to methods. Signed-off-by: SolarFactories --- oidc.go | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++ permission.go | 20 +++++++++++++++++ user.go | 51 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 130 insertions(+), 1 deletion(-) diff --git a/oidc.go b/oidc.go index 2a68178..b8ef978 100644 --- a/oidc.go +++ b/oidc.go @@ -43,6 +43,11 @@ type OIDCTokens struct { } func (s OIDCService) Available(ctx context.Context) (available bool, err error) { + err = s.client.assertServerVersionAtLeast("4.0.0") + if err != nil { + return + } + req, err := s.client.newRequest(ctx, http.MethodGet, "/api/v1/oidc/available", withAcceptContentType("text/plain")) if err != nil { return @@ -59,6 +64,11 @@ func (s OIDCService) Available(ctx context.Context) (available bool, err error) } func (s OIDCService) GetAllGroups(ctx context.Context, po PageOptions) (p Page[OIDCGroup], err error) { + err = s.client.assertServerVersionAtLeast("4.0.0") + if err != nil { + return + } + req, err := s.client.newRequest(ctx, http.MethodGet, "/api/v1/oidc/group", withPageOptions(po)) if err != nil { return @@ -74,6 +84,11 @@ func (s OIDCService) GetAllGroups(ctx context.Context, po PageOptions) (p Page[O } func (s OIDCService) CreateGroup(ctx context.Context, name string) (g OIDCGroup, err error) { + err = s.client.assertServerVersionAtLeast("4.0.0") + if err != nil { + return + } + req, err := s.client.newRequest(ctx, http.MethodPut, "/api/v1/oidc/group", withBody(OIDCGroup{Name: name})) if err != nil { return @@ -83,6 +98,11 @@ func (s OIDCService) CreateGroup(ctx context.Context, name string) (g OIDCGroup, return } func (s OIDCService) UpdateGroup(ctx context.Context, group OIDCGroup) (g OIDCGroup, err error) { + err = s.client.assertServerVersionAtLeast("4.0.0") + if err != nil { + return + } + req, err := s.client.newRequest(ctx, http.MethodPost, "/api/v1/oidc/group", withBody(group)) if err != nil { return @@ -93,6 +113,11 @@ func (s OIDCService) UpdateGroup(ctx context.Context, group OIDCGroup) (g OIDCGr } func (s OIDCService) DeleteGroup(ctx context.Context, groupUUID uuid.UUID) (err error) { + err = s.client.assertServerVersionAtLeast("4.0.0") + if err != nil { + return + } + req, err := s.client.newRequest(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/oidc/group/%s", groupUUID.String())) if err != nil { return @@ -118,6 +143,11 @@ func (s OIDCService) GetAllTeamsOf(ctx context.Context, group OIDCGroup, po Page } func (s OIDCService) AddTeamMapping(ctx context.Context, mapping OIDCMappingRequest) (m OIDCMapping, err error) { + err = s.client.assertServerVersionAtLeast("4.0.0") + if err != nil { + return + } + req, err := s.client.newRequest(ctx, http.MethodPut, "/api/v1/oidc/mapping", withBody(mapping)) if err != nil { return @@ -128,6 +158,11 @@ func (s OIDCService) AddTeamMapping(ctx context.Context, mapping OIDCMappingRequ } func (s OIDCService) RemoveTeamMapping(ctx context.Context, mappingID uuid.UUID) (err error) { + err = s.client.assertServerVersionAtLeast("4.0.0") + if err != nil { + return + } + req, err := s.client.newRequest(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/oidc/mapping/%s", mappingID.String())) if err != nil { return @@ -138,6 +173,11 @@ func (s OIDCService) RemoveTeamMapping(ctx context.Context, mappingID uuid.UUID) } func (s OIDCService) RemoveTeamMapping2(ctx context.Context, groupID, teamID uuid.UUID) (err error) { + err = s.client.assertServerVersionAtLeast("4.0.0") + if err != nil { + return + } + req, err := s.client.newRequest(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/oidc/group/%s/team/%s/mapping", groupID.String(), teamID.String())) if err != nil { return @@ -148,6 +188,11 @@ func (s OIDCService) RemoveTeamMapping2(ctx context.Context, groupID, teamID uui } func (s OIDCService) GetAllUsers(ctx context.Context) (p Page[OIDCUser], err error) { + err = s.client.assertServerVersionAtLeast("4.0.0") + if err != nil { + return + } + req, err := s.client.newRequest(ctx, http.MethodGet, "/api/v1/user/oidc") if err != nil { return @@ -163,6 +208,11 @@ func (s OIDCService) GetAllUsers(ctx context.Context) (p Page[OIDCUser], err err } func (s OIDCService) CreateUser(ctx context.Context, userReq OIDCUser) (userRes OIDCUser, err error) { + err = s.client.assertServerVersionAtLeast("4.0.0") + if err != nil { + return + } + req, err := s.client.newRequest(ctx, http.MethodPut, "/api/v1/user/oidc", withBody(userReq)) if err != nil { return @@ -173,6 +223,11 @@ func (s OIDCService) CreateUser(ctx context.Context, userReq OIDCUser) (userRes } func (s OIDCService) DeleteUser(ctx context.Context, user OIDCUser) (err error) { + err = s.client.assertServerVersionAtLeast("4.0.0") + if err != nil { + return + } + req, err := s.client.newRequest(ctx, http.MethodDelete, "/api/v1/user/oidc", withBody(user)) if err != nil { return @@ -183,6 +238,11 @@ func (s OIDCService) DeleteUser(ctx context.Context, user OIDCUser) (err error) } func (s OIDCService) Login(ctx context.Context, tokens OIDCTokens) (token string, err error) { + err = s.client.assertServerVersionAtLeast("4.0.0") + if err != nil { + return + } + body := url.Values{} body.Set("idToken", tokens.ID) body.Set("accessToken", tokens.Access) diff --git a/permission.go b/permission.go index d25ce79..e0dbc5f 100644 --- a/permission.go +++ b/permission.go @@ -67,3 +67,23 @@ func (ps PermissionService) RemovePermissionFromTeam(ctx context.Context, permis _, err = ps.client.doRequest(req, &t) return } + +func (ps PermissionService) AddPermissionToUser(ctx context.Context, permission Permission, username string) (user UserPrincipal, err error) { + req, err := ps.client.newRequest(ctx, http.MethodPost, fmt.Sprintf("/api/v1/permission/%s/user/%s", permission.Name, username)) + if err != nil { + return + } + + _, err = ps.client.doRequest(req, &user) + return +} + +func (ps PermissionService) RemovePermissionFromUser(ctx context.Context, permission Permission, username string) (user UserPrincipal, err error) { + req, err := ps.client.newRequest(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/permission/%s/user/%s", permission.Name, username)) + if err != nil { + return + } + + _, err = ps.client.doRequest(req, &user) + return +} diff --git a/user.go b/user.go index 93372ac..9ea18bb 100644 --- a/user.go +++ b/user.go @@ -41,6 +41,11 @@ type IdentifiableObject struct { } func (us UserService) Login(ctx context.Context, username, password string) (token string, err error) { + err = us.client.assertServerVersionAtLeast("3.0.0") + if err != nil { + return + } + body := url.Values{} body.Set("username", username) body.Set("password", password) @@ -57,6 +62,11 @@ func (us UserService) Login(ctx context.Context, username, password string) (tok } func (us UserService) ForceChangePassword(ctx context.Context, username, password, newPassword string) (err error) { + err = us.client.assertServerVersionAtLeast("3.0.0") + if err != nil { + return + } + body := url.Values{} body.Set("username", username) body.Set("password", password) @@ -74,8 +84,12 @@ func (us UserService) ForceChangePassword(ctx context.Context, username, passwor return } -// TODO: Add minimum API version checks func (us UserService) GetAllManaged(ctx context.Context, po PageOptions) (p Page[ManagedUser], err error) { + err = us.client.assertServerVersionAtLeast("3.0.0") + if err != nil { + return + } + req, err := us.client.newRequest(ctx, http.MethodGet, "/api/v1/user/managed", withPageOptions(po)) if err != nil { return @@ -85,6 +99,11 @@ func (us UserService) GetAllManaged(ctx context.Context, po PageOptions) (p Page } func (us UserService) CreateManaged(ctx context.Context, usr ManagedUser) (user ManagedUser, err error) { + err = us.client.assertServerVersionAtLeast("3.0.0") + if err != nil { + return + } + req, err := us.client.newRequest(ctx, http.MethodPut, "/api/v1/user/managed", withBody(usr)) if err != nil { return @@ -94,6 +113,11 @@ func (us UserService) CreateManaged(ctx context.Context, usr ManagedUser) (user } func (us UserService) UpdateManaged(ctx context.Context, usr ManagedUser) (user ManagedUser, err error) { + err = us.client.assertServerVersionAtLeast("3.0.0") + if err != nil { + return + } + req, err := us.client.newRequest(ctx, http.MethodPost, "/api/v1/user/managed", withBody(usr)) if err != nil { return @@ -103,6 +127,11 @@ func (us UserService) UpdateManaged(ctx context.Context, usr ManagedUser) (user } func (us UserService) DeleteManaged(ctx context.Context, user ManagedUser) (err error) { + err = us.client.assertServerVersionAtLeast("3.0.0") + if err != nil { + return + } + req, err := us.client.newRequest(ctx, http.MethodDelete, "/api/v1/user/managed", withBody(user)) if err != nil { return @@ -112,6 +141,11 @@ func (us UserService) DeleteManaged(ctx context.Context, user ManagedUser) (err } func (us UserService) AddTeamToUser(ctx context.Context, username string, team uuid.UUID) (user UserPrincipal, err error) { + err = us.client.assertServerVersionAtLeast("3.0.0") + if err != nil { + return + } + req, err := us.client.newRequest(ctx, http.MethodPost, fmt.Sprintf("/api/v1/user/%s/membership", username), withBody(IdentifiableObject{ UUID: team, })) @@ -124,6 +158,11 @@ func (us UserService) AddTeamToUser(ctx context.Context, username string, team u } func (us UserService) RemoveTeamFromUser(ctx context.Context, username string, team uuid.UUID) (user UserPrincipal, err error) { + err = us.client.assertServerVersionAtLeast("3.0.0") + if err != nil { + return + } + req, err := us.client.newRequest(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/user/%s/membership", username), withBody(IdentifiableObject{ UUID: team, })) @@ -136,6 +175,11 @@ func (us UserService) RemoveTeamFromUser(ctx context.Context, username string, t } func (us UserService) GetSelf(ctx context.Context) (user UserPrincipal, err error) { + err = us.client.assertServerVersionAtLeast("3.0.0") + if err != nil { + return + } + req, err := us.client.newRequest(ctx, http.MethodGet, "/api/v1/user/self") if err != nil { return @@ -146,6 +190,11 @@ func (us UserService) GetSelf(ctx context.Context) (user UserPrincipal, err erro } func (us UserService) UpdateSelf(ctx context.Context, userReq ManagedUser) (userRes ManagedUser, err error) { + err = us.client.assertServerVersionAtLeast("3.0.0") + if err != nil { + return + } + req, err := us.client.newRequest(ctx, http.MethodPost, "/api/v1/user/self", withBody(userReq)) if err != nil { return From 5637b6ad7fc7822d64ebd130a3dcdec971879498 Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Sun, 14 Dec 2025 19:42:19 +0000 Subject: [PATCH 05/11] Add tests for most of OIDCService. Signed-off-by: SolarFactories --- about_test.go | 4 +- oidc_test.go | 213 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 215 insertions(+), 2 deletions(-) create mode 100644 oidc_test.go diff --git a/about_test.go b/about_test.go index d71c45f..04846df 100644 --- a/about_test.go +++ b/about_test.go @@ -43,8 +43,8 @@ func TestHealth(t *testing.T) { require.Equal(t, "database", health.Checks[0].Name) require.Equal(t, "UP", health.Checks[0].Status) require.NotNil(t, health.Checks[0].Data) - require.Equal(t, "UP", health.Checks[0].Data.(map[string]interface{})["nontx_connection_pool"]) - require.Equal(t, "UP", health.Checks[0].Data.(map[string]interface{})["tx_connection_pool"]) + require.Equal(t, "UP", health.Checks[0].Data.(map[string]any)["nontx_connection_pool"]) + require.Equal(t, "UP", health.Checks[0].Data.(map[string]any)["tx_connection_pool"]) } type testContainerOptions struct { diff --git a/oidc_test.go b/oidc_test.go new file mode 100644 index 0000000..5625175 --- /dev/null +++ b/oidc_test.go @@ -0,0 +1,213 @@ +package dtrack + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOIDCGroup_Lifecycle(t *testing.T) { + ctx := context.Background() + po := PageOptions{ + PageSize: 10, + } + client := setUpContainer(t, testContainerOptions{ + APIPermissions: []string{ + PermissionSystemConfiguration, + }, + }) + // Check absence + { + groups, err := client.OIDC.GetAllGroups(ctx, po) + require.NoError(t, err) + require.Empty(t, groups.Items) + require.Zero(t, groups.TotalCount) + } + // Create Group + group, err := client.OIDC.CreateGroup(ctx, "Test_Group") + require.NoError(t, err) + require.Equal(t, group.Name, "Test_Group") + require.NotZero(t, group.UUID) + + // Check presence + { + groups, err := client.OIDC.GetAllGroups(ctx, po) + require.NoError(t, err) + require.Equal(t, groups.TotalCount, 1) + require.Equal(t, len(groups.Items), 1) + require.Equal(t, groups.Items[0], group) + } + + // Update Group + { + updated, err := client.OIDC.UpdateGroup(ctx, OIDCGroup{ + UUID: group.UUID, + Name: "Updated_Test_Group", + }) + require.NoError(t, err) + require.Equal(t, updated.UUID, group.UUID) + require.Equal(t, updated.Name, "Updated_Test_Group") + } + + // Check updated + { + groups, err := client.OIDC.GetAllGroups(ctx, po) + require.NoError(t, err) + require.Equal(t, groups.TotalCount, 1) + require.Equal(t, len(groups.Items), 1) + require.Equal(t, groups.Items[0], OIDCGroup{ + UUID: group.UUID, + Name: "Updated_Test_Group", + }) + } + + // Delete Group + { + err := client.OIDC.DeleteGroup(ctx, group.UUID) + require.NoError(t, err) + } + + // Check absence + { + groups, err := client.OIDC.GetAllGroups(ctx, po) + require.NoError(t, err) + require.Empty(t, groups.Items) + } +} + +func TestOIDCTeamMappings(t *testing.T) { + ctx := context.Background() + po := PageOptions{ + PageSize: 10, + } + client := setUpContainer(t, testContainerOptions{ + APIPermissions: []string{ + PermissionSystemConfiguration, + }, + }) + + // Add Team + team, err := client.Team.Create(ctx, Team{ + Name: "Team_Name", + }) + require.NoError(t, err) + + // Add OIDC Group + group, err := client.OIDC.CreateGroup(ctx, "Group_Name") + require.NoError(t, err) + + // Check absence + { + teams, err := client.OIDC.GetAllTeamsOf(ctx, group, po) + require.NoError(t, err) + require.Empty(t, teams.Items) + require.Zero(t, teams.TotalCount) + } + + // Add Mapping + mapping, err := client.OIDC.AddTeamMapping(ctx, OIDCMappingRequest{ + Team: team.UUID, + Group: group.UUID, + }) + require.NoError(t, err) + + // Check presence + { + teams, err := client.OIDC.GetAllTeamsOf(ctx, group, po) + require.NoError(t, err) + require.Equal(t, teams.TotalCount, 1) + require.Equal(t, teams.Items, []Team{team}) + } + + // Delete using mapping ID + { + err := client.OIDC.RemoveTeamMapping(ctx, mapping.UUID) + require.NoError(t, err) + } + + // Check absence + { + teams, err := client.OIDC.GetAllTeamsOf(ctx, group, po) + require.NoError(t, err) + require.Empty(t, teams.Items) + require.Zero(t, teams.TotalCount) + } + + // Add Mapping + mapping, err = client.OIDC.AddTeamMapping(ctx, OIDCMappingRequest{ + Team: team.UUID, + Group: group.UUID, + }) + require.NoError(t, err) + + // Check presence + { + teams, err := client.OIDC.GetAllTeamsOf(ctx, group, po) + require.NoError(t, err) + require.Equal(t, teams.TotalCount, 1) + require.Equal(t, teams.Items, []Team{team}) + } + + // Delete using Team ID, Group ID + { + err := client.OIDC.RemoveTeamMapping2(ctx, group.UUID, team.UUID) + require.NoError(t, err) + } + + // Check absence + { + teams, err := client.OIDC.GetAllTeamsOf(ctx, group, po) + require.NoError(t, err) + require.Empty(t, teams.Items) + require.Zero(t, teams.TotalCount) + } +} + +func TestOIDCUsers(t *testing.T) { + ctx := context.Background() + client := setUpContainer(t, testContainerOptions{ + APIPermissions: []string{ + PermissionSystemConfiguration, + }, + }) + + // Check absence + { + users, err := client.OIDC.GetAllUsers(ctx) + require.NoError(t, err) + require.Empty(t, users.Items) + require.Zero(t, users.TotalCount) + } + + // Create User + user, err := client.OIDC.CreateUser(ctx, OIDCUser{ + SubjectIdentifier: "Sub", + Username: "Username", + }) + require.NoError(t, err) + require.Equal(t, user.SubjectIdentifier, "Sub") + require.Equal(t, user.Username, "Username") + + // Check presence + { + users, err := client.OIDC.GetAllUsers(ctx) + require.NoError(t, err) + require.Equal(t, users.TotalCount, 1) + require.Equal(t, users.Items[0], user) + } + + // Delete User + { + err := client.OIDC.DeleteUser(ctx, user) + require.NoError(t, err) + } + + // Check absence + { + users, err := client.OIDC.GetAllUsers(ctx) + require.NoError(t, err) + require.Empty(t, users.Items) + require.Zero(t, users.TotalCount) + } +} From 0c8d263d51a207baf6b285d8ac64b6434c217791 Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Sun, 14 Dec 2025 21:24:51 +0000 Subject: [PATCH 06/11] Removed OIDCService.GetAllGroups, and OIDCService.GetAllTeamsOf from returning pages, instead of lists - to match the data returned from API - as API endpoints are not paginated - also removed passing PageOptions due to not being paginated. Updated tests accordingly. Removed SubjectIdentifier in test for creating OIDCUser, as only username is used by API. Signed-off-by: SolarFactories --- oidc.go | 20 ++++++--------- oidc_test.go | 70 ++++++++++++++++++++++------------------------------ 2 files changed, 37 insertions(+), 53 deletions(-) diff --git a/oidc.go b/oidc.go index b8ef978..4632f8a 100644 --- a/oidc.go +++ b/oidc.go @@ -63,24 +63,20 @@ func (s OIDCService) Available(ctx context.Context) (available bool, err error) return } -func (s OIDCService) GetAllGroups(ctx context.Context, po PageOptions) (p Page[OIDCGroup], err error) { +func (s OIDCService) GetAllGroups(ctx context.Context) (groups []OIDCGroup, err error) { err = s.client.assertServerVersionAtLeast("4.0.0") if err != nil { return } - req, err := s.client.newRequest(ctx, http.MethodGet, "/api/v1/oidc/group", withPageOptions(po)) + req, err := s.client.newRequest(ctx, http.MethodGet, "/api/v1/oidc/group") if err != nil { return } - res, err := s.client.doRequest(req, &p.Items) - if err != nil { - return - } - - p.TotalCount = res.TotalCount + _, err = s.client.doRequest(req, &groups) return + } func (s OIDCService) CreateGroup(ctx context.Context, name string) (g OIDCGroup, err error) { @@ -127,18 +123,18 @@ func (s OIDCService) DeleteGroup(ctx context.Context, groupUUID uuid.UUID) (err return } -func (s OIDCService) GetAllTeamsOf(ctx context.Context, group OIDCGroup, po PageOptions) (p Page[Team], err error) { - req, err := s.client.newRequest(ctx, http.MethodGet, fmt.Sprintf("/api/v1/oidc/group/%s/team", group.UUID.String()), withPageOptions(po)) +func (s OIDCService) GetAllTeamsOf(ctx context.Context, group OIDCGroup) (teams []Team, err error) { + err = s.client.assertServerVersionAtLeast("4.0.0") if err != nil { return } - res, err := s.client.doRequest(req, &p.Items) + req, err := s.client.newRequest(ctx, http.MethodGet, fmt.Sprintf("/api/v1/oidc/group/%s/team", group.UUID.String())) if err != nil { return } - p.TotalCount = res.TotalCount + _, err = s.client.doRequest(req, &teams) return } diff --git a/oidc_test.go b/oidc_test.go index 5625175..1dbc63d 100644 --- a/oidc_test.go +++ b/oidc_test.go @@ -2,27 +2,24 @@ package dtrack import ( "context" + "fmt" "testing" "github.com/stretchr/testify/require" ) -func TestOIDCGroup_Lifecycle(t *testing.T) { +func TestOIDCGroup(t *testing.T) { ctx := context.Background() - po := PageOptions{ - PageSize: 10, - } client := setUpContainer(t, testContainerOptions{ APIPermissions: []string{ - PermissionSystemConfiguration, + PermissionAccessManagement, }, }) // Check absence { - groups, err := client.OIDC.GetAllGroups(ctx, po) + groups, err := client.OIDC.GetAllGroups(ctx) require.NoError(t, err) - require.Empty(t, groups.Items) - require.Zero(t, groups.TotalCount) + require.Empty(t, groups) } // Create Group group, err := client.OIDC.CreateGroup(ctx, "Test_Group") @@ -32,11 +29,10 @@ func TestOIDCGroup_Lifecycle(t *testing.T) { // Check presence { - groups, err := client.OIDC.GetAllGroups(ctx, po) + groups, err := client.OIDC.GetAllGroups(ctx) require.NoError(t, err) - require.Equal(t, groups.TotalCount, 1) - require.Equal(t, len(groups.Items), 1) - require.Equal(t, groups.Items[0], group) + require.Equal(t, len(groups), 1) + require.Equal(t, groups[0], group) } // Update Group @@ -52,11 +48,10 @@ func TestOIDCGroup_Lifecycle(t *testing.T) { // Check updated { - groups, err := client.OIDC.GetAllGroups(ctx, po) + groups, err := client.OIDC.GetAllGroups(ctx) require.NoError(t, err) - require.Equal(t, groups.TotalCount, 1) - require.Equal(t, len(groups.Items), 1) - require.Equal(t, groups.Items[0], OIDCGroup{ + require.Equal(t, len(groups), 1) + require.Equal(t, groups[0], OIDCGroup{ UUID: group.UUID, Name: "Updated_Test_Group", }) @@ -70,20 +65,17 @@ func TestOIDCGroup_Lifecycle(t *testing.T) { // Check absence { - groups, err := client.OIDC.GetAllGroups(ctx, po) + groups, err := client.OIDC.GetAllGroups(ctx) require.NoError(t, err) - require.Empty(t, groups.Items) + require.Empty(t, groups) } } func TestOIDCTeamMappings(t *testing.T) { ctx := context.Background() - po := PageOptions{ - PageSize: 10, - } client := setUpContainer(t, testContainerOptions{ APIPermissions: []string{ - PermissionSystemConfiguration, + PermissionAccessManagement, }, }) @@ -99,10 +91,9 @@ func TestOIDCTeamMappings(t *testing.T) { // Check absence { - teams, err := client.OIDC.GetAllTeamsOf(ctx, group, po) + teams, err := client.OIDC.GetAllTeamsOf(ctx, group) require.NoError(t, err) - require.Empty(t, teams.Items) - require.Zero(t, teams.TotalCount) + require.Empty(t, teams) } // Add Mapping @@ -114,10 +105,10 @@ func TestOIDCTeamMappings(t *testing.T) { // Check presence { - teams, err := client.OIDC.GetAllTeamsOf(ctx, group, po) + teams, err := client.OIDC.GetAllTeamsOf(ctx, group) require.NoError(t, err) - require.Equal(t, teams.TotalCount, 1) - require.Equal(t, teams.Items, []Team{team}) + require.Equal(t, len(teams), 1) + require.Equal(t, teams[0].UUID, team.UUID) } // Delete using mapping ID @@ -128,10 +119,9 @@ func TestOIDCTeamMappings(t *testing.T) { // Check absence { - teams, err := client.OIDC.GetAllTeamsOf(ctx, group, po) + teams, err := client.OIDC.GetAllTeamsOf(ctx, group) require.NoError(t, err) - require.Empty(t, teams.Items) - require.Zero(t, teams.TotalCount) + require.Empty(t, teams) } // Add Mapping @@ -143,10 +133,10 @@ func TestOIDCTeamMappings(t *testing.T) { // Check presence { - teams, err := client.OIDC.GetAllTeamsOf(ctx, group, po) + teams, err := client.OIDC.GetAllTeamsOf(ctx, group) require.NoError(t, err) - require.Equal(t, teams.TotalCount, 1) - require.Equal(t, teams.Items, []Team{team}) + require.Equal(t, len(teams), 1) + require.Equal(t, teams[0].UUID, team.UUID) } // Delete using Team ID, Group ID @@ -157,10 +147,9 @@ func TestOIDCTeamMappings(t *testing.T) { // Check absence { - teams, err := client.OIDC.GetAllTeamsOf(ctx, group, po) + teams, err := client.OIDC.GetAllTeamsOf(ctx, group) require.NoError(t, err) - require.Empty(t, teams.Items) - require.Zero(t, teams.TotalCount) + require.Empty(t, teams) } } @@ -168,7 +157,7 @@ func TestOIDCUsers(t *testing.T) { ctx := context.Background() client := setUpContainer(t, testContainerOptions{ APIPermissions: []string{ - PermissionSystemConfiguration, + PermissionAccessManagement, }, }) @@ -182,11 +171,10 @@ func TestOIDCUsers(t *testing.T) { // Create User user, err := client.OIDC.CreateUser(ctx, OIDCUser{ - SubjectIdentifier: "Sub", - Username: "Username", + Username: "Username", }) require.NoError(t, err) - require.Equal(t, user.SubjectIdentifier, "Sub") + fmt.Printf("%+v", user) require.Equal(t, user.Username, "Username") // Check presence From 47fe84fc6d045d2a9e88ff36f853e204b7898c24 Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Sun, 14 Dec 2025 22:38:43 +0000 Subject: [PATCH 07/11] Apply golangci lint recommendations. Signed-off-by: SolarFactories --- client.go | 2 +- oidc_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client.go b/client.go index 25716e7..d077401 100644 --- a/client.go +++ b/client.go @@ -193,7 +193,7 @@ func withPathParams(params map[string]string) requestOption { } for k, v := range params { - req.URL.Path = strings.Replace(req.URL.Path, fmt.Sprintf("{%s}", k), v, -1) + req.URL.Path = strings.ReplaceAll(req.URL.Path, fmt.Sprintf("{%s}", k), v) } return nil } diff --git a/oidc_test.go b/oidc_test.go index 1dbc63d..f6f02dd 100644 --- a/oidc_test.go +++ b/oidc_test.go @@ -125,7 +125,7 @@ func TestOIDCTeamMappings(t *testing.T) { } // Add Mapping - mapping, err = client.OIDC.AddTeamMapping(ctx, OIDCMappingRequest{ + _, err = client.OIDC.AddTeamMapping(ctx, OIDCMappingRequest{ Team: team.UUID, Group: group.UUID, }) From 1f6ef02fc083e0c3ded9d979fb125bb59ceaef09 Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Sun, 14 Dec 2025 22:54:05 +0000 Subject: [PATCH 08/11] Increase test timeout from default 10m to 15m. Signed-off-by: SolarFactories --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 8362410..c81fa6b 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ build: .PHONY: build test: - go test -v -cover + go test -v -cover -timeout=15m .PHONY: test clean: From 041e7c66a3db1c1087a044ce12f545d26472b11d Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Thu, 1 Jan 2026 18:05:07 +0000 Subject: [PATCH 09/11] Remove leading / in path for user methods. Signed-off-by: SolarFactories --- permission.go | 4 ++-- user.go | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/permission.go b/permission.go index def363b..4763b1c 100644 --- a/permission.go +++ b/permission.go @@ -69,7 +69,7 @@ func (ps PermissionService) RemovePermissionFromTeam(ctx context.Context, permis } func (ps PermissionService) AddPermissionToUser(ctx context.Context, permission Permission, username string) (user UserPrincipal, err error) { - req, err := ps.client.newRequest(ctx, http.MethodPost, fmt.Sprintf("/api/v1/permission/%s/user/%s", permission.Name, username)) + req, err := ps.client.newRequest(ctx, http.MethodPost, fmt.Sprintf("api/v1/permission/%s/user/%s", permission.Name, username)) if err != nil { return } @@ -79,7 +79,7 @@ func (ps PermissionService) AddPermissionToUser(ctx context.Context, permission } func (ps PermissionService) RemovePermissionFromUser(ctx context.Context, permission Permission, username string) (user UserPrincipal, err error) { - req, err := ps.client.newRequest(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/permission/%s/user/%s", permission.Name, username)) + req, err := ps.client.newRequest(ctx, http.MethodDelete, fmt.Sprintf("api/v1/permission/%s/user/%s", permission.Name, username)) if err != nil { return } diff --git a/user.go b/user.go index a4be80e..d9ee07b 100644 --- a/user.go +++ b/user.go @@ -90,7 +90,7 @@ func (us UserService) GetAllManaged(ctx context.Context, po PageOptions) (p Page return } - req, err := us.client.newRequest(ctx, http.MethodGet, "/api/v1/user/managed", withPageOptions(po)) + req, err := us.client.newRequest(ctx, http.MethodGet, "api/v1/user/managed", withPageOptions(po)) if err != nil { return } @@ -104,7 +104,7 @@ func (us UserService) CreateManaged(ctx context.Context, usr ManagedUser) (user return } - req, err := us.client.newRequest(ctx, http.MethodPut, "/api/v1/user/managed", withBody(usr)) + req, err := us.client.newRequest(ctx, http.MethodPut, "api/v1/user/managed", withBody(usr)) if err != nil { return } @@ -118,7 +118,7 @@ func (us UserService) UpdateManaged(ctx context.Context, usr ManagedUser) (user return } - req, err := us.client.newRequest(ctx, http.MethodPost, "/api/v1/user/managed", withBody(usr)) + req, err := us.client.newRequest(ctx, http.MethodPost, "api/v1/user/managed", withBody(usr)) if err != nil { return } @@ -132,7 +132,7 @@ func (us UserService) DeleteManaged(ctx context.Context, user ManagedUser) (err return } - req, err := us.client.newRequest(ctx, http.MethodDelete, "/api/v1/user/managed", withBody(user)) + req, err := us.client.newRequest(ctx, http.MethodDelete, "api/v1/user/managed", withBody(user)) if err != nil { return } @@ -146,7 +146,7 @@ func (us UserService) AddTeamToUser(ctx context.Context, username string, team u return } - req, err := us.client.newRequest(ctx, http.MethodPost, fmt.Sprintf("/api/v1/user/%s/membership", username), withBody(IdentifiableObject{ + req, err := us.client.newRequest(ctx, http.MethodPost, fmt.Sprintf("api/v1/user/%s/membership", username), withBody(IdentifiableObject{ UUID: team, })) if err != nil { @@ -163,7 +163,7 @@ func (us UserService) RemoveTeamFromUser(ctx context.Context, username string, t return } - req, err := us.client.newRequest(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/user/%s/membership", username), withBody(IdentifiableObject{ + req, err := us.client.newRequest(ctx, http.MethodDelete, fmt.Sprintf("api/v1/user/%s/membership", username), withBody(IdentifiableObject{ UUID: team, })) if err != nil { @@ -180,7 +180,7 @@ func (us UserService) GetSelf(ctx context.Context) (user UserPrincipal, err erro return } - req, err := us.client.newRequest(ctx, http.MethodGet, "/api/v1/user/self") + req, err := us.client.newRequest(ctx, http.MethodGet, "api/v1/user/self") if err != nil { return } @@ -195,7 +195,7 @@ func (us UserService) UpdateSelf(ctx context.Context, userReq ManagedUser) (user return } - req, err := us.client.newRequest(ctx, http.MethodPost, "/api/v1/user/self", withBody(userReq)) + req, err := us.client.newRequest(ctx, http.MethodPost, "api/v1/user/self", withBody(userReq)) if err != nil { return } From 64ed3c5467f581ca2b1eeccefa599e9fe9cca84c Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Fri, 2 Jan 2026 12:00:32 +0000 Subject: [PATCH 10/11] Move Health check binding to new HealthService. Signed-off-by: SolarFactories --- about.go | 21 --------------------- about_test.go | 16 ---------------- client.go | 6 ++++-- health.go | 31 +++++++++++++++++++++++++++++++ health_test.go | 24 ++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 39 deletions(-) create mode 100644 health.go create mode 100644 health_test.go diff --git a/about.go b/about.go index e97b7e9..07d6d50 100644 --- a/about.go +++ b/about.go @@ -23,17 +23,6 @@ type AboutFramework struct { Timestamp string `json:"timestamp"` } -type HealthCheck struct { - Name string `json:"name"` - Status string `json:"status"` - Data interface{} `json:"data,omitempty"` -} - -type Health struct { - Status string `json:"status"` - Checks []HealthCheck `json:"checks"` -} - type AboutService struct { client *Client } @@ -47,13 +36,3 @@ func (as AboutService) Get(ctx context.Context) (a About, err error) { _, err = as.client.doRequest(req, &a) return } - -func (as AboutService) Health(ctx context.Context) (h Health, err error) { - req, err := as.client.newRequest(ctx, http.MethodGet, "health", withoutAuth()) - if err != nil { - return - } - - _, err = as.client.doRequest(req, &h) - return -} diff --git a/about_test.go b/about_test.go index 04846df..0970e09 100644 --- a/about_test.go +++ b/about_test.go @@ -31,22 +31,6 @@ func TestAboutService_Get(t *testing.T) { require.Equal(t, "Alpine", about.Framework.Name) } -func TestHealth(t *testing.T) { - client := setUpContainer(t, testContainerOptions{}) - - health, err := client.About.Health(context.TODO()) - require.NoError(t, err) - require.NotNil(t, health) - - require.Equal(t, "UP", health.Status) - require.Equal(t, 1, len(health.Checks)) - require.Equal(t, "database", health.Checks[0].Name) - require.Equal(t, "UP", health.Checks[0].Status) - require.NotNil(t, health.Checks[0].Data) - require.Equal(t, "UP", health.Checks[0].Data.(map[string]any)["nontx_connection_pool"]) - require.Equal(t, "UP", health.Checks[0].Data.(map[string]any)["tx_connection_pool"]) -} - type testContainerOptions struct { Version string APIPermissions []string diff --git a/client.go b/client.go index d077401..4a75d7b 100644 --- a/client.go +++ b/client.go @@ -41,8 +41,9 @@ type Client struct { BOM BOMService Component ComponentService Config ConfigService - Finding FindingService Event EventService + Finding FindingService + Health HealthService LDAP LDAPService License LicenseService Metrics MetricsService @@ -93,8 +94,9 @@ func NewClient(baseURL string, options ...ClientOption) (*Client, error) { client.BOM = BOMService{client: &client} client.Component = ComponentService{client: &client} client.Config = ConfigService{client: &client} - client.Finding = FindingService{client: &client} client.Event = EventService{client: &client} + client.Finding = FindingService{client: &client} + client.Health = HealthService{client: &client} client.LDAP = LDAPService{client: &client} client.License = LicenseService{client: &client} client.Metrics = MetricsService{client: &client} diff --git a/health.go b/health.go new file mode 100644 index 0000000..b0cc615 --- /dev/null +++ b/health.go @@ -0,0 +1,31 @@ +package dtrack + +import ( + "context" + "net/http" +) + +type HealthCheck struct { + Name string `json:"name"` + Status string `json:"status"` + Data interface{} `json:"data,omitempty"` +} + +type Health struct { + Status string `json:"status"` + Checks []HealthCheck `json:"checks"` +} + +type HealthService struct { + client *Client +} + +func (hs HealthService) Get(ctx context.Context) (h Health, err error) { + req, err := hs.client.newRequest(ctx, http.MethodGet, "health", withoutAuth()) + if err != nil { + return + } + + _, err = hs.client.doRequest(req, &h) + return +} diff --git a/health_test.go b/health_test.go new file mode 100644 index 0000000..51c3f9f --- /dev/null +++ b/health_test.go @@ -0,0 +1,24 @@ +package dtrack + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestHealth(t *testing.T) { + client := setUpContainer(t, testContainerOptions{}) + + health, err := client.Health.Get(context.TODO()) + require.NoError(t, err) + require.NotNil(t, health) + + require.Equal(t, "UP", health.Status) + require.Equal(t, 1, len(health.Checks)) + require.Equal(t, "database", health.Checks[0].Name) + require.Equal(t, "UP", health.Checks[0].Status) + require.NotNil(t, health.Checks[0].Data) + require.Equal(t, "UP", health.Checks[0].Data.(map[string]any)["nontx_connection_pool"]) + require.Equal(t, "UP", health.Checks[0].Data.(map[string]any)["tx_connection_pool"]) +} From addd76a5a3c41cc92e508c6c1f808ec2969471a0 Mon Sep 17 00:00:00 2001 From: SolarFactories Date: Fri, 2 Jan 2026 12:58:39 +0000 Subject: [PATCH 11/11] Annotated intermittent 500 response in TestComponentLifecycle. Signed-off-by: SolarFactories --- component_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/component_test.go b/component_test.go index fabc8fa..1c502d3 100644 --- a/component_test.go +++ b/component_test.go @@ -69,6 +69,8 @@ func TestComponentLifecycle(t *testing.T) { // Delete { err := client.Component.Delete(context.Background(), component.UUID) + // Occassionally receives 500 response from API - https://github.com/DependencyTrack/client-go/actions/runs/20657420675/job/59312871798?pr=55 + // Due to the intermittent nature, the cause is not yet identified. require.NoError(t, err) }