Skip to content

Commit

Permalink
[FEAT]: Create a GitHub App from a manifest
Browse files Browse the repository at this point in the history
* Create a GitHub App from a manifest

* Add missing InstallationPermissions

* observable and tests

* Remove ManualRoute on Observable route

---------

Co-authored-by: Nick Floyd <139819+nickfloyd@users.noreply.github.com>
  • Loading branch information
colbylwilliams and nickfloyd committed Jun 24, 2024
1 parent b208057 commit c2aee1a
Show file tree
Hide file tree
Showing 11 changed files with 282 additions and 13 deletions.
8 changes: 8 additions & 0 deletions Octokit.Reactive/Clients/IObservableGitHubAppsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -110,5 +110,13 @@ public interface IObservableGitHubAppsClient
/// <remarks>https://developer.github.com/v3/apps/#find-user-installation</remarks>
/// <param name="user">The name of the user</param>
IObservable<Installation> GetUserInstallationForCurrent(string user);

/// <summary>
/// Creates a GitHub app by completing the handshake necessary when implementing the GitHub App Manifest flow.
/// https://docs.github.com/apps/sharing-github-apps/registering-a-github-app-from-a-manifest
/// </summary>
/// <remarks>https://docs.github.com/rest/apps/apps#create-a-github-app-from-a-manifest</remarks>
/// <param name="code">Temporary code in a code parameter.</param>
IObservable<GitHubAppFromManifest> CreateAppFromManifest(string code);
}
}
13 changes: 13 additions & 0 deletions Octokit.Reactive/Clients/ObservableGitHubAppsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,5 +175,18 @@ public IObservable<Installation> GetUserInstallationForCurrent(string user)

return _client.GetUserInstallationForCurrent(user).ToObservable();
}

/// <summary>
/// Creates a GitHub app by completing the handshake necessary when implementing the GitHub App Manifest flow.
/// https://docs.github.com/apps/sharing-github-apps/registering-a-github-app-from-a-manifest
/// </summary>
/// <remarks>https://docs.github.com/rest/apps/apps#create-a-github-app-from-a-manifest</remarks>
/// <param name="code">Temporary code in a code parameter.</param>
public IObservable<GitHubAppFromManifest> CreateAppFromManifest(string code)
{
Ensure.ArgumentNotNullOrEmptyString(code, nameof(code));

return _client.CreateAppFromManifest(code).ToObservable();
}
}
}
32 changes: 32 additions & 0 deletions Octokit.Tests/Clients/GitHubAppsClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -273,5 +273,37 @@ public void GetsFromCorrectUrl()
connection.Received().Get<Installation>(Arg.Is<Uri>(u => u.ToString() == "users/ducks/installation"), null);
}
}

public class TheCreateAppFromManifestMethod
{
[Fact]
public async Task EnsuresNonNullArguments()
{
var connection = Substitute.For<IApiConnection>();
var client = new GitHubAppsClient(connection);

await Assert.ThrowsAsync<ArgumentNullException>(() => client.CreateAppFromManifest(null));
}

[Fact]
public async Task EnsuresNonEmptyArguments()
{
var connection = Substitute.For<IApiConnection>();
var client = new GitHubAppsClient(connection);

await Assert.ThrowsAsync<ArgumentException>(() => client.CreateAppFromManifest(""));
}

[Fact]
public void PostsFromCorrectUrl()
{
var connection = Substitute.For<IApiConnection>();
var client = new GitHubAppsClient(connection);

client.CreateAppFromManifest("abc123");

connection.Received().Post<GitHubAppFromManifest>(Arg.Is<Uri>(u => u.ToString() == "app-manifests/abc123/conversions"));
}
}
}
}
81 changes: 81 additions & 0 deletions Octokit.Tests/Models/GitHubAppFromManifestTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using Octokit.Internal;
using Xunit;

namespace Octokit.Tests.Models
{
public class GitHubAppFromManifestTests
{
[Fact]
public void CanBeDeserialized()
{
// from https://docs.github.com/rest/apps/apps#create-a-github-app-from-a-manifest
const string json = @"{
""id"": 1,
""slug"": ""octoapp"",
""node_id"": ""MDxOkludGVncmF0aW9uMQ=="",
""owner"": {
""login"": ""github"",
""id"": 1,
""node_id"": ""MDEyOk9yZ2FuaXphdGlvbjE="",
""url"": ""https://api.github.com/orgs/github"",
""repos_url"": ""https://api.github.com/orgs/github/repos"",
""events_url"": ""https://api.github.com/orgs/github/events"",
""avatar_url"": ""https://github.com/images/error/octocat_happy.gif"",
""gravatar_id"": """",
""html_url"": ""https://github.com/octocat"",
""followers_url"": ""https://api.github.com/users/octocat/followers"",
""following_url"": ""https://api.github.com/users/octocat/following{/other_user}"",
""gists_url"": ""https://api.github.com/users/octocat/gists{/gist_id}"",
""starred_url"": ""https://api.github.com/users/octocat/starred{/owner}{/repo}"",
""subscriptions_url"": ""https://api.github.com/users/octocat/subscriptions"",
""organizations_url"": ""https://api.github.com/users/octocat/orgs"",
""received_events_url"": ""https://api.github.com/users/octocat/received_events"",
""type"": ""User"",
""site_admin"": true
},
""name"": ""Octocat App"",
""description"": ""A short description of the app"",
""external_url"": ""https://example.com"",
""html_url"": ""https://github.com/apps/octoapp"",
""created_at"": ""2017-07-08T16:18:44-04:00"",
""updated_at"": ""2017-07-08T16:18:44-04:00"",
""permissions"": {
""metadata"": ""read"",
""contents"": ""read"",
""issues"": ""write"",
""single_file"": ""write""
},
""events"": [
""push"",
""pull_request""
],
""client_id"": ""Iv1.8a61f9b3a7aba766"",
""client_secret"": ""1726be1638095a19edd134c77bde3aa2ece1e5d8"",
""webhook_secret"": ""e340154128314309424b7c8e90325147d99fdafa"",
""pem"": ""-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAuEPzOUE+kiEH1WLiMeBytTEF856j0hOVcSUSUkZxKvqczkWM\n9vo1gDyC7ZXhdH9fKh32aapba3RSsp4ke+giSmYTk2mGR538ShSDxh0OgpJmjiKP\nX0Bj4j5sFqfXuCtl9SkH4iueivv4R53ktqM+n6hk98l6hRwC39GVIblAh2lEM4L/\n6WvYwuQXPMM5OG2Ryh2tDZ1WS5RKfgq+9ksNJ5Q9UtqtqHkO+E63N5OK9sbzpUUm\noNaOl3udTlZD3A8iqwMPVxH4SxgATBPAc+bmjk6BMJ0qIzDcVGTrqrzUiywCTLma\nszdk8GjzXtPDmuBgNn+o6s02qVGpyydgEuqmTQIDAQABAoIBACL6AvkjQVVLn8kJ\ndBYznJJ4M8ECo+YEgaFwgAHODT0zRQCCgzd+Vxl4YwHmKV2Lr+y2s0drZt8GvYva\nKOK8NYYZyi15IlwFyRXmvvykF1UBpSXluYFDH7KaVroWMgRreHcIys5LqVSIb6Bo\ngDmK0yBLPp8qR29s2b7ScZRtLaqGJiX+j55rNzrZwxHkxFHyG9OG+u9IsBElcKCP\nkYCVE8ZdYexfnKOZbgn2kZB9qu0T/Mdvki8yk3I2bI6xYO24oQmhnT36qnqWoCBX\nNuCNsBQgpYZeZET8mEAUmo9d+ABmIHIvSs005agK8xRaP4+6jYgy6WwoejJRF5yd\nNBuF7aECgYEA50nZ4FiZYV0vcJDxFYeY3kYOvVuKn8OyW+2rg7JIQTremIjv8FkE\nZnwuF9ZRxgqLxUIfKKfzp/5l5LrycNoj2YKfHKnRejxRWXqG+ZETfxxlmlRns0QG\nJ4+BYL0CoanDSeA4fuyn4Bv7cy/03TDhfg/Uq0Aeg+hhcPE/vx3ebPsCgYEAy/Pv\neDLssOSdeyIxf0Brtocg6aPXIVaLdus+bXmLg77rJIFytAZmTTW8SkkSczWtucI3\nFI1I6sei/8FdPzAl62/JDdlf7Wd9K7JIotY4TzT7Tm7QU7xpfLLYIP1bOFjN81rk\n77oOD4LsXcosB/U6s1blPJMZ6AlO2EKs10UuR1cCgYBipzuJ2ADEaOz9RLWwi0AH\nPza2Sj+c2epQD9ZivD7Zo/Sid3ZwvGeGF13JyR7kLEdmAkgsHUdu1rI7mAolXMaB\n1pdrsHureeLxGbRM6za3tzMXWv1Il7FQWoPC8ZwXvMOR1VQDv4nzq7vbbA8z8c+c\n57+8tALQHOTDOgQIzwK61QKBgERGVc0EJy4Uag+VY8J4m1ZQKBluqo7TfP6DQ7O8\nM5MX73maB/7yAX8pVO39RjrhJlYACRZNMbK+v/ckEQYdJSSKmGCVe0JrGYDuPtic\nI9+IGfSorf7KHPoMmMN6bPYQ7Gjh7a++tgRFTMEc8956Hnt4xGahy9NcglNtBpVN\n6G8jAoGBAMCh028pdzJa/xeBHLLaVB2sc0Fe7993WlsPmnVE779dAz7qMscOtXJK\nfgtriltLSSD6rTA9hUAsL/X62rY0wdXuNdijjBb/qvrx7CAV6i37NK1CjABNjsfG\nZM372Ac6zc1EqSrid2IjET1YqyIW2KGLI1R2xbQc98UGlt48OdWu\n-----END RSA PRIVATE KEY-----\n""
}";

var app = new SimpleJsonSerializer().Deserialize<GitHubAppFromManifest>(json);

Assert.Equal(1, app.Id);
Assert.Equal("octoapp", app.Slug);
Assert.Equal("MDxOkludGVncmF0aW9uMQ==", app.NodeId);
Assert.Equal("Octocat App", app.Name);
Assert.Equal("A short description of the app", app.Description);
Assert.Equal("https://example.com", app.ExternalUrl);
Assert.Equal("https://github.com/apps/octoapp", app.HtmlUrl);
Assert.Equal(InstallationReadWritePermissionLevel.Write, app.Permissions.SingleFile);
Assert.Contains("push", app.Events);

Assert.Equal("Iv1.8a61f9b3a7aba766", app.ClientId);
Assert.Equal("1726be1638095a19edd134c77bde3aa2ece1e5d8", app.ClientSecret);
Assert.Equal("e340154128314309424b7c8e90325147d99fdafa", app.WebhookSecret);
Assert.Equal("-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAuEPzOUE+kiEH1WLiMeBytTEF856j0hOVcSUSUkZxKvqczkWM\n9vo1gDyC7ZXhdH9fKh32aapba3RSsp4ke+giSmYTk2mGR538ShSDxh0OgpJmjiKP\nX0Bj4j5sFqfXuCtl9SkH4iueivv4R53ktqM+n6hk98l6hRwC39GVIblAh2lEM4L/\n6WvYwuQXPMM5OG2Ryh2tDZ1WS5RKfgq+9ksNJ5Q9UtqtqHkO+E63N5OK9sbzpUUm\noNaOl3udTlZD3A8iqwMPVxH4SxgATBPAc+bmjk6BMJ0qIzDcVGTrqrzUiywCTLma\nszdk8GjzXtPDmuBgNn+o6s02qVGpyydgEuqmTQIDAQABAoIBACL6AvkjQVVLn8kJ\ndBYznJJ4M8ECo+YEgaFwgAHODT0zRQCCgzd+Vxl4YwHmKV2Lr+y2s0drZt8GvYva\nKOK8NYYZyi15IlwFyRXmvvykF1UBpSXluYFDH7KaVroWMgRreHcIys5LqVSIb6Bo\ngDmK0yBLPp8qR29s2b7ScZRtLaqGJiX+j55rNzrZwxHkxFHyG9OG+u9IsBElcKCP\nkYCVE8ZdYexfnKOZbgn2kZB9qu0T/Mdvki8yk3I2bI6xYO24oQmhnT36qnqWoCBX\nNuCNsBQgpYZeZET8mEAUmo9d+ABmIHIvSs005agK8xRaP4+6jYgy6WwoejJRF5yd\nNBuF7aECgYEA50nZ4FiZYV0vcJDxFYeY3kYOvVuKn8OyW+2rg7JIQTremIjv8FkE\nZnwuF9ZRxgqLxUIfKKfzp/5l5LrycNoj2YKfHKnRejxRWXqG+ZETfxxlmlRns0QG\nJ4+BYL0CoanDSeA4fuyn4Bv7cy/03TDhfg/Uq0Aeg+hhcPE/vx3ebPsCgYEAy/Pv\neDLssOSdeyIxf0Brtocg6aPXIVaLdus+bXmLg77rJIFytAZmTTW8SkkSczWtucI3\nFI1I6sei/8FdPzAl62/JDdlf7Wd9K7JIotY4TzT7Tm7QU7xpfLLYIP1bOFjN81rk\n77oOD4LsXcosB/U6s1blPJMZ6AlO2EKs10UuR1cCgYBipzuJ2ADEaOz9RLWwi0AH\nPza2Sj+c2epQD9ZivD7Zo/Sid3ZwvGeGF13JyR7kLEdmAkgsHUdu1rI7mAolXMaB\n1pdrsHureeLxGbRM6za3tzMXWv1Il7FQWoPC8ZwXvMOR1VQDv4nzq7vbbA8z8c+c\n57+8tALQHOTDOgQIzwK61QKBgERGVc0EJy4Uag+VY8J4m1ZQKBluqo7TfP6DQ7O8\nM5MX73maB/7yAX8pVO39RjrhJlYACRZNMbK+v/ckEQYdJSSKmGCVe0JrGYDuPtic\nI9+IGfSorf7KHPoMmMN6bPYQ7Gjh7a++tgRFTMEc8956Hnt4xGahy9NcglNtBpVN\n6G8jAoGBAMCh028pdzJa/xeBHLLaVB2sc0Fe7993WlsPmnVE779dAz7qMscOtXJK\nfgtriltLSSD6rTA9hUAsL/X62rY0wdXuNdijjBb/qvrx7CAV6i37NK1CjABNjsfG\nZM372Ac6zc1EqSrid2IjET1YqyIW2KGLI1R2xbQc98UGlt48OdWu\n-----END RSA PRIVATE KEY-----\n", app.Pem);

Assert.Equal(1, app.Owner.Id);
Assert.Equal("github", app.Owner.Login);
Assert.Equal("https://api.github.com/orgs/github", app.Owner.Url);
Assert.Equal(AccountType.User, app.Owner.Type);
}
}
}
32 changes: 32 additions & 0 deletions Octokit.Tests/Reactive/ObservableGitHubAppsClientTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -287,5 +287,37 @@ public void GetsFromCorrectUrl()
gitHubClient.GitHubApps.Received().GetUserInstallationForCurrent("ducks");
}
}

public class TheCreateAppFromManifestMethod
{
[Fact]
public void EnsuresNonNullArguments()
{
var gitHubClient = Substitute.For<IGitHubClient>();
var client = new ObservableGitHubAppsClient(gitHubClient);

Assert.Throws<ArgumentNullException>(() => client.CreateAppFromManifest(null));
}

[Fact]
public void EnsuresNonEmptyArguments()
{
var gitHubClient = Substitute.For<IGitHubClient>();
var client = new ObservableGitHubAppsClient(gitHubClient);

Assert.Throws<ArgumentException>(() => client.CreateAppFromManifest(""));
}

[Fact]
public void GetsFromCorrectUrl()
{
var gitHubClient = Substitute.For<IGitHubClient>();
var client = new ObservableGitHubAppsClient(gitHubClient);

client.CreateAppFromManifest("abc123");

gitHubClient.GitHubApps.Received().CreateAppFromManifest("abc123");
}
}
}
}
14 changes: 14 additions & 0 deletions Octokit/Clients/GitHubAppsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,5 +192,19 @@ public Task<Installation> GetUserInstallationForCurrent(string user)

return ApiConnection.Get<Installation>(ApiUrls.UserInstallation(user), null);
}

/// <summary>
/// Creates a GitHub app by completing the handshake necessary when implementing the GitHub App Manifest flow.
/// https://docs.github.com/apps/sharing-github-apps/registering-a-github-app-from-a-manifest
/// </summary>
/// <remarks>https://docs.github.com/rest/apps/apps#create-a-github-app-from-a-manifest</remarks>
/// <param name="code">Temporary code in a code parameter.</param>
[ManualRoute("POST", "/app-manifests/{code}/conversions")]
public Task<GitHubAppFromManifest> CreateAppFromManifest(string code)
{
Ensure.ArgumentNotNullOrEmptyString(code, nameof(code));

return ApiConnection.Post<GitHubAppFromManifest>(ApiUrls.AppManifestConversions(code));
}
}
}
10 changes: 9 additions & 1 deletion Octokit/Clients/IGitHubAppsClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,13 @@ public interface IGitHubAppsClient
/// <remarks>https://developer.github.com/v3/apps/#find-user-installation</remarks>
/// <param name="user">The name of the user</param>
Task<Installation> GetUserInstallationForCurrent(string user);

/// <summary>
/// Creates a GitHub app by completing the handshake necessary when implementing the GitHub App Manifest flow.
/// https://docs.github.com/apps/sharing-github-apps/registering-a-github-app-from-a-manifest
/// </summary>
/// <remarks>https://docs.github.com/rest/apps/apps#create-a-github-app-from-a-manifest</remarks>
/// <param name="code">Temporary code in a code parameter.</param>
Task<GitHubAppFromManifest> CreateAppFromManifest(string code);
}
}
}
9 changes: 9 additions & 0 deletions Octokit/Helpers/ApiUrls.cs
Original file line number Diff line number Diff line change
Expand Up @@ -403,6 +403,15 @@ public static Uri NotificationSubscription(int notificationId)
return "notifications/threads/{0}/subscription".FormatUri(notificationId);
}

/// <summary>
/// Returns the <see cref="Uri"/> to complete the handshake necessary when implementing the GitHub App Manifest flow.
/// </summary>
/// <param name="code">Temporary code in a code parameter.</param>
public static Uri AppManifestConversions(string code)
{
return "app-manifests/{0}/conversions".FormatUri(code);
}

/// <summary>
/// Returns the <see cref="Uri"/> for creating a new installation token.
/// </summary>
Expand Down
30 changes: 18 additions & 12 deletions Octokit/Models/Response/GitHubApp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ public class GitHubApp
{
public GitHubApp() { }

public GitHubApp(long id, string slug, string name, User owner, string description, string externalUrl, string htmlUrl, DateTimeOffset createdAt, DateTimeOffset updatedAt, InstallationPermissions permissions, IReadOnlyList<string> events)
public GitHubApp(long id, string slug, string nodeId, string name, User owner, string description, string externalUrl, string htmlUrl, DateTimeOffset createdAt, DateTimeOffset updatedAt, InstallationPermissions permissions, IReadOnlyList<string> events)
{
Id = id;
Slug = slug;
NodeId = nodeId;
Name = name;
Owner = owner;
Description = description;
Expand All @@ -31,57 +32,62 @@ public GitHubApp(long id, string slug, string name, User owner, string descripti
/// <summary>
/// The Id of the GitHub App.
/// </summary>
public long Id { get; private set; }
public long Id { get; protected set; }

/// <summary>
/// The url-friendly version of the GitHub App name.
/// </summary>
public string Slug { get; private set; }
public string Slug { get; protected set; }

/// <summary>
/// GraphQL Node Id
/// </summary>
public string NodeId { get; protected set; }

/// <summary>
/// The Name of the GitHub App.
/// </summary>
public string Name { get; private set; }
public string Name { get; protected set; }

/// <summary>
/// The Owner of the GitHub App.
/// </summary>
public User Owner { get; private set; }
public User Owner { get; protected set; }

/// <summary>
/// The Description of the GitHub App.
/// </summary>
public string Description { get; private set; }
public string Description { get; protected set; }

/// <summary>
/// The URL to the GitHub App's external website.
/// </summary>
public string ExternalUrl { get; private set; }
public string ExternalUrl { get; protected set; }

/// <summary>
/// The URL to view the GitHub App on GitHub.
/// </summary>
public string HtmlUrl { get; private set; }
public string HtmlUrl { get; protected set; }

/// <summary>
/// Date the GitHub App was created.
/// </summary>
public DateTimeOffset CreatedAt { get; private set; }
public DateTimeOffset CreatedAt { get; protected set; }

/// <summary>
/// Date the GitHub App was last updated.
/// </summary>
public DateTimeOffset UpdatedAt { get; private set; }
public DateTimeOffset UpdatedAt { get; protected set; }

/// <summary>
/// The Permissions granted to the Installation
/// </summary>
public InstallationPermissions Permissions { get; private set; }
public InstallationPermissions Permissions { get; protected set; }

/// <summary>
/// The Events subscribed to by the Installation
/// </summary>
public IReadOnlyList<string> Events { get; private set; }
public IReadOnlyList<string> Events { get; protected set; }

internal string DebuggerDisplay
{
Expand Down
45 changes: 45 additions & 0 deletions Octokit/Models/Response/GitHubAppFromManifest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;

namespace Octokit
{
/// <summary>
/// Represents a GitHub application from a manifest.
/// https://docs.github.com/rest/apps/apps#create-a-github-app-from-a-manifest
/// </summary>
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class GitHubAppFromManifest : GitHubApp
{
public GitHubAppFromManifest() { }

public GitHubAppFromManifest(long id, string slug, string nodeId, string name, User owner, string description, string externalUrl, string htmlUrl, DateTimeOffset createdAt, DateTimeOffset updatedAt, InstallationPermissions permissions, IReadOnlyList<string> events, string clientId, string clientSecret, string webhookSecret, string pem)
: base(id, slug, nodeId, name, owner, description, externalUrl, htmlUrl, createdAt, updatedAt, permissions, events)
{
ClientId = clientId;
ClientSecret = clientSecret;
WebhookSecret = webhookSecret;
Pem = pem;
}

/// <summary>
/// The Client Id of the GitHub App.
/// </summary>
public string ClientId { get; private set; }

/// <summary>
/// The Client Secret of the GitHub App.
/// </summary>
public string ClientSecret { get; private set; }

/// <summary>
/// The Webhook Secret of the GitHub App.
/// </summary>
public string WebhookSecret { get; private set; }

/// <summary>
/// The PEM of the GitHub App.
/// </summary>
public string Pem { get; private set; }
}
}
Loading

0 comments on commit c2aee1a

Please sign in to comment.