Repository: hub4j/github-api
Pull request #1616 (2023-02-21 13:24:25)

Improve deploy key and  user key handling
NOTE: 
This is a copy of https://github.com/hub4j/github-api/pull/1593 made from a personal account to allow "Allow edits from maintainers". The code is similar - the commits have been rebased and squashed onto latest main though.


# Description

Added additional fields to deploykey object: [created_at, last_used, added_by, read_only](https://docs.github.com/en/rest/deploy-keys?apiVersion=2022-11-28#get-a-deploy-key).
Added methods to allow [adding](https://docs.github.com/en/rest/users/keys?apiVersion=2022-11-28#create-a-public-ssh-key-for-the-authenticated-user) / [deleting](https://docs.github.com/en/rest/users/keys?apiVersion=2022-11-28#delete-a-public-ssh-key-for-the-authenticated-user) public keys on a user.

# Before submitting a PR:

- [x] Changes must not break binary backwards compatibility. If you are unclear on how to make the change you think is needed while maintaining backward compatibility, [CONTRIBUTING.md](CONTRIBUTING.md) for details.
- [x] Add JavaDocs and other comments as appropriate. Consider including links in comments to relevant documentation on https://docs.github.com/en/rest .  
- [x] Add tests that cover any added or changed code. This generally requires capturing snapshot test data. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
- [x] Run `mvn -D enable-ci clean install site` locally. If this command doesn't succeed, your change will not pass CI.
- [x] Push your changes to a branch other than `main`. You will create your PR from that branch.

# When creating a PR: 

- [x] Fill in the "Description" above with clear summary of the changes. This includes:
  - [x] If this PR fixes one or more issues, include "Fixes #<issue number>" lines for each issue. 
  - [x] Provide links to relevant documentation on https://docs.github.com/en/rest where possible.
- [x] All lines of new code should be covered by tests as reported by code coverage. Any lines that are not covered must have PR comments explaining why they cannot be covered. For example, "Reaching this particular exception is hard and is not a particular common scenario."
- [x] Enable "Allow edits from maintainers".



Patch:
diff --git a/pom.xml b/pom.xml
index 1f13a867c..7af77ae42 100644
--- a/pom.xml
+++ b/pom.xml
@@ -453,6 +453,12 @@
       <version>${hamcrest.version}</version>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>com.github.npathai</groupId>
+      <artifactId>hamcrest-optional</artifactId>
+      <version>2.0.0</version>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>junit</groupId>
       <artifactId>junit</artifactId>
diff --git a/src/main/java/org/kohsuke/github/GHDeployKey.java b/src/main/java/org/kohsuke/github/GHDeployKey.java
index e49579ee0..2af3b9d7e 100644
--- a/src/main/java/org/kohsuke/github/GHDeployKey.java
+++ b/src/main/java/org/kohsuke/github/GHDeployKey.java
@@ -3,6 +3,7 @@ package org.kohsuke.github;
 import org.apache.commons.lang3.builder.ToStringBuilder;
 
 import java.io.IOException;
+import java.util.Date;
 
 // TODO: Auto-generated Javadoc
 /**
@@ -20,6 +21,18 @@ public class GHDeployKey {
     protected long id;
     private GHRepository owner;
 
+    /** Creation date of the deploy key */
+    private String created_at;
+
+    /** Last used date of the deploy key */
+    private String last_used;
+
+    /** Name of user that added the deploy key */
+    private String added_by;
+
+    /** Whether the deploykey has readonly permission or full access */
+    private boolean read_only;
+
     /**
      * Gets id.
      *
@@ -65,6 +78,42 @@ public class GHDeployKey {
         return verified;
     }
 
+    /**
+     * Gets created_at.
+     *
+     * @return the created_at
+     */
+    public Date getCreatedAt() {
+        return GitHubClient.parseDate(created_at);
+    }
+
+    /**
+     * Gets last_used.
+     *
+     * @return the last_used
+     */
+    public Date getLastUsedAt() {
+        return GitHubClient.parseDate(last_used);
+    }
+
+    /**
+     * Gets added_by
+     *
+     * @return the added_by
+     */
+    public String getAdded_by() {
+        return added_by;
+    }
+
+    /**
+     * Is read_only
+     *
+     * @return true if the key can only read. False if the key has write permission as well.
+     */
+    public boolean isRead_only() {
+        return read_only;
+    }
+
     /**
      * Wrap gh deploy key.
      *
@@ -95,7 +144,14 @@ public class GHDeployKey {
      * @return the string
      */
     public String toString() {
-        return new ToStringBuilder(this).append("title", title).append("id", id).append("key", key).toString();
+        return new ToStringBuilder(this).append("title", title)
+                .append("id", id)
+                .append("key", key)
+                .append("created_at", created_at)
+                .append("last_used", last_used)
+                .append("added_by", added_by)
+                .append("read_only", read_only)
+                .toString();
     }
 
     /**
diff --git a/src/main/java/org/kohsuke/github/GHKey.java b/src/main/java/org/kohsuke/github/GHKey.java
index a60717e66..01c73819b 100644
--- a/src/main/java/org/kohsuke/github/GHKey.java
+++ b/src/main/java/org/kohsuke/github/GHKey.java
@@ -3,6 +3,8 @@ package org.kohsuke.github;
 import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 import org.apache.commons.lang3.builder.ToStringBuilder;
 
+import java.io.IOException;
+
 // TODO: Auto-generated Javadoc
 /**
  * SSH public key.
@@ -74,4 +76,14 @@ public class GHKey extends GitHubInteractiveObject {
     public String toString() {
         return new ToStringBuilder(this).append("title", title).append("id", id).append("key", key).toString();
     }
+
+    /**
+     * Delete the GHKey
+     *
+     * @throws IOException
+     *             the io exception
+     */
+    public void delete() throws IOException {
+        root().createRequest().method("DELETE").withUrlPath(String.format("/user/keys/%d", id)).send();
+    }
 }
diff --git a/src/main/java/org/kohsuke/github/GHMyself.java b/src/main/java/org/kohsuke/github/GHMyself.java
index ea3dd5f3d..43914f2bd 100644
--- a/src/main/java/org/kohsuke/github/GHMyself.java
+++ b/src/main/java/org/kohsuke/github/GHMyself.java
@@ -83,6 +83,28 @@ public class GHMyself extends GHUser {
         return root().createRequest().withUrlPath("/user/keys").toIterable(GHKey[].class, null).toList();
     }
 
+    /**
+     * Add public SSH key for the user.
+     * <p>
+     * https://docs.github.com/en/rest/users/keys?apiVersion=2022-11-28#create-a-public-ssh-key-for-the-authenticated-user
+     *
+     * @param title
+     *            Title of the SSH key
+     * @param key
+     *            the public key
+     * @return the newly created Github key
+     * @throws IOException
+     *             the io exception
+     */
+    public GHKey addPublicKey(String title, String key) throws IOException {
+        return root().createRequest()
+                .withUrlPath("/user/keys")
+                .method("POST")
+                .with("title", title)
+                .with("key", key)
+                .fetch(GHKey.class);
+    }
+
     /**
      * Returns the read-only list of all the public verified keys of the current user.
      * <p>


Test patch:
diff --git a/src/test/java/org/kohsuke/github/ArchTests.java b/src/test/java/org/kohsuke/github/ArchTests.java
index 598b6383d..72a6b9c26 100644
--- a/src/test/java/org/kohsuke/github/ArchTests.java
+++ b/src/test/java/org/kohsuke/github/ArchTests.java
@@ -128,6 +128,7 @@ public class ArchTests {
                         targetMethodIs(ToStringBuilder.class, "append", String.class, Object.class),
                         targetMethodIs(ToStringBuilder.class, "append", String.class, long.class),
                         targetMethodIs(ToStringBuilder.class, "append", String.class, int.class),
+                        targetMethodIs(ToStringBuilder.class, "append", String.class, boolean.class),
                         targetMethodIs(ToStringBuilder.class, "isEmpty"),
                         targetMethodIs(ToStringBuilder.class, "equals"),
                         targetMethodIs(ToStringBuilder.class, "capitalize"),
diff --git a/src/test/java/org/kohsuke/github/GHDeployKeyTest.java b/src/test/java/org/kohsuke/github/GHDeployKeyTest.java
new file mode 100644
index 000000000..35d321888
--- /dev/null
+++ b/src/test/java/org/kohsuke/github/GHDeployKeyTest.java
@@ -0,0 +1,81 @@
+package org.kohsuke.github;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+
+import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresent;
+import static org.hamcrest.Matchers.*;
+
+/**
+ * The Class GHDeployKeyTest.
+ *
+ * @author Jonas van Vliet
+ */
+public class GHDeployKeyTest extends AbstractGitHubWireMockTest {
+    private static final String DEPLOY_KEY_TEST_REPO_NAME = "hub4j-test-org/GHDeployKeyTest";
+    private static final String ED_25519_READONLY = "DeployKey - ed25519 - readonly";
+    private static final String RSA_4096_READWRITE = "Deploykey - rsa4096 - readwrite";
+    private static final String KEY_CREATOR_USERNAME = "van-vliet";
+
+    /**
+     * Test get deploymentkeys.
+     *
+     * @throws IOException
+     *             Signals that an I/O exception has occurred.
+     */
+    @Test
+    public void testGetDeployKeys() throws IOException {
+        final GHRepository repo = getRepository();
+        final List<GHDeployKey> deployKeys = repo.getDeployKeys();
+        assertThat("There should be 2 deploykeys in " + DEPLOY_KEY_TEST_REPO_NAME, deployKeys, hasSize(2));
+
+        Optional<GHDeployKey> ed25519Key = deployKeys.stream()
+                .filter(key -> key.getTitle().equals(ED_25519_READONLY))
+                .findAny();
+        assertThat("The key exists", ed25519Key, isPresent());
+        assertThat("The key was created at the specified date",
+                ed25519Key.get().getCreatedAt(),
+                is(Date.from(Instant.parse("2023-02-08T10:00:15.00Z"))));
+        assertThat("The key is created by " + KEY_CREATOR_USERNAME,
+                ed25519Key.get().getAdded_by(),
+                is(KEY_CREATOR_USERNAME));
+        assertThat("The key has a last_used value",
+                ed25519Key.get().getLastUsedAt(),
+                is(Date.from(Instant.parse("2023-02-08T10:02:11.00Z"))));
+        assertThat("The key only has read access", ed25519Key.get().isRead_only(), is(true));
+        assertThat("Object has a toString()", ed25519Key.get().toString(), is(notNullValue()));
+
+        Optional<GHDeployKey> rsa_4096Key = deployKeys.stream()
+                .filter(key -> key.getTitle().equals(RSA_4096_READWRITE))
+                .findAny();
+        assertThat("The key exists", rsa_4096Key, isPresent());
+        assertThat("The key was created at the specified date",
+                rsa_4096Key.get().getCreatedAt(),
+                is(Date.from(Instant.parse("2023-01-26T14:12:12.00Z"))));
+        assertThat("The key is created by " + KEY_CREATOR_USERNAME,
+                rsa_4096Key.get().getAdded_by(),
+                is(KEY_CREATOR_USERNAME));
+        assertThat("The key has never been used", rsa_4096Key.get().getLastUsedAt(), is(nullValue()));
+        assertThat("The key only has read/write access", rsa_4096Key.get().isRead_only(), is(false));
+    }
+
+    /**
+     * Gets the repository.
+     *
+     * @return the repository
+     * @throws IOException
+     *             Signals that an I/O exception has occurred.
+     */
+    protected GHRepository getRepository() throws IOException {
+        return getRepository(gitHub);
+    }
+
+    private GHRepository getRepository(final GitHub gitHub) throws IOException {
+        return gitHub.getRepository(DEPLOY_KEY_TEST_REPO_NAME);
+    }
+}
diff --git a/src/test/java/org/kohsuke/github/GHPublicKeyTest.java b/src/test/java/org/kohsuke/github/GHPublicKeyTest.java
new file mode 100644
index 000000000..4bd505b16
--- /dev/null
+++ b/src/test/java/org/kohsuke/github/GHPublicKeyTest.java
@@ -0,0 +1,33 @@
+package org.kohsuke.github;
+
+import org.junit.Test;
+
+/**
+ * The Class GHPublicKeyTest.
+ *
+ * @author Jonas van Vliet
+ */
+public class GHPublicKeyTest extends AbstractGitHubWireMockTest {
+
+    private static final String TMP_KEY_NAME = "Temporary user key";
+    private static final String WIREMOCK_SSH_PUBLIC_KEY = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDepW2/BSVFM2AfuGGsvi+vjQzC0EBD3R+/7PNEvP0/nvTWxiC/tthfvvCJR6TKrsprCir5tiJFm73gX+K18W0RKYpkyg8H6d1eZu3q/JOiGvoDPeN8Oe9hOGeeexw1WOiz7ESPHzZYXI981evzHAzxxn8zibr2EryopVNsXyoenw==";
+
+    /**
+     * Test adding a public key to the user
+     *
+     * @throws Exception
+     *             the exception
+     */
+    @Test
+    public void testAddPublicKey() throws Exception {
+        GHKey newPublicKey = null;
+        try {
+            GHMyself me = gitHub.getMyself();
+            newPublicKey = me.addPublicKey(TMP_KEY_NAME, WIREMOCK_SSH_PUBLIC_KEY);
+        } finally {
+            if (newPublicKey != null) {
+                newPublicKey.delete();
+            }
+        }
+    }
+}
diff --git a/src/test/resources/org/kohsuke/github/GHDeployKeyTest/wiremock/testGetDeployKeys/__files/repos_hub4j-test-org_ghdeploykeytest-2.json b/src/test/resources/org/kohsuke/github/GHDeployKeyTest/wiremock/testGetDeployKeys/__files/repos_hub4j-test-org_ghdeploykeytest-2.json
new file mode 100644
index 000000000..cb09c0dd1
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/GHDeployKeyTest/wiremock/testGetDeployKeys/__files/repos_hub4j-test-org_ghdeploykeytest-2.json
@@ -0,0 +1,149 @@
+{
+  "id": 593636170,
+  "node_id": "R_kgDOI2IrSg",
+  "name": "GHDeployKeyTest",
+  "full_name": "hub4j-test-org/GHDeployKeyTest",
+  "private": false,
+  "owner": {
+    "login": "hub4j-test-org",
+    "id": 7544739,
+    "node_id": "MDEyOk9yZ2FuaXphdGlvbjc1NDQ3Mzk=",
+    "avatar_url": "https://avatars.githubusercontent.com/u/7544739?v=4",
+    "gravatar_id": "",
+    "url": "https://api.github.com/users/hub4j-test-org",
+    "html_url": "https://github.com/hub4j-test-org",
+    "followers_url": "https://api.github.com/users/hub4j-test-org/followers",
+    "following_url": "https://api.github.com/users/hub4j-test-org/following{/other_user}",
+    "gists_url": "https://api.github.com/users/hub4j-test-org/gists{/gist_id}",
+    "starred_url": "https://api.github.com/users/hub4j-test-org/starred{/owner}{/repo}",
+    "subscriptions_url": "https://api.github.com/users/hub4j-test-org/subscriptions",
+    "organizations_url": "https://api.github.com/users/hub4j-test-org/orgs",
+    "repos_url": "https://api.github.com/users/hub4j-test-org/repos",
+    "events_url": "https://api.github.com/users/hub4j-test-org/events{/privacy}",
+    "received_events_url": "https://api.github.com/users/hub4j-test-org/received_events",
+    "type": "Organization",
+    "site_admin": false
+  },
+  "html_url": "https://github.com/hub4j-test-org/GHDeployKeyTest",
+  "description": "Repository used by GHDeployKeyTest",
+  "fork": false,
+  "url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest",
+  "forks_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/forks",
+  "keys_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/keys{/key_id}",
+  "collaborators_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/collaborators{/collaborator}",
+  "teams_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/teams",
+  "hooks_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/hooks",
+  "issue_events_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/issues/events{/number}",
+  "events_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/events",
+  "assignees_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/assignees{/user}",
+  "branches_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/branches{/branch}",
+  "tags_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/tags",
+  "blobs_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/git/blobs{/sha}",
+  "git_tags_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/git/tags{/sha}",
+  "git_refs_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/git/refs{/sha}",
+  "trees_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/git/trees{/sha}",
+  "statuses_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/statuses/{sha}",
+  "languages_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/languages",
+  "stargazers_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/stargazers",
+  "contributors_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/contributors",
+  "subscribers_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/subscribers",
+  "subscription_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/subscription",
+  "commits_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/commits{/sha}",
+  "git_commits_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/git/commits{/sha}",
+  "comments_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/comments{/number}",
+  "issue_comment_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/issues/comments{/number}",
+  "contents_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/contents/{+path}",
+  "compare_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/compare/{base}...{head}",
+  "merges_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/merges",
+  "archive_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/{archive_format}{/ref}",
+  "downloads_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/downloads",
+  "issues_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/issues{/number}",
+  "pulls_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/pulls{/number}",
+  "milestones_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/milestones{/number}",
+  "notifications_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/notifications{?since,all,participating}",
+  "labels_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/labels{/name}",
+  "releases_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/releases{/id}",
+  "deployments_url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/deployments",
+  "created_at": "2023-01-26T13:58:23Z",
+  "updated_at": "2023-01-26T13:58:23Z",
+  "pushed_at": "2023-01-26T13:58:25Z",
+  "git_url": "git://github.com/hub4j-test-org/GHDeployKeyTest.git",
+  "ssh_url": "git@github.com:hub4j-test-org/GHDeployKeyTest.git",
+  "clone_url": "https://github.com/hub4j-test-org/GHDeployKeyTest.git",
+  "svn_url": "https://github.com/hub4j-test-org/GHDeployKeyTest",
+  "homepage": null,
+  "size": 0,
+  "stargazers_count": 0,
+  "watchers_count": 0,
+  "language": null,
+  "has_issues": true,
+  "has_projects": true,
+  "has_downloads": true,
+  "has_wiki": true,
+  "has_pages": false,
+  "has_discussions": false,
+  "forks_count": 0,
+  "mirror_url": null,
+  "archived": false,
+  "disabled": false,
+  "open_issues_count": 0,
+  "license": null,
+  "allow_forking": true,
+  "is_template": false,
+  "web_commit_signoff_required": false,
+  "topics": [],
+  "visibility": "public",
+  "forks": 0,
+  "open_issues": 0,
+  "watchers": 0,
+  "default_branch": "main",
+  "permissions": {
+    "admin": true,
+    "maintain": true,
+    "push": true,
+    "triage": true,
+    "pull": true
+  },
+  "temp_clone_token": "",
+  "allow_squash_merge": true,
+  "allow_merge_commit": true,
+  "allow_rebase_merge": true,
+  "allow_auto_merge": false,
+  "delete_branch_on_merge": false,
+  "allow_update_branch": false,
+  "use_squash_pr_title_as_default": false,
+  "squash_merge_commit_message": "COMMIT_MESSAGES",
+  "squash_merge_commit_title": "COMMIT_OR_PR_TITLE",
+  "merge_commit_message": "PR_TITLE",
+  "merge_commit_title": "MERGE_MESSAGE",
+  "organization": {
+    "login": "hub4j-test-org",
+    "id": 7544739,
+    "node_id": "MDEyOk9yZ2FuaXphdGlvbjc1NDQ3Mzk=",
+    "avatar_url": "https://avatars.githubusercontent.com/u/7544739?v=4",
+    "gravatar_id": "",
+    "url": "https://api.github.com/users/hub4j-test-org",
+    "html_url": "https://github.com/hub4j-test-org",
+    "followers_url": "https://api.github.com/users/hub4j-test-org/followers",
+    "following_url": "https://api.github.com/users/hub4j-test-org/following{/other_user}",
+    "gists_url": "https://api.github.com/users/hub4j-test-org/gists{/gist_id}",
+    "starred_url": "https://api.github.com/users/hub4j-test-org/starred{/owner}{/repo}",
+    "subscriptions_url": "https://api.github.com/users/hub4j-test-org/subscriptions",
+    "organizations_url": "https://api.github.com/users/hub4j-test-org/orgs",
+    "repos_url": "https://api.github.com/users/hub4j-test-org/repos",
+    "events_url": "https://api.github.com/users/hub4j-test-org/events{/privacy}",
+    "received_events_url": "https://api.github.com/users/hub4j-test-org/received_events",
+    "type": "Organization",
+    "site_admin": false
+  },
+  "security_and_analysis": {
+    "secret_scanning": {
+      "status": "disabled"
+    },
+    "secret_scanning_push_protection": {
+      "status": "disabled"
+    }
+  },
+  "network_count": 0,
+  "subscribers_count": 16
+}
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/GHDeployKeyTest/wiremock/testGetDeployKeys/__files/repos_hub4j-test-org_ghdeploykeytest_keys-3.json b/src/test/resources/org/kohsuke/github/GHDeployKeyTest/wiremock/testGetDeployKeys/__files/repos_hub4j-test-org_ghdeploykeytest_keys-3.json
new file mode 100644
index 000000000..95120e92c
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/GHDeployKeyTest/wiremock/testGetDeployKeys/__files/repos_hub4j-test-org_ghdeploykeytest_keys-3.json
@@ -0,0 +1,24 @@
+[
+  {
+    "id": 76924278,
+    "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDBO4AyE/CjnMt0YeJ4QFnU87qr4u534Q5FHRZmTUP7Ixi/ks6iMVJAU71GltuP1NIUynOTTA+Y+XNHfCp+/wNCi6B6o63Ouk2Debx7cf3ufFXM7G48iTG3ULK2qfPzm8OkfRP4urg+AmdrQAUuH3Wx3CinqlGt8phJECsRkQqdoYoikOrRtx9iNx0Cz45xD4JyRBOjZix0kZTqQy8LKnpxXA8nIzJJoh9eYTOPT3Pf3X3qeOUpN2QTSH7ya9xvE+lCxEMqP6m/vnA1tm5Vzso53bU/pG/SZwNESpafD9Dkry8AiCg9zfzFnzikwWHhrPRqCjIwYuMdaXbBtQBknPx62nDkpS/b0iFvdIoRmWTNbTng38urH5lFRLE7y13WW1GWYPeiSF3zY6B0B+rdNJm2RNrZtvtbQ0e3ruWVR8ZIVtQysT0/ueGO0g2pmodY0lB2Z6EidOx4F46i8mitUgABwhnlrzfki/nO/K0voIUh48D5M8tJsR7kCj+G0e1zXoTl7OF7/g8br8zYlSDczIZBgjORT+XBOr5My6cbSFYRratR2cy1ZB7XpcolCp89mPY3MTQOTOrNIE4BhoJHS6ws7rIv/ZNQeW+4lktPq0x59Tdcv5vgYAdAiu3qpjTcaVxxkAVE8AxNz237+CRs4+jWCsGHl2BAHCEtM9KVqLR77w==",
+    "url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/keys/76924278",
+    "title": "Deploykey - rsa4096 - readwrite",
+    "verified": true,
+    "created_at": "2023-01-26T14:12:12Z",
+    "read_only": false,
+    "last_used": null,
+    "added_by": "van-vliet"
+  },
+  {
+    "id": 77542797,
+    "key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIINaSsehgTpibnTTtH4kKohPhwfzqo0OvfwP4w8qZfPp",
+    "url": "https://api.github.com/repos/hub4j-test-org/GHDeployKeyTest/keys/77542797",
+    "title": "DeployKey - ed25519 - readonly",
+    "verified": true,
+    "created_at": "2023-02-08T10:00:15Z",
+    "read_only": true,
+    "last_used": "2023-02-08T10:02:11Z",
+    "added_by": "van-vliet"
+  }
+]
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/GHDeployKeyTest/wiremock/testGetDeployKeys/__files/user-1.json b/src/test/resources/org/kohsuke/github/GHDeployKeyTest/wiremock/testGetDeployKeys/__files/user-1.json
new file mode 100644
index 000000000..c47cc3780
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/GHDeployKeyTest/wiremock/testGetDeployKeys/__files/user-1.json
@@ -0,0 +1,46 @@
+{
+  "login": "van-vliet",
+  "id": 11989400,
+  "node_id": "MDQ6VXNlcjExOTg5NDAw",
+  "avatar_url": "https://avatars.githubusercontent.com/u/11989400?v=4",
+  "gravatar_id": "",
+  "url": "https://api.github.com/users/van-vliet",
+  "html_url": "https://github.com/van-vliet",
+  "followers_url": "https://api.github.com/users/van-vliet/followers",
+  "following_url": "https://api.github.com/users/van-vliet/following{/other_user}",
+  "gists_url": "https://api.github.com/users/van-vliet/gists{/gist_id}",
+  "starred_url": "https://api.github.com/users/van-vliet/starred{/owner}{/repo}",
+  "subscriptions_url": "https://api.github.com/users/van-vliet/subscriptions",
+  "organizations_url": "https://api.github.com/users/van-vliet/orgs",
+  "repos_url": "https://api.github.com/users/van-vliet/repos",
+  "events_url": "https://api.github.com/users/van-vliet/events{/privacy}",
+  "received_events_url": "https://api.github.com/users/van-vliet/received_events",
+  "type": "User",
+  "site_admin": false,
+  "name": "Jonas van Vliet",
+  "company": null,
+  "blog": "",
+  "location": null,
+  "email": null,
+  "hireable": null,
+  "bio": null,
+  "twitter_username": null,
+  "public_repos": 1,
+  "public_gists": 0,
+  "followers": 1,
+  "following": 1,
+  "created_at": "2015-04-17T05:56:56Z",
+  "updated_at": "2022-11-04T06:44:42Z",
+  "private_gists": 0,
+  "total_private_repos": 0,
+  "owned_private_repos": 0,
+  "disk_usage": 0,
+  "collaborators": 0,
+  "two_factor_authentication": true,
+  "plan": {
+    "name": "free",
+    "space": 976562499,
+    "collaborators": 0,
+    "private_repos": 10000
+  }
+}
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/GHDeployKeyTest/wiremock/testGetDeployKeys/mappings/repos_hub4j-test-org_ghdeploykeytest-2.json b/src/test/resources/org/kohsuke/github/GHDeployKeyTest/wiremock/testGetDeployKeys/mappings/repos_hub4j-test-org_ghdeploykeytest-2.json
new file mode 100644
index 000000000..a3a920349
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/GHDeployKeyTest/wiremock/testGetDeployKeys/mappings/repos_hub4j-test-org_ghdeploykeytest-2.json
@@ -0,0 +1,51 @@
+{
+  "id": "86196b46-7963-4ade-8331-f6b8677ebb56",
+  "name": "repos_hub4j-test-org_ghdeploykeytest",
+  "request": {
+    "url": "/repos/hub4j-test-org/GHDeployKeyTest",
+    "method": "GET",
+    "headers": {
+      "Accept": {
+        "equalTo": "application/vnd.github.v3+json"
+      }
+    }
+  },
+  "response": {
+    "status": 200,
+    "bodyFileName": "repos_hub4j-test-org_ghdeploykeytest-2.json",
+    "headers": {
+      "Server": "GitHub.com",
+      "Date": "Wed, 08 Feb 2023 10:16:06 GMT",
+      "Content-Type": "application/json; charset=utf-8",
+      "Cache-Control": "private, max-age=60, s-maxage=60",
+      "Vary": [
+        "Accept, Authorization, Cookie, X-GitHub-OTP",
+        "Accept-Encoding, Accept, X-Requested-With"
+      ],
+      "ETag": "W/\"ccb5e13d45de3c0541c6be0f53bb4bffb08cf1dcb916a2eeb417a2d162181d25\"",
+      "Last-Modified": "Thu, 26 Jan 2023 13:58:23 GMT",
+      "X-OAuth-Scopes": "read:user, repo",
+      "X-Accepted-OAuth-Scopes": "repo",
+      "github-authentication-token-expiration": "2023-02-15 10:03:43 UTC",
+      "X-GitHub-Media-Type": "github.v3; format=json",
+      "x-github-api-version-selected": "2022-11-28",
+      "X-RateLimit-Limit": "5000",
+      "X-RateLimit-Remaining": "4983",
+      "X-RateLimit-Reset": "1675854466",
+      "X-RateLimit-Used": "17",
+      "X-RateLimit-Resource": "core",
+      "Access-Control-Expose-Headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset",
+      "Access-Control-Allow-Origin": "*",
+      "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
+      "X-Frame-Options": "deny",
+      "X-Content-Type-Options": "nosniff",
+      "X-XSS-Protection": "0",
+      "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
+      "Content-Security-Policy": "default-src 'none'",
+      "X-GitHub-Request-Id": "E1B8:4352:46E1A3:481D53:63E37666"
+    }
+  },
+  "uuid": "86196b46-7963-4ade-8331-f6b8677ebb56",
+  "persistent": true,
+  "insertionIndex": 2
+}
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/GHDeployKeyTest/wiremock/testGetDeployKeys/mappings/repos_hub4j-test-org_ghdeploykeytest_keys-3.json b/src/test/resources/org/kohsuke/github/GHDeployKeyTest/wiremock/testGetDeployKeys/mappings/repos_hub4j-test-org_ghdeploykeytest_keys-3.json
new file mode 100644
index 000000000..6854466d6
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/GHDeployKeyTest/wiremock/testGetDeployKeys/mappings/repos_hub4j-test-org_ghdeploykeytest_keys-3.json
@@ -0,0 +1,50 @@
+{
+  "id": "49bb1666-7758-49de-9134-f0735a9a235d",
+  "name": "repos_hub4j-test-org_ghdeploykeytest_keys",
+  "request": {
+    "url": "/repos/hub4j-test-org/GHDeployKeyTest/keys",
+    "method": "GET",
+    "headers": {
+      "Accept": {
+        "equalTo": "application/vnd.github.v3+json"
+      }
+    }
+  },
+  "response": {
+    "status": 200,
+    "bodyFileName": "repos_hub4j-test-org_ghdeploykeytest_keys-3.json",
+    "headers": {
+      "Server": "GitHub.com",
+      "Date": "Wed, 08 Feb 2023 10:16:07 GMT",
+      "Content-Type": "application/json; charset=utf-8",
+      "Cache-Control": "private, max-age=60, s-maxage=60",
+      "Vary": [
+        "Accept, Authorization, Cookie, X-GitHub-OTP",
+        "Accept-Encoding, Accept, X-Requested-With"
+      ],
+      "ETag": "W/\"9bc942dbeacfdba2d3174c5da85d7e37a3d600068bea67c0e8e789201c7a9c90\"",
+      "X-OAuth-Scopes": "read:user, repo",
+      "X-Accepted-OAuth-Scopes": "",
+      "github-authentication-token-expiration": "2023-02-15 10:03:43 UTC",
+      "X-GitHub-Media-Type": "github.v3; format=json",
+      "x-github-api-version-selected": "2022-11-28",
+      "X-RateLimit-Limit": "5000",
+      "X-RateLimit-Remaining": "4982",
+      "X-RateLimit-Reset": "1675854466",
+      "X-RateLimit-Used": "18",
+      "X-RateLimit-Resource": "core",
+      "Access-Control-Expose-Headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset",
+      "Access-Control-Allow-Origin": "*",
+      "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
+      "X-Frame-Options": "deny",
+      "X-Content-Type-Options": "nosniff",
+      "X-XSS-Protection": "0",
+      "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
+      "Content-Security-Policy": "default-src 'none'",
+      "X-GitHub-Request-Id": "E1B9:672E:49A60B:4AE64E:63E37667"
+    }
+  },
+  "uuid": "49bb1666-7758-49de-9134-f0735a9a235d",
+  "persistent": true,
+  "insertionIndex": 3
+}
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/GHDeployKeyTest/wiremock/testGetDeployKeys/mappings/user-1.json b/src/test/resources/org/kohsuke/github/GHDeployKeyTest/wiremock/testGetDeployKeys/mappings/user-1.json
new file mode 100644
index 000000000..fb3aea41e
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/GHDeployKeyTest/wiremock/testGetDeployKeys/mappings/user-1.json
@@ -0,0 +1,51 @@
+{
+  "id": "ffe193ff-1772-4bdb-a87d-f09d73a55e9c",
+  "name": "user",
+  "request": {
+    "url": "/user",
+    "method": "GET",
+    "headers": {
+      "Accept": {
+        "equalTo": "application/vnd.github.v3+json"
+      }
+    }
+  },
+  "response": {
+    "status": 200,
+    "bodyFileName": "user-1.json",
+    "headers": {
+      "Server": "GitHub.com",
+      "Date": "Wed, 08 Feb 2023 10:16:06 GMT",
+      "Content-Type": "application/json; charset=utf-8",
+      "Cache-Control": "private, max-age=60, s-maxage=60",
+      "Vary": [
+        "Accept, Authorization, Cookie, X-GitHub-OTP",
+        "Accept-Encoding, Accept, X-Requested-With"
+      ],
+      "ETag": "W/\"d886bab2c0d06ed3ad08a6e1296c04c22f9a975d735ded6a05ba24cf69cee315\"",
+      "Last-Modified": "Fri, 04 Nov 2022 06:44:42 GMT",
+      "X-OAuth-Scopes": "read:user, repo",
+      "X-Accepted-OAuth-Scopes": "",
+      "github-authentication-token-expiration": "2023-02-15 10:03:43 UTC",
+      "X-GitHub-Media-Type": "github.v3; format=json",
+      "x-github-api-version-selected": "2022-11-28",
+      "X-RateLimit-Limit": "5000",
+      "X-RateLimit-Remaining": "4985",
+      "X-RateLimit-Reset": "1675854466",
+      "X-RateLimit-Used": "15",
+      "X-RateLimit-Resource": "core",
+      "Access-Control-Expose-Headers": "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset",
+      "Access-Control-Allow-Origin": "*",
+      "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
+      "X-Frame-Options": "deny",
+      "X-Content-Type-Options": "nosniff",
+      "X-XSS-Protection": "0",
+      "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
+      "Content-Security-Policy": "default-src 'none'",
+      "X-GitHub-Request-Id": "E1B6:4092:490FCD:4A4F75:63E37665"
+    }
+  },
+  "uuid": "ffe193ff-1772-4bdb-a87d-f09d73a55e9c",
+  "persistent": true,
+  "insertionIndex": 1
+}
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/GHPublicKeyTest/wiremock/testAddPublicKey/__files/user-1.json b/src/test/resources/org/kohsuke/github/GHPublicKeyTest/wiremock/testAddPublicKey/__files/user-1.json
new file mode 100644
index 000000000..3e646ff91
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/GHPublicKeyTest/wiremock/testAddPublicKey/__files/user-1.json
@@ -0,0 +1,46 @@
+{
+  "login": "jvv-test-2",
+  "id": 123935167,
+  "node_id": "U_kgDOB2MZvw",
+  "avatar_url": "https://avatars.githubusercontent.com/u/123935167?v=4",
+  "gravatar_id": "",
+  "url": "https://api.github.com/users/jvv-test-2",
+  "html_url": "https://github.com/jvv-test-2",
+  "followers_url": "https://api.github.com/users/jvv-test-2/followers",
+  "following_url": "https://api.github.com/users/jvv-test-2/following{/other_user}",
+  "gists_url": "https://api.github.com/users/jvv-test-2/gists{/gist_id}",
+  "starred_url": "https://api.github.com/users/jvv-test-2/starred{/owner}{/repo}",
+  "subscriptions_url": "https://api.github.com/users/jvv-test-2/subscriptions",
+  "organizations_url": "https://api.github.com/users/jvv-test-2/orgs",
+  "repos_url": "https://api.github.com/users/jvv-test-2/repos",
+  "events_url": "https://api.github.com/users/jvv-test-2/events{/privacy}",
+  "received_events_url": "https://api.github.com/users/jvv-test-2/received_events",
+  "type": "User",
+  "site_admin": false,
+  "name": null,
+  "company": null,
+  "blog": "",
+  "location": null,
+  "email": null,
+  "hireable": null,
+  "bio": null,
+  "twitter_username": null,
+  "public_repos": 0,
+  "public_gists": 0,
+  "followers": 0,
+  "following": 0,
+  "created_at": "2023-01-30T08:21:43Z",
+  "updated_at": "2023-01-30T08:21:43Z",
+  "private_gists": 0,
+  "total_private_repos": 0,
+  "owned_private_repos": 0,
+  "disk_usage": 0,
+  "collaborators": 0,
+  "two_factor_authentication": false,
+  "plan": {
+    "name": "free",
+    "space": 976562499,
+    "collaborators": 0,
+    "private_repos": 10000
+  }
+}
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/GHPublicKeyTest/wiremock/testAddPublicKey/__files/user_keys-2.json b/src/test/resources/org/kohsuke/github/GHPublicKeyTest/wiremock/testAddPublicKey/__files/user_keys-2.json
new file mode 100644
index 000000000..e9656f379
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/GHPublicKeyTest/wiremock/testAddPublicKey/__files/user_keys-2.json
@@ -0,0 +1,9 @@
+{
+  "id": 77080429,
+  "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDepW2/BSVFM2AfuGGsvi+vjQzC0EBD3R+/7PNEvP0/nvTWxiC/tthfvvCJR6TKrsprCir5tiJFm73gX+K18W0RKYpkyg8H6d1eZu3q/JOiGvoDPeN8Oe9hOGeeexw1WOiz7ESPHzZYXI981evzHAzxxn8zibr2EryopVNsXyoenw==",
+  "url": "https://api.github.com/user/keys/77080429",
+  "title": "Temporary user key",
+  "verified": true,
+  "created_at": "2023-01-30T09:31:56Z",
+  "read_only": false
+}
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/GHPublicKeyTest/wiremock/testAddPublicKey/mappings/user-1.json b/src/test/resources/org/kohsuke/github/GHPublicKeyTest/wiremock/testAddPublicKey/mappings/user-1.json
new file mode 100644
index 000000000..e79a4076e
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/GHPublicKeyTest/wiremock/testAddPublicKey/mappings/user-1.json
@@ -0,0 +1,49 @@
+{
+  "id": "67a88087-3119-4e65-8d1e-b642d1e59c4a",
+  "name": "user",
+  "request": {
+    "url": "/user",
+    "method": "GET",
+    "headers": {
+      "Accept": {
+        "equalTo": "application/vnd.github.v3+json"
+      }
+    }
+  },
+  "response": {
+    "status": 200,
+    "bodyFileName": "user-1.json",
+    "headers": {
+      "Server": "GitHub.com",
+      "Date": "Mon, 30 Jan 2023 09:31:55 GMT",
+      "Content-Type": "application/json; charset=utf-8",
+      "Cache-Control": "private, max-age=60, s-maxage=60",
+      "Vary": [
+        "Accept, Authorization, Cookie, X-GitHub-OTP",
+        "Accept-Encoding, Accept, X-Requested-With"
+      ],
+      "ETag": "W/\"f9a4696e8778e5018e61274cef95759771cfa9dee3c39b931513a562cf779e80\"",
+      "Last-Modified": "Mon, 30 Jan 2023 08:21:43 GMT",
+      "X-OAuth-Scopes": "admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, admin:ssh_signing_key, delete:packages, delete_repo, gist, notifications, project, repo, user, workflow, write:discussion, write:packages",
+      "X-Accepted-OAuth-Scopes": "",
+      "github-authentication-token-expiration": "2023-03-01 08:23:08 UTC",
+      "X-GitHub-Media-Type": "github.v3; format=json",
+      "x-github-api-version-selected": "2022-11-28",
+      "X-RateLimit-Limit": "5000",
+      "X-RateLimit-Remaining": "4999",
+      "X-RateLimit-Reset": "1675074715",
+      "X-RateLimit-Used": "1",
+      "X-RateLimit-Resource": "core",
+      "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
+      "X-Frame-Options": "deny",
+      "X-Content-Type-Options": "nosniff",
+      "X-XSS-Protection": "0",
+      "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
+      "Content-Security-Policy": "default-src 'none'",
+      "X-GitHub-Request-Id": "CAB6:8706:A7B3FF:AAB6FC:63D78E8B"
+    }
+  },
+  "uuid": "67a88087-3119-4e65-8d1e-b642d1e59c4a",
+  "persistent": true,
+  "insertionIndex": 1
+}
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/GHPublicKeyTest/wiremock/testAddPublicKey/mappings/user_keys-2.json b/src/test/resources/org/kohsuke/github/GHPublicKeyTest/wiremock/testAddPublicKey/mappings/user_keys-2.json
new file mode 100644
index 000000000..4ef079a7f
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/GHPublicKeyTest/wiremock/testAddPublicKey/mappings/user_keys-2.json
@@ -0,0 +1,56 @@
+{
+  "id": "2a67008e-cf5e-4b4c-894a-878f6c718a05",
+  "name": "user_keys",
+  "request": {
+    "url": "/user/keys",
+    "method": "POST",
+    "headers": {
+      "Accept": {
+        "equalTo": "application/vnd.github.v3+json"
+      }
+    },
+    "bodyPatterns": [
+      {
+        "equalToJson": "{\"title\":\"Temporary user key\",\"key\":\"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQDepW2/BSVFM2AfuGGsvi+vjQzC0EBD3R+/7PNEvP0/nvTWxiC/tthfvvCJR6TKrsprCir5tiJFm73gX+K18W0RKYpkyg8H6d1eZu3q/JOiGvoDPeN8Oe9hOGeeexw1WOiz7ESPHzZYXI981evzHAzxxn8zibr2EryopVNsXyoenw==\"}",
+        "ignoreArrayOrder": true,
+        "ignoreExtraElements": false
+      }
+    ]
+  },
+  "response": {
+    "status": 201,
+    "bodyFileName": "user_keys-2.json",
+    "headers": {
+      "Server": "GitHub.com",
+      "Date": "Mon, 30 Jan 2023 09:31:56 GMT",
+      "Content-Type": "application/json; charset=utf-8",
+      "Cache-Control": "private, max-age=60, s-maxage=60",
+      "Vary": [
+        "Accept, Authorization, Cookie, X-GitHub-OTP",
+        "Accept-Encoding, Accept, X-Requested-With"
+      ],
+      "ETag": "\"6510ce8437cc68cf299fe8f5209abc961b86e6b4d35dfa4ae626ce43b32b2580\"",
+      "X-OAuth-Scopes": "admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, admin:ssh_signing_key, delete:packages, delete_repo, gist, notifications, project, repo, user, workflow, write:discussion, write:packages",
+      "X-Accepted-OAuth-Scopes": "admin:public_key, write:public_key",
+      "github-authentication-token-expiration": "2023-03-01 08:23:08 UTC",
+      "X-GitHub-Media-Type": "github.v3; format=json",
+      "x-github-api-version-selected": "2022-11-28",
+      "X-RateLimit-Limit": "5000",
+      "X-RateLimit-Remaining": "4997",
+      "X-RateLimit-Reset": "1675074715",
+      "X-RateLimit-Used": "3",
+      "X-RateLimit-Resource": "core",
+      "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
+      "X-Frame-Options": "deny",
+      "X-Content-Type-Options": "nosniff",
+      "X-XSS-Protection": "0",
+      "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
+      "Content-Security-Policy": "default-src 'none'",
+      "X-GitHub-Request-Id": "CAB8:5F74:B9E56B:BCE687:63D78E8B",
+      "Location": "https://api.github.com/user/keys/77080429"
+    }
+  },
+  "uuid": "2a67008e-cf5e-4b4c-894a-878f6c718a05",
+  "persistent": true,
+  "insertionIndex": 2
+}
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/GHPublicKeyTest/wiremock/testAddPublicKey/mappings/user_keys_77080429-3.json b/src/test/resources/org/kohsuke/github/GHPublicKeyTest/wiremock/testAddPublicKey/mappings/user_keys_77080429-3.json
new file mode 100644
index 000000000..2ea399ed4
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/GHPublicKeyTest/wiremock/testAddPublicKey/mappings/user_keys_77080429-3.json
@@ -0,0 +1,41 @@
+{
+  "id": "d91a5200-ed4b-4145-be9f-fece79f6f92d",
+  "name": "user_keys_77080429",
+  "request": {
+    "url": "/user/keys/77080429",
+    "method": "DELETE",
+    "headers": {
+      "Accept": {
+        "equalTo": "application/vnd.github.v3+json"
+      }
+    }
+  },
+  "response": {
+    "status": 204,
+    "headers": {
+      "Server": "GitHub.com",
+      "Date": "Mon, 30 Jan 2023 09:31:56 GMT",
+      "X-OAuth-Scopes": "admin:gpg_key, admin:org, admin:org_hook, admin:public_key, admin:repo_hook, admin:ssh_signing_key, delete:packages, delete_repo, gist, notifications, project, repo, user, workflow, write:discussion, write:packages",
+      "X-Accepted-OAuth-Scopes": "admin:public_key",
+      "github-authentication-token-expiration": "2023-03-01 08:23:08 UTC",
+      "X-GitHub-Media-Type": "github.v3; format=json",
+      "x-github-api-version-selected": "2022-11-28",
+      "X-RateLimit-Limit": "5000",
+      "X-RateLimit-Remaining": "4996",
+      "X-RateLimit-Reset": "1675074715",
+      "X-RateLimit-Used": "4",
+      "X-RateLimit-Resource": "core",
+      "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
+      "X-Frame-Options": "deny",
+      "X-Content-Type-Options": "nosniff",
+      "X-XSS-Protection": "0",
+      "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
+      "Content-Security-Policy": "default-src 'none'",
+      "Vary": "Accept-Encoding, Accept, X-Requested-With",
+      "X-GitHub-Request-Id": "CAB9:B29F:A4B977:A7B346:63D78E8C"
+    }
+  },
+  "uuid": "d91a5200-ed4b-4145-be9f-fece79f6f92d",
+  "persistent": true,
+  "insertionIndex": 3
+}
\ No newline at end of file