diff --git a/accounts.go b/accounts.go index 8caf07e..c8baaaf 100644 --- a/accounts.go +++ b/accounts.go @@ -453,6 +453,25 @@ func (s *AccountsService) GetSSHKey(ctx context.Context, accountID, sshKeyID str return v, resp, err } +// AddSSHKey adds an SSH key to a user's account. +// Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#add-ssh-key +func (s *AccountsService) AddSSHKey(ctx context.Context, accountID string, sshKey string) (*SSHKeyInfo, *Response, error) { + u := fmt.Sprintf("accounts/%s/sshkeys", accountID) + + req, err := s.client.NewRawPostRequest(ctx, u, sshKey) + if err != nil { + return nil, nil, err + } + + var keyInfo SSHKeyInfo + resp, err := s.client.Do(req, &keyInfo) + if err != nil { + return nil, resp, err + } + + return &keyInfo, resp, err +} + // ListGPGKeys returns the GPG keys of an account. // // Gerrit API docs: https://gerrit-review.googlesource.com/Documentation/rest-api-accounts.html#list-gpg-keys @@ -957,6 +976,5 @@ func (s *AccountsService) UnstarChange(ctx context.Context, accountID, changeID /* Missing Account Endpoints: - Add SSH Key Get Avatar */ diff --git a/accounts_test.go b/accounts_test.go new file mode 100644 index 0000000..2e5acaa --- /dev/null +++ b/accounts_test.go @@ -0,0 +1,71 @@ +package gerrit_test + +import ( + "context" + "fmt" + "io" + "net/http" + "strings" + "testing" +) + +// TestAddSSHKey tests the addition of an SSH key to an account. +func TestAddSSHKey(t *testing.T) { + setup() + defer teardown() + + testMux.HandleFunc("/accounts/self/sshkeys", func(w http.ResponseWriter, r *http.Request) { + // Ensure the request method is POST + if r.Method != http.MethodPost { + t.Errorf("Expected POST request, got %s", r.Method) + } + + // Ensure Content-Type is text/plain + if r.Header.Get("Content-Type") != "text/plain" { + t.Errorf("Expected Content-Type 'text/plain', got %s", r.Header.Get("Content-Type")) + } + + // Read body and validate SSH key + expectedSSHKey := "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw== john.doe@example.com" + body, _ := io.ReadAll(r.Body) + receivedSSHKey := strings.TrimSpace(string(body)) + + if receivedSSHKey != expectedSSHKey { + t.Errorf("Expected SSH key '%s', but received '%s'", expectedSSHKey, receivedSSHKey) + } + + // Mock successful JSON response + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `{ + "seq": 2, + "ssh_public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw== john.doe@example.com", + "encoded_key": "AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw==", + "algorithm": "ssh-rsa", + "comment": "john.doe@example.com", + "valid": true + }`) + }) + + ctx := context.Background() + sshKey := "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA0T...YImydZAw== john.doe@example.com" + + // Use testClient.Accounts instead of undefined Accounts variable + keyInfo, _, err := testClient.Accounts.AddSSHKey(ctx, "self", sshKey) + if err != nil { + t.Fatalf("AddSSHKey returned error: %v", err) + } + + // Verify SSH key information in the response + if keyInfo.SSHPublicKey != sshKey { + t.Errorf("Expected SSH key '%s', got '%s'", sshKey, keyInfo.SSHPublicKey) + } + + if keyInfo.Valid != true { + t.Errorf("Expected key validity to be true, got false") + } + + if keyInfo.Comment != "john.doe@example.com" { + t.Errorf("Expected comment 'john.doe@example.com', got '%s'", keyInfo.Comment) + } +} diff --git a/gerrit.go b/gerrit.go index 5dfa904..a757b3a 100644 --- a/gerrit.go +++ b/gerrit.go @@ -289,6 +289,34 @@ func (c *Client) NewRawPutRequest(ctx context.Context, urlStr string, body strin return req, nil } +// NewRawPostRequest creates a raw POST request and makes no attempt to encode +// or marshal the body. Just passes it straight through. +func (c *Client) NewRawPostRequest(ctx context.Context, urlStr string, body string) (*http.Request, error) { + // Build URL for request + u, err := c.buildURLForRequest(urlStr) + if err != nil { + return nil, err + } + + buf := bytes.NewBuffer([]byte(body)) + req, err := http.NewRequestWithContext(ctx, "POST", u, buf) + if err != nil { + return nil, err + } + + // Apply Authentication + if err := c.addAuthentication(ctx, req); err != nil { + return nil, err + } + + // Request compact JSON + // See https://gerrit-review.googlesource.com/Documentation/rest-api.html#output + req.Header.Add("Accept", "application/json") + req.Header.Add("Content-Type", "text/plain") + + return req, nil +} + // Call is a combine function for Client.NewRequest and Client.Do. // // Most API methods are quite the same. diff --git a/gerrit_test.go b/gerrit_test.go index d80cb34..3c73483 100644 --- a/gerrit_test.go +++ b/gerrit_test.go @@ -437,6 +437,38 @@ func TestNewRawPutRequest(t *testing.T) { } } +func TestNewRawPostRequest(t *testing.T) { + ctx := context.Background() + c, err := gerrit.NewClient(ctx, testGerritInstanceURL, nil) + if err != nil { + t.Errorf("An error occured. Expected nil. Got %+v.", err) + } + + inURL, outURL := "/foo", testGerritInstanceURL+"foo" + req, _ := c.NewRawPostRequest(ctx, inURL, "test raw POST contents") + + // Test that relative URL was expanded + if got, want := req.URL.String(), outURL; got != want { + t.Errorf("NewRequest(%q) URL is %v, want %v", inURL, got, want) + } + + // Test that HTTP method is POST + if got, want := req.Method, http.MethodPost; got != want { + t.Errorf("NewRawPostRequest method is %v, want %v", got, want) + } + + // Test that request body is passed correctly + body, _ := io.ReadAll(req.Body) + if got, want := string(body), "test raw POST contents"; got != want { + t.Errorf("NewRequest Body is %v, want %v", got, want) + } + + // Test that Content-Type is set correctly + if got, want := req.Header.Get("Content-Type"), "text/plain"; got != want { + t.Errorf("NewRawPostRequest Content-Type is %v, want %v", got, want) + } +} + func testURLParseError(t *testing.T, err error) { if err == nil { t.Errorf("Expected error to be returned")