Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ func NewAPIWithVersion(globalConfig *conf.GlobalConfiguration, db *storage.Conne
r.Route("/{client_id}", func(r *router) {
r.Use(api.oauthServer.LoadOAuthServerClient)
r.Get("/", api.oauthServer.OAuthServerClientGet)
r.Put("/", api.oauthServer.OAuthServerClientUpdate)
r.Delete("/", api.oauthServer.OAuthServerClientDelete)
r.Post("/regenerate_secret", api.oauthServer.OAuthServerClientRegenerateSecret)
})
Expand Down
27 changes: 27 additions & 0 deletions internal/api/oauthserver/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,33 @@ func (s *Server) OAuthServerClientGet(w http.ResponseWriter, r *http.Request) er
return shared.SendJSON(w, http.StatusOK, response)
}

// OAuthServerClientUpdate handles PUT /admin/oauth/clients/{client_id}
func (s *Server) OAuthServerClientUpdate(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
client := shared.GetOAuthServerClient(ctx)

var params OAuthServerClientUpdateParams
if err := json.NewDecoder(r.Body).Decode(&params); err != nil {
return apierrors.NewBadRequestError(apierrors.ErrorCodeBadJSON, "Invalid JSON body")
}

// Return early if no fields are provided for update
if params.isEmpty() {
return apierrors.NewBadRequestError(apierrors.ErrorCodeValidationFailed, "No fields provided for update")
}

updatedClient, err := s.updateOAuthServerClient(ctx, client.ID, &params)
if err != nil {
if httpErr, ok := err.(*apierrors.HTTPError); ok {
return httpErr
}
return apierrors.NewInternalServerError("Error updating OAuth client").WithInternalError(err)
}

response := oauthServerClientToResponse(updatedClient)
return shared.SendJSON(w, http.StatusOK, response)
}

// OAuthServerClientDelete handles DELETE /admin/oauth/clients/{client_id}
func (s *Server) OAuthServerClientDelete(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
Expand Down
159 changes: 159 additions & 0 deletions internal/api/oauthserver/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,165 @@ func (ts *OAuthClientTestSuite) TestOAuthServerClientListHandler() {
}
}

func (ts *OAuthClientTestSuite) TestOAuthServerClientUpdateHandler() {
// Create a test client first
client, _ := ts.createTestOAuthClient()

// Test updating all fields
newRedirectURIs := []string{"https://newapp.example.com/callback"}
newGrantTypes := []string{"authorization_code", "refresh_token"}
newClientName := "Updated Client Name"
newClientURI := "https://newapp.example.com"
newLogoURI := "https://newapp.example.com/logo.png"

payload := OAuthServerClientUpdateParams{
RedirectURIs: &newRedirectURIs,
GrantTypes: &newGrantTypes,
ClientName: &newClientName,
ClientURI: &newClientURI,
LogoURI: &newLogoURI,
}

body, err := json.Marshal(payload)
require.NoError(ts.T(), err)

req := httptest.NewRequest(http.MethodPut, "/admin/oauth/clients/"+client.ID.String(), bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")

ctx := shared.WithOAuthServerClient(req.Context(), client)
req = req.WithContext(ctx)

w := httptest.NewRecorder()

err = ts.Server.OAuthServerClientUpdate(w, req)
require.NoError(ts.T(), err)

assert.Equal(ts.T(), http.StatusOK, w.Code)

var response OAuthServerClientResponse
err = json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(ts.T(), err)

assert.Equal(ts.T(), client.ID.String(), response.ClientID)
assert.Equal(ts.T(), newClientName, response.ClientName)
assert.Equal(ts.T(), newRedirectURIs, response.RedirectURIs)
assert.Equal(ts.T(), newGrantTypes, response.GrantTypes)
assert.Equal(ts.T(), newClientURI, response.ClientURI)
assert.Equal(ts.T(), newLogoURI, response.LogoURI)
assert.Empty(ts.T(), response.ClientSecret) // Should NOT be included in update response
}

func (ts *OAuthClientTestSuite) TestOAuthServerClientUpdateHandlerPartial() {
// Create a test client first
client, _ := ts.createTestOAuthClient()

// Test updating only client name
newClientName := "Partially Updated Name"
payload := OAuthServerClientUpdateParams{
ClientName: &newClientName,
}

body, err := json.Marshal(payload)
require.NoError(ts.T(), err)

req := httptest.NewRequest(http.MethodPut, "/admin/oauth/clients/"+client.ID.String(), bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")

ctx := shared.WithOAuthServerClient(req.Context(), client)
req = req.WithContext(ctx)

w := httptest.NewRecorder()

err = ts.Server.OAuthServerClientUpdate(w, req)
require.NoError(ts.T(), err)

assert.Equal(ts.T(), http.StatusOK, w.Code)

var response OAuthServerClientResponse
err = json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(ts.T(), err)

// Verify only client name was updated
assert.Equal(ts.T(), newClientName, response.ClientName)
// Verify other fields remained unchanged
assert.Equal(ts.T(), client.GetRedirectURIs(), response.RedirectURIs)
}

func (ts *OAuthClientTestSuite) TestOAuthServerClientUpdateHandlerEmptyBody() {
// Create a test client first
client, _ := ts.createTestOAuthClient()

// Test with empty body
payload := OAuthServerClientUpdateParams{}

body, err := json.Marshal(payload)
require.NoError(ts.T(), err)

req := httptest.NewRequest(http.MethodPut, "/admin/oauth/clients/"+client.ID.String(), bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")

ctx := shared.WithOAuthServerClient(req.Context(), client)
req = req.WithContext(ctx)

w := httptest.NewRecorder()

err = ts.Server.OAuthServerClientUpdate(w, req)
require.Error(ts.T(), err)
assert.Contains(ts.T(), err.Error(), "No fields provided for update")
}

func (ts *OAuthClientTestSuite) TestOAuthServerClientUpdateHandlerInvalidValidation() {
// Create a test client first
client, _ := ts.createTestOAuthClient()

// Test with invalid redirect URI
invalidRedirectURIs := []string{"invalid-uri"}
payload := OAuthServerClientUpdateParams{
RedirectURIs: &invalidRedirectURIs,
}

body, err := json.Marshal(payload)
require.NoError(ts.T(), err)

req := httptest.NewRequest(http.MethodPut, "/admin/oauth/clients/"+client.ID.String(), bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")

ctx := shared.WithOAuthServerClient(req.Context(), client)
req = req.WithContext(ctx)

w := httptest.NewRecorder()

err = ts.Server.OAuthServerClientUpdate(w, req)
require.Error(ts.T(), err)
assert.Contains(ts.T(), err.Error(), "invalid redirect_uri")
}

func (ts *OAuthClientTestSuite) TestOAuthServerClientUpdateHandlerSameValues() {
// Create a test client first
client, _ := ts.createTestOAuthClient()

// Update with same values (should succeed)
currentName := "Test Client"
payload := OAuthServerClientUpdateParams{
ClientName: &currentName,
}

body, err := json.Marshal(payload)
require.NoError(ts.T(), err)

req := httptest.NewRequest(http.MethodPut, "/admin/oauth/clients/"+client.ID.String(), bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")

ctx := shared.WithOAuthServerClient(req.Context(), client)
req = req.WithContext(ctx)

w := httptest.NewRecorder()

err = ts.Server.OAuthServerClientUpdate(w, req)
require.NoError(ts.T(), err)
assert.Equal(ts.T(), http.StatusOK, w.Code)
}

func (ts *OAuthClientTestSuite) TestHandlerValidation() {
// Test invalid JSON body
req := httptest.NewRequest(http.MethodPost, "/admin/oauth/clients", bytes.NewReader([]byte("invalid json")))
Expand Down
Loading