From da5c4d7d3c6c096af6cc576908d2b49ddad72a2a Mon Sep 17 00:00:00 2001 From: Alexander Sklar Date: Fri, 16 Jun 2023 10:26:39 -0700 Subject: [PATCH] [FEAT]: Adds codespaces APIs --- .../IObservableCodespacesClient.cs | 20 +++++ Octokit.Reactive/IObservableGitHubClient.cs | 1 + .../ObservableCodespacesClient.cs | 47 +++++++++++ Octokit.Reactive/ObservableGitHubClient.cs | 2 + .../Clients/CodespacesClientTests.cs | 59 ++++++++++++++ Octokit.Tests.Integration/Helper.cs | 5 ++ .../Clients/CodespacesClientTests.cs | 72 +++++++++++++++++ Octokit/Clients/Codespace.cs | 49 ++++++++++++ Octokit/Clients/CodespaceState.cs | 42 ++++++++++ Octokit/Clients/CodespacesClient.cs | 80 +++++++++++++++++++ Octokit/Clients/CodespacesCollection.cs | 26 ++++++ Octokit/Clients/ICodespacesClient.cs | 14 ++++ Octokit/Clients/Machine.cs | 30 +++++++ Octokit/GitHubClient.cs | 3 + Octokit/Helpers/ApiUrls.cs | 25 ++++++ Octokit/IGitHubClient.cs | 2 + script/configure-integration-tests.ps1 | 4 +- 17 files changed, 480 insertions(+), 1 deletion(-) create mode 100644 Octokit.Reactive/IObservableCodespacesClient.cs create mode 100644 Octokit.Reactive/ObservableCodespacesClient.cs create mode 100644 Octokit.Tests.Integration/Clients/CodespacesClientTests.cs create mode 100644 Octokit.Tests/Clients/CodespacesClientTests.cs create mode 100644 Octokit/Clients/Codespace.cs create mode 100644 Octokit/Clients/CodespaceState.cs create mode 100644 Octokit/Clients/CodespacesClient.cs create mode 100644 Octokit/Clients/CodespacesCollection.cs create mode 100644 Octokit/Clients/ICodespacesClient.cs create mode 100644 Octokit/Clients/Machine.cs diff --git a/Octokit.Reactive/IObservableCodespacesClient.cs b/Octokit.Reactive/IObservableCodespacesClient.cs new file mode 100644 index 0000000000..cb3e67ba25 --- /dev/null +++ b/Octokit.Reactive/IObservableCodespacesClient.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading.Tasks; + +namespace Octokit.Reactive +{ + /// + /// A client for GitHub's Codespaces API. + /// + /// + /// See the codespaces API documentation for more information. + /// + public interface IObservableCodespacesClient + { + IObservable GetAll(); + IObservable GetForRepository(string owner, string repo); + IObservable Get(string codespaceName); + IObservable Start(string codespaceName); + IObservable Stop(string codespaceName); + } +} \ No newline at end of file diff --git a/Octokit.Reactive/IObservableGitHubClient.cs b/Octokit.Reactive/IObservableGitHubClient.cs index 10cd16e933..bc610f7fca 100644 --- a/Octokit.Reactive/IObservableGitHubClient.cs +++ b/Octokit.Reactive/IObservableGitHubClient.cs @@ -42,5 +42,6 @@ public interface IObservableGitHubClient : IApiInfoProvider IObservableRateLimitClient RateLimit { get; } IObservableMetaClient Meta { get; } IObservableActionsClient Actions { get; } + IObservableCodespacesClient Codespaces { get; } } } diff --git a/Octokit.Reactive/ObservableCodespacesClient.cs b/Octokit.Reactive/ObservableCodespacesClient.cs new file mode 100644 index 0000000000..74ce55a872 --- /dev/null +++ b/Octokit.Reactive/ObservableCodespacesClient.cs @@ -0,0 +1,47 @@ +using System; +using System.Reactive.Threading.Tasks; + +namespace Octokit.Reactive +{ + public class ObservableCodespacesClient : IObservableCodespacesClient + { + private ICodespacesClient _client; + private IConnection _connection; + + public ObservableCodespacesClient(IGitHubClient githubClient) + { + _client = githubClient.Codespaces; + _connection = githubClient.Connection; + } + + public IObservable Get(string codespaceName) + { + Ensure.ArgumentNotNull(codespaceName, nameof(codespaceName)); + return _client.Get(codespaceName).ToObservable(); + } + + public IObservable GetAll() + { + return _client.GetAll().ToObservable(); + } + + public IObservable GetForRepository(string owner, string repo) + { + Ensure.ArgumentNotNull(owner, nameof(owner)); + Ensure.ArgumentNotNull(repo, nameof(repo)); + return _client.GetForRepository(owner, repo).ToObservable(); + } + + public IObservable Start(string codespaceName) + { + Ensure.ArgumentNotNull(codespaceName, nameof(codespaceName)); + return _client.Start(codespaceName).ToObservable(); + } + + public IObservable Stop(string codespaceName) + { + Ensure.ArgumentNotNull(codespaceName, nameof(codespaceName)); + return _client.Stop(codespaceName).ToObservable(); + } + } +} \ No newline at end of file diff --git a/Octokit.Reactive/ObservableGitHubClient.cs b/Octokit.Reactive/ObservableGitHubClient.cs index 7507a382cf..7b8da221a3 100644 --- a/Octokit.Reactive/ObservableGitHubClient.cs +++ b/Octokit.Reactive/ObservableGitHubClient.cs @@ -57,6 +57,7 @@ public ObservableGitHubClient(IGitHubClient gitHubClient) RateLimit = new ObservableRateLimitClient(gitHubClient); Meta = new ObservableMetaClient(gitHubClient); Actions = new ObservableActionsClient(gitHubClient); + Codespaces = new ObservableCodespacesClient(gitHubClient); } public IConnection Connection @@ -105,6 +106,7 @@ public void SetRequestTimeout(TimeSpan timeout) public IObservableMetaClient Meta { get; private set; } public IObservableActionsClient Actions { get; private set; } + public IObservableCodespacesClient Codespaces { get; private set; } /// /// Gets the latest API Info - this will be null if no API calls have been made /// diff --git a/Octokit.Tests.Integration/Clients/CodespacesClientTests.cs b/Octokit.Tests.Integration/Clients/CodespacesClientTests.cs new file mode 100644 index 0000000000..f1b6342384 --- /dev/null +++ b/Octokit.Tests.Integration/Clients/CodespacesClientTests.cs @@ -0,0 +1,59 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Octokit; +using Octokit.Tests.Helpers; +using Octokit.Tests.Integration; +using Xunit; + +public class CodespacesClientTests +{ + readonly ICodespacesClient _fixture; + + public CodespacesClientTests() + { + var github = Helper.GetAuthenticatedClient(); + _fixture = github.Codespaces; + } + + [IntegrationTest] + public async Task CanGetCodespaces() + { + var retrieved = await _fixture.GetAll(); + Assert.NotNull(retrieved); + } + + [IntegrationTest] + public async Task CanGetCodespacesForRepo() + { + var retrieved = await _fixture.GetForRepository(Helper.UserName, Helper.RepositoryWithCodespaces); + Assert.NotNull(retrieved); + } + + [IntegrationTest] + public async Task CanGetCodespaceByName() + { + var collection = await _fixture.GetForRepository(Helper.UserName, Helper.RepositoryWithCodespaces); + var codespaceName = collection.Codespaces.First().Name; + var retrieved = await _fixture.Get(codespaceName); + Assert.NotNull(retrieved); + } + + [IntegrationTest] + public async Task CanStartCodespace() + { + var collection = await _fixture.GetForRepository(Helper.UserName, Helper.RepositoryWithCodespaces); + var codespaceName = collection.Codespaces.First().Name; + var retrieved = await _fixture.Start(codespaceName); + Assert.NotNull(retrieved); + } + + [IntegrationTest] + public async Task CanStopCodespace() + { + var collection = await _fixture.GetForRepository(Helper.UserName, Helper.RepositoryWithCodespaces); + var codespaceName = collection.Codespaces.First().Name; + var retrieved = await _fixture.Stop(codespaceName); + Assert.NotNull(retrieved); + } +} diff --git a/Octokit.Tests.Integration/Helper.cs b/Octokit.Tests.Integration/Helper.cs index 9c94d78f64..001613cdf8 100644 --- a/Octokit.Tests.Integration/Helper.cs +++ b/Octokit.Tests.Integration/Helper.cs @@ -171,6 +171,11 @@ public static string GitHubAppSlug get { return Environment.GetEnvironmentVariable("OCTOKIT_GITHUBAPP_SLUG"); } } + public static string RepositoryWithCodespaces + { + get { return Environment.GetEnvironmentVariable("OCTOKIT_REPOSITORY_WITH_CODESPACES"); } + } + public static void DeleteRepo(IConnection connection, Repository repository) { if (repository != null) diff --git a/Octokit.Tests/Clients/CodespacesClientTests.cs b/Octokit.Tests/Clients/CodespacesClientTests.cs new file mode 100644 index 0000000000..41fad4a51c --- /dev/null +++ b/Octokit.Tests/Clients/CodespacesClientTests.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Threading.Tasks; +using NSubstitute; +using Octokit.Internal; +using Octokit; +using Octokit.Tests; +using Xunit; + +using static Octokit.Internal.TestSetup; + +public class CodespacesClientTests +{ + public class TheCtor + { + [Fact] + public void EnsuresNonNullArguments() + { + Assert.Throws(() => new CodespacesClient(null)); + } + } + + public class TheGetAllMethod + { + [Fact] + public void RequestsCorrectGetAllUrl() + { + var connection = Substitute.For(); + var client = new CodespacesClient(connection); + + client.GetAll(); + connection.Received().Get(Arg.Is(u => u.ToString() == "user/codespaces")); + } + + [Fact] + public void RequestsCorrectGetForRepositoryUrl() + { + var connection = Substitute.For(); + var client = new CodespacesClient(connection); + client.GetForRepository("owner", "repo"); + connection.Received().Get(Arg.Is(u => u.ToString() == "repos/owner/repo/codespaces")); + } + + [Fact] + public void RequestsCorrectGetUrl() + { + var connection = Substitute.For(); + var client = new CodespacesClient(connection); + client.Get("codespaceName"); + connection.Received().Get(Arg.Is(u => u.ToString() == "user/codespaces/codespaceName")); + } + + [Fact] + public void RequestsCorrectStartUrl() + { + var connection = Substitute.For(); + var client = new CodespacesClient(connection); + client.Start("codespaceName"); + connection.Received().Post(Arg.Is(u => u.ToString() == "user/codespaces/codespaceName/start")); + } + + [Fact] + public void RequestsCorrectStopUrl() + { + var connection = Substitute.For(); + var client = new CodespacesClient(connection); + client.Stop("codespaceName"); + connection.Received().Post(Arg.Is(u => u.ToString() == "user/codespaces/codespaceName/stop")); + } + } +} diff --git a/Octokit/Clients/Codespace.cs b/Octokit/Clients/Codespace.cs new file mode 100644 index 0000000000..e0a3da4ce0 --- /dev/null +++ b/Octokit/Clients/Codespace.cs @@ -0,0 +1,49 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Runtime.CompilerServices; + +namespace Octokit +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class Codespace + { + public int Id { get; private set; } + public string Name { get; private set; } + public User Owner { get; private set; } + public User BillableOwner { get; private set; } + public Repository Repository { get; private set; } + public Machine Machine { get; private set; } + public DateTime CreatedAt { get;private set; } + public DateTime UpdatedAt { get; private set; } + public DateTime LastUsedAt { get; private set; } + public StringEnum State { get; private set; } + public string Url { get; private set; } + public string MachinesUrl { get; private set; } + public string WebUrl { get; private set; } + public string StartUrl { get; private set; } + public string StopUrl { get; private set; } + + public Codespace(int id, string name, User owner, User billableOwner, Repository repository, Machine machine, DateTime createdAt, DateTime updatedAt, DateTime lastUsedAt, StringEnum state, string url, string machinesUrl, string webUrl, string startUrl, string stopUrl) + { + Id = id; + Name = name; + Owner = owner; + BillableOwner = billableOwner; + Repository = repository; + Machine = machine; + CreatedAt = createdAt; + UpdatedAt = updatedAt; + LastUsedAt = lastUsedAt; + State = state; + Url = url; + MachinesUrl = machinesUrl; + WebUrl = webUrl; + StartUrl = startUrl; + StopUrl = stopUrl; + } + + public Codespace() { } + internal string DebuggerDisplay => string.Format(CultureInfo.CurrentCulture, "Codespace: Id: {0}", Id); + } +} \ No newline at end of file diff --git a/Octokit/Clients/CodespaceState.cs b/Octokit/Clients/CodespaceState.cs new file mode 100644 index 0000000000..1cff50d5cf --- /dev/null +++ b/Octokit/Clients/CodespaceState.cs @@ -0,0 +1,42 @@ +using Octokit.Internal; + +namespace Octokit +{ + public enum CodespaceState + { + [Parameter(Value = "Unknown")] + Unknown, + [Parameter(Value = "Created")] + Created, + [Parameter(Value = "Queued")] + Queued, + [Parameter(Value = "Provisioning")] + Provisioning, + [Parameter(Value = "Available")] + Available, + [Parameter(Value = "Awaiting")] + Awaiting, + [Parameter(Value = "Unavailable")] + Unavailable, + [Parameter(Value = "Deleted")] + Deleted, + [Parameter(Value = "Moved")] + Moved, + [Parameter(Value = "Shutdown")] + Shutdown, + [Parameter(Value = "Archived")] + Archived, + [Parameter(Value = "Starting")] + Starting, + [Parameter(Value = "ShuttingDown")] + ShuttingDown, + [Parameter(Value = "Failed")] + Failed, + [Parameter(Value = "Exporting")] + Exporting, + [Parameter(Value = "Updating")] + Updating, + [Parameter(Value = "Rebuilding")] + Rebuilding, + } +} \ No newline at end of file diff --git a/Octokit/Clients/CodespacesClient.cs b/Octokit/Clients/CodespacesClient.cs new file mode 100644 index 0000000000..afe47ce8ad --- /dev/null +++ b/Octokit/Clients/CodespacesClient.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; + +namespace Octokit +{ + /// + /// A client for GitHub's Codespaces API. + /// Gets and creates Codespaces. + /// + /// + /// See the Codespaces API documentation for more information. + /// + public class CodespacesClient : ApiClient, ICodespacesClient + { + /// + /// Instantiates a new GitHub Codespaces API client. + /// + /// + public CodespacesClient(IApiConnection apiConnection) : base(apiConnection) + { + } + + /// + /// Returns all the codespaces for the authenticated user. + /// + /// A codespaces collection + [ManualRoute("GET", "/user/codespaces")] + public Task GetAll() + { + return ApiConnection.Get(ApiUrls.Codespaces()); + } + + /// + /// Returns all the codespaces for the specified repository. + /// + /// + /// + /// A codespaces collection + [ManualRoute("GET", "/repos/{owner}/{repo}/codespaces")] + public Task GetForRepository(string owner, string repo) + { + return ApiConnection.Get(ApiUrls.CodespacesForRepository(owner, repo)); + } + + /// + /// Gets a codespace for the authenticated user. + /// + /// + /// A codespace + [ManualRoute("GET", "/user/codespaces/{codespace_name}")] + public Task Get(string codespaceName) + { + return ApiConnection.Get(ApiUrls.Codespace(codespaceName)); + } + + /// + /// Starts a codespace for the authenticated user. + /// + /// + /// + [ManualRoute("POST", "/user/codespaces/{codespace_name}/start")] + public Task Start(string codespaceName) + { + return ApiConnection.Post(ApiUrls.CodespaceStart(codespaceName)); + } + + /// + /// Stops a codespace for the authenticated user. + /// + /// + /// + [ManualRoute("POST", "/user/codespaces/{codespace_name}/stop")] + public Task Stop(string codespaceName) + { + return ApiConnection.Post(ApiUrls.CodespaceStop(codespaceName)); + } + } +} diff --git a/Octokit/Clients/CodespacesCollection.cs b/Octokit/Clients/CodespacesCollection.cs new file mode 100644 index 0000000000..e91282ab53 --- /dev/null +++ b/Octokit/Clients/CodespacesCollection.cs @@ -0,0 +1,26 @@ +using Octokit.Internal; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class CodespacesCollection + { + public CodespacesCollection(IReadOnlyList codespaces, int count) + { + Codespaces = codespaces; + Count = count; + } + + public CodespacesCollection() { } + + [Parameter(Key = "total_count")] + public int Count { get; private set; } + [Parameter(Key = "codespaces")] + public IReadOnlyList Codespaces { get; private set; } = new List(); + + internal string DebuggerDisplay => string.Format(CultureInfo.CurrentCulture, "CodespacesCollection: Count: {0}", Count); + } +} \ No newline at end of file diff --git a/Octokit/Clients/ICodespacesClient.cs b/Octokit/Clients/ICodespacesClient.cs new file mode 100644 index 0000000000..a3c630d5b7 --- /dev/null +++ b/Octokit/Clients/ICodespacesClient.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Octokit +{ + public interface ICodespacesClient + { + Task GetAll(); + Task GetForRepository(string owner, string repo); + Task Get(string codespaceName); + Task Start(string codespaceName); + Task Stop(string codespaceName); + } +} \ No newline at end of file diff --git a/Octokit/Clients/Machine.cs b/Octokit/Clients/Machine.cs new file mode 100644 index 0000000000..2153ffbb2f --- /dev/null +++ b/Octokit/Clients/Machine.cs @@ -0,0 +1,30 @@ +using System.Diagnostics; +using System.Globalization; + +namespace Octokit +{ + [DebuggerDisplay("{DebuggerDisplay,nq}")] + public class Machine + { + public string Name { get; private set; } + public string DisplayName { get; private set; } + public string OperatingSystem { get; private set; } + public long StorageInBytes { get; private set; } + public long MemoryInBytes { get; private set; } + public long CpuCount { get; private set; } + + public Machine(string name, string displayName, string operatingSystem, long storageInBytes, long memoryInBytes, long cpuCount) + { + Name = name; + DisplayName = displayName; + OperatingSystem = operatingSystem; + StorageInBytes = storageInBytes; + MemoryInBytes = memoryInBytes; + CpuCount = cpuCount; + } + + public Machine() { } + + internal string DebuggerDisplay => string.Format(CultureInfo.CurrentCulture, "Machine: {0}", DisplayName); + } +} \ No newline at end of file diff --git a/Octokit/GitHubClient.cs b/Octokit/GitHubClient.cs index 367dbdf5c1..2a839b3b1f 100644 --- a/Octokit/GitHubClient.cs +++ b/Octokit/GitHubClient.cs @@ -120,6 +120,7 @@ public GitHubClient(IConnection connection) RateLimit = new RateLimitClient(apiConnection); Meta = new MetaClient(apiConnection); Actions = new ActionsClient(apiConnection); + Codespaces = new CodespacesClient(apiConnection); } /// @@ -393,6 +394,8 @@ public Uri BaseAddress /// public IActionsClient Actions { get; private set; } + public ICodespacesClient Codespaces { get; private set; } + static Uri FixUpBaseUri(Uri uri) { Ensure.ArgumentNotNull(uri, nameof(uri)); diff --git a/Octokit/Helpers/ApiUrls.cs b/Octokit/Helpers/ApiUrls.cs index 0c870a42c7..20765f96cc 100644 --- a/Octokit/Helpers/ApiUrls.cs +++ b/Octokit/Helpers/ApiUrls.cs @@ -17,6 +17,7 @@ public static partial class ApiUrls static readonly Uri _currentUserNotificationsEndpoint = new Uri("notifications", UriKind.Relative); static readonly Uri _currentUserAllIssues = new Uri("issues", UriKind.Relative); static readonly Uri _currentUserOwnedAndMemberIssues = new Uri("user/issues", UriKind.Relative); + static readonly Uri _currentUserAllCodespaces = new Uri("user/codespaces", UriKind.Relative); /// /// Returns the that returns all public repositories in @@ -5447,5 +5448,29 @@ public static Uri ActionsListOrganizationRunnerGroupRepositories(string org, lon return "orgs/{0}/actions/runner-groups/{1}/repositories".FormatUri(org, runnerGroupId); } + public static Uri Codespaces() + { + return _currentUserAllCodespaces; + } + + public static Uri CodespacesForRepository(string owner, string repo) + { + return "repos/{0}/{1}/codespaces".FormatUri(owner, repo); + } + + public static Uri Codespace(string codespaceName) + { + return "user/codespaces/{0}".FormatUri(codespaceName); + } + + public static Uri CodespaceStart(string codespaceName) + { + return "user/codespaces/{0}/start".FormatUri(codespaceName); + } + + public static Uri CodespaceStop(string codespaceName) + { + return "user/codespaces/{0}/stop".FormatUri(codespaceName); + } } } diff --git a/Octokit/IGitHubClient.cs b/Octokit/IGitHubClient.cs index 9bd53eb802..0fe8d3d3af 100644 --- a/Octokit/IGitHubClient.cs +++ b/Octokit/IGitHubClient.cs @@ -215,5 +215,7 @@ public interface IGitHubClient : IApiInfoProvider /// ILicensesClient Licenses { get; } IEmojisClient Emojis { get; } + + ICodespacesClient Codespaces { get; } } } diff --git a/script/configure-integration-tests.ps1 b/script/configure-integration-tests.ps1 index 4a3d6cdd6d..6a4f07aa5c 100644 --- a/script/configure-integration-tests.ps1 +++ b/script/configure-integration-tests.ps1 @@ -118,4 +118,6 @@ if (AskYesNoQuestion "Do you wish to enable GitHub Enterprise (GHE) Integration VerifyEnvironmentVariable "GitHub Enterprise application ClientID" "OCTOKIT_GHE_CLIENTID" $true VerifyEnvironmentVariable "GitHub Enterprise application Secret" "OCTOKIT_GHE_CLIENTSECRET" $true -} \ No newline at end of file +} + +VerifyEnvironmentVariable "Repository with codespaces" "OCTOKIT_REPOSITORY_WITH_CODESPACES" $true