diff --git a/Octokit.Tests.Integration/Octokit.Tests.Integration.csproj b/Octokit.Tests.Integration/Octokit.Tests.Integration.csproj index 0d91a136..2e2ddae2 100644 --- a/Octokit.Tests.Integration/Octokit.Tests.Integration.csproj +++ b/Octokit.Tests.Integration/Octokit.Tests.Integration.csproj @@ -49,6 +49,7 @@ + diff --git a/Octokit.Tests.Integration/ReleasesClientTests.cs b/Octokit.Tests.Integration/ReleasesClientTests.cs new file mode 100644 index 00000000..8dcd7f91 --- /dev/null +++ b/Octokit.Tests.Integration/ReleasesClientTests.cs @@ -0,0 +1,26 @@ +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Octokit.Tests.Integration +{ + public class ReleasesClientTests + { + public class TheGetReleasesMethod + { + [IntegrationTest] + public async Task ReturnsReleases() + { + var github = new GitHubClient + { + Credentials = AutomationSettings.Current.GitHubCredentials + }; + + var releases = await github.Releases.GetAll("git-tfs", "git-tfs"); + + Assert.True(releases.Count > 5); + Assert.True(releases.Any(release => release.TagName == "v0.18.0")); + } + } + } +} diff --git a/Octokit.Tests/Clients/ReleasesClientTests.cs b/Octokit.Tests/Clients/ReleasesClientTests.cs new file mode 100644 index 00000000..a5af7d07 --- /dev/null +++ b/Octokit.Tests/Clients/ReleasesClientTests.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using NSubstitute; +using Octokit.Clients; +using Octokit.Http; +using Octokit.Tests.Helpers; +using Xunit; + +namespace Octokit.Tests.Clients +{ + public class ReleasesClientTests + { + + public class TheGetReleasesMethod + { + [Fact] + public void RequestsCorrectUrl() + { + var client = Substitute.For>(); + var releasesClient = new ReleasesClient(client); + + releasesClient.GetAll("fake", "repo"); + + client.Received().GetAll(Arg.Is(u => u.ToString() == "/repos/fake/repo/releases"), null); + } + + [Fact] + public async Task EnsuresNonNullArguments() + { + var releasesClient = new ReleasesClient(Substitute.For>()); + + await AssertEx.Throws(async () => await releasesClient.GetAll(null, "name")); + await AssertEx.Throws(async () => await releasesClient.GetAll("owner", null)); + } + } + + public class TheCreateReleaseMethod + { + [Fact] + public void RequestsCorrectUrl() + { + var client = Substitute.For>(); + var releasesClient = new ReleasesClient(client); + var data = new ReleaseUpdate("fake-tag"); + + releasesClient.CreateRelease("fake", "repo", data); + + client.Received().Create(Arg.Is(u => u.ToString() == "/repos/fake/repo/releases"), data); + } + + [Fact] + public async Task EnsuresArgumentsNotNull() + { + var releasesClient = new ReleasesClient(Substitute.For>()); + var data = new ReleaseUpdate("fake-tag"); + + Assert.Throws(() => new ReleaseUpdate(null)); + await AssertEx.Throws(async () => await releasesClient.CreateRelease(null, "name", data)); + await AssertEx.Throws(async () => await releasesClient.CreateRelease("owner", null, data)); + await AssertEx.Throws(async () => await releasesClient.CreateRelease("owner", "name", null)); + } + } + + public class TheUploadReleaseAssetMethod + { + [Fact] + public void UploadsToCorrectUrl() + { + var client = Substitute.For>(); + var releasesClient = new ReleasesClient(client); + var release = new Release { UploadUrl = "https://uploads.test.dev/does/not/matter/releases/1/assets{?name}" }; + var rawData = Substitute.For(); + var upload = new ReleaseAssetUpload { FileName = "example.zip", ContentType = "application/zip", RawData = rawData }; + + releasesClient.UploadAsset(release, upload); + + client.Received().Upload(Arg.Is(u => u.ToString() == "https://uploads.test.dev/does/not/matter/releases/1/assets?name=example.zip"), + rawData, + Arg.Is(contentType => contentType == "application/zip")); + } + + [Fact] + public async Task EnsuresArgumentsNotNull() + { + var releasesClient = new ReleasesClient(Substitute.For>()); + + var release = new Release { UploadUrl = "https://uploads.github.com/anything" }; + var uploadData = new ReleaseAssetUpload { FileName = "good", ContentType = "good/good", RawData = Stream.Null }; + await AssertEx.Throws(async () => await releasesClient.UploadAsset(null, uploadData)); + await AssertEx.Throws(async () => await releasesClient.UploadAsset(release, null)); + } + } + } +} diff --git a/Octokit.Tests/Fixtures/Fixtures.cs b/Octokit.Tests/Fixtures/Fixtures.cs index b9bdac72..faf5497a 100644 --- a/Octokit.Tests/Fixtures/Fixtures.cs +++ b/Octokit.Tests/Fixtures/Fixtures.cs @@ -19,5 +19,14 @@ public static EmbeddedResource RepositoriesJson = new EmbeddedResource(typeof(Fixtures).Assembly, "Octokit.Tests.Fixtures.repositories.json"); + + public static EmbeddedResource ReleasesJson = + new EmbeddedResource(typeof(Fixtures).Assembly, "Octokit.Tests.Fixtures.releases.json"); + + public static EmbeddedResource ReleaseJson = + new EmbeddedResource(typeof(Fixtures).Assembly, "Octokit.Tests.Fixtures.release.json"); + + public static EmbeddedResource ReleaseAssetJson = + new EmbeddedResource(typeof(Fixtures).Assembly, "Octokit.Tests.Fixtures.release_asset.json"); } } diff --git a/Octokit.Tests/Fixtures/release.json b/Octokit.Tests/Fixtures/release.json new file mode 100644 index 00000000..78847fed --- /dev/null +++ b/Octokit.Tests/Fixtures/release.json @@ -0,0 +1,13 @@ +{"url": "https://api.github.com/repos/octocat/Hello-World/releases/1", + "html_url": "https://github.com/octocat/Hello-World/releases/v1.0.0", + "assets_url": "https://api.github.com/repos/octocat/Hello-World/releases/1/assets", + "upload_url": "https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets{?name}", + "id": 1, + "tag_name": "v1.0.0", + "target_commitish": "master", + "name": "v1.0.0", + "body": "Description of the release", + "draft": false, + "prerelease": false, + "created_at": "2013-02-27T19:35:32Z", + "published_at": "2013-02-27T19:35:32Z"} \ No newline at end of file diff --git a/Octokit.Tests/Fixtures/release_asset.json b/Octokit.Tests/Fixtures/release_asset.json new file mode 100644 index 00000000..aba80fa7 --- /dev/null +++ b/Octokit.Tests/Fixtures/release_asset.json @@ -0,0 +1,12 @@ +{ + "url": "https://api.github.com/repos/git-tfs/git-tfs/releases/assets/7057", + "id": 7057, + "name": "GitTfs-0.18.0.zip", + "label": "GitTfs-0.18.0.zip", + "content_type": "application/zip", + "state": "uploaded", + "size": 1098250, + "download_count": 1337, + "created_at": "2013-07-26T15:11:05Z", + "updated_at": "2013-07-26T15:11:19Z" +} \ No newline at end of file diff --git a/Octokit.Tests/Fixtures/releases.json b/Octokit.Tests/Fixtures/releases.json new file mode 100644 index 00000000..36022092 --- /dev/null +++ b/Octokit.Tests/Fixtures/releases.json @@ -0,0 +1,230 @@ +[ + { + "url": "https://api.github.com/repos/git-tfs/git-tfs/releases/16259", + "assets_url": "https://api.github.com/repos/git-tfs/git-tfs/releases/16259/assets", + "upload_url": "https://uploads.github.com/repos/git-tfs/git-tfs/releases/16259/assets{?name}", + "html_url": "https://github.com/git-tfs/git-tfs/releases/v0.18.0", + "id": 16259, + "tag_name": "v0.18.0", + "target_commitish": "master", + "name": "0.18.0", + "body": "* Improve unshelve (#351)\r\n* Better support for running git-tfs in bare repositories (#353)\r\n* Use the saved author file for `rcheckin` (#366)\r\n* Add \"except\" to include an otherwise \"ignored\" file (#377)\r\n* Update libgit2sharp (#387)\r\n* Add a `--nofetch` option to the `branch` (#392) and `init-branch` (#379) commands\r\n* Reduce memory consumption during a fetch (#394)\r\n* Use only one workspace per fetch (rather than one workspace per changeset fetched) (#414)\r\n* Other fixes (#367, #376, #385, #389, #390, #397, #398, #400, #416)\r\n\r\n[Full diff](https://github.com/git-tfs/git-tfs/compare/v0.17.2...v0.18.0)", + "draft": false, + "prerelease": false, + "created_at": "2013-07-26T14:26:15Z", + "published_at": "2013-07-26T15:10:28Z", + "assets": [ + { + "url": "https://api.github.com/repos/git-tfs/git-tfs/releases/assets/7057", + "id": 7057, + "name": "GitTfs-0.18.0.zip", + "label": "GitTfs-0.18.0.zip", + "content_type": "application/zip", + "state": "uploaded", + "size": 1098250, + "download_count": 1337, + "created_at": "2013-07-26T15:11:05Z", + "updated_at": "2013-07-26T15:11:19Z" + } + ] + }, + { + "url": "https://api.github.com/repos/git-tfs/git-tfs/releases/10541", + "assets_url": "https://api.github.com/repos/git-tfs/git-tfs/releases/10541/assets", + "upload_url": "https://uploads.github.com/repos/git-tfs/git-tfs/releases/10541/assets{?name}", + "html_url": "https://github.com/git-tfs/git-tfs/releases/v0.12.1", + "id": 10541, + "tag_name": "v0.12.1", + "target_commitish": "master", + "name": "", + "body": "- Fixed: 'TF14045: The identity MYDOMAIN\\John Doe is not a recognized identity' (#76, #81)\r\n- Fixed: exception on unshelve if some items was renamed (#77)\r\n- Fixed: rare problem when TFS' mixed mode assemblies cannot be loaded correctly (#93)\r\n- Some fixes for Unicode filenames and TFS usernames (#80)\r\n- git-tfs exit codes are now positive\r\n- git-tfs cleans up files if clone command resulted in exception (#94)\r\n- Restored VS2008 functionality (#99)", + "draft": false, + "prerelease": false, + "created_at": "2011-11-02T12:28:53Z", + "published_at": "2013-07-16T10:14:20Z", + "assets": [ + + ] + }, + { + "url": "https://api.github.com/repos/git-tfs/git-tfs/releases/10540", + "assets_url": "https://api.github.com/repos/git-tfs/git-tfs/releases/10540/assets", + "upload_url": "https://uploads.github.com/repos/git-tfs/git-tfs/releases/10540/assets{?name}", + "html_url": "https://github.com/git-tfs/git-tfs/releases/v0.14.0", + "id": 10540, + "tag_name": "v0.14.0", + "target_commitish": "master", + "name": "", + "body": "- Fixed a bug in shelve (#133).\r\n- Fixed rename problem in checkintool (#148).\r\n- Fixed shelve -f (#157).\r\n- Fixed (or unfixed) case sensitivity (#159).\r\n- When a git subprocess exits with error, show the return/error code (#151).\r\n- Add support for VS11.", + "draft": false, + "prerelease": false, + "created_at": "2012-05-21T15:34:23Z", + "published_at": "2013-07-16T10:13:38Z", + "assets": [ + + ] + }, + { + "url": "https://api.github.com/repos/git-tfs/git-tfs/releases/10539", + "assets_url": "https://api.github.com/repos/git-tfs/git-tfs/releases/10539/assets", + "upload_url": "https://uploads.github.com/repos/git-tfs/git-tfs/releases/10539/assets{?name}", + "html_url": "https://github.com/git-tfs/git-tfs/releases/v0.15.0", + "id": 10539, + "tag_name": "v0.15.0", + "target_commitish": "master", + "name": "", + "body": "- Use [libgit2sharp](https://github.com/libgit2/libgit2sharp).\r\n- Add default comment for shelves (#187)\r\n- Add support for files with international characters (#200)\r\n- Fix the mixed case problem (once and for all?) (#213)\r\n- Add support for authors file\r\n- Set up CI with [travis](http://travis-ci.org/git-tfs/git-tfs) and [teamcity](http://teamcity.codebetter.com/)", + "draft": false, + "prerelease": false, + "created_at": "2012-09-10T13:19:18Z", + "published_at": "2013-07-16T10:13:00Z", + "assets": [ + + ] + }, + { + "url": "https://api.github.com/repos/git-tfs/git-tfs/releases/277", + "assets_url": "https://api.github.com/repos/git-tfs/git-tfs/releases/277/assets", + "upload_url": "https://uploads.github.com/repos/git-tfs/git-tfs/releases/277/assets{?name}", + "html_url": "https://github.com/git-tfs/git-tfs/releases/v0.17.2", + "id": 277, + "tag_name": "v0.17.2", + "target_commitish": "master", + "name": "0.17.2", + "body": "* Use the git author as the TFS committer during `git tfs rcheckin` (#336) and `git tfs rcheckin --quick` (#357)\r\n* Improve temporary workspace handling (#328, #372)\r\n* Use libgit2sharp more and git-core less (#361)\r\n* Bug fix for bare repositories (#352)\r\n* Bug fix for crash during `git tfs clone` (#349)\r\n* Bug fix for VS2008 (#362)\r\n* Update libgit2sharp\r\n* Improved release process (#333, #340)\r\n\r\n[Full diff](https://github.com/git-tfs/git-tfs/compare/v0.17.1...v0.17.2)", + "draft": false, + "prerelease": false, + "created_at": "2013-05-23T10:16:55Z", + "published_at": "2013-05-23T11:31:35Z", + "assets": [ + { + "url": "https://api.github.com/repos/git-tfs/git-tfs/releases/assets/931", + "id": 931, + "name": "gittfs-0.17.2.zip", + "label": "gittfs-0.17.2.zip", + "content_type": "application/zip", + "state": "uploaded", + "size": 1048704, + "download_count": 276, + "created_at": "2013-07-03T11:22:11Z", + "updated_at": "2013-07-03T11:22:25Z" + } + ] + }, + { + "url": "https://api.github.com/repos/git-tfs/git-tfs/releases/203", + "assets_url": "https://api.github.com/repos/git-tfs/git-tfs/releases/203/assets", + "upload_url": "https://uploads.github.com/repos/git-tfs/git-tfs/releases/203/assets{?name}", + "html_url": "https://github.com/git-tfs/git-tfs/releases/v0.17.1", + "id": 203, + "tag_name": "v0.17.1", + "target_commitish": "master", + "name": "v0.17.1", + "body": "- Fixed `git tfs clone` broken in some cases in 0.17 (#330)", + "draft": false, + "prerelease": false, + "created_at": "2013-03-25T15:36:32Z", + "published_at": "2013-03-25T15:43:42Z", + "assets": [ + { + "url": "https://api.github.com/repos/git-tfs/git-tfs/releases/assets/40", + "id": 40, + "name": "GitTfs-0.17.1.zip", + "label": "GitTfs-0.17.1.zip", + "content_type": "application/x-zip-compressed", + "state": "uploaded", + "size": 879773, + "download_count": 1750, + "created_at": "2013-03-25T15:40:00Z", + "updated_at": "2013-07-16T10:10:26Z" + } + ] + }, + { + "url": "https://api.github.com/repos/git-tfs/git-tfs/releases/198", + "assets_url": "https://api.github.com/repos/git-tfs/git-tfs/releases/198/assets", + "upload_url": "https://uploads.github.com/repos/git-tfs/git-tfs/releases/198/assets{?name}", + "html_url": "https://github.com/git-tfs/git-tfs/releases/v0.17.0", + "id": 198, + "tag_name": "v0.17.0", + "target_commitish": "master", + "name": "0.17.0", + "body": "- [branch](commands/branch.md)\r\n- [labels](commands/labels.md) (#256)\r\n- git tfs pull --rebase (#254)\r\n- git tfs clone --with-branches (#255)\r\n- unicode support (#204)\r\n- Use a custom workspace (#266)\r\n- Clean workspaces directory (#269)\r\n- Add a note on the commit to keep trace of the workitems (#276)\r\n- Remove orphan folders (except in specific cases) (#323)", + "draft": false, + "prerelease": false, + "created_at": "2013-03-22T01:50:42Z", + "published_at": "2013-03-22T02:14:10Z", + "assets": [ + { + "url": "https://api.github.com/repos/git-tfs/git-tfs/releases/assets/39", + "id": 39, + "name": "GitTfs-0.17.0.zip", + "label": "GitTfs-0.17.0.zip", + "content_type": "application/zip", + "state": "uploaded", + "size": 879769, + "download_count": 27, + "created_at": "2013-03-22T02:13:02Z", + "updated_at": "2013-07-16T10:10:54Z" + } + ] + }, + { + "url": "https://api.github.com/repos/git-tfs/git-tfs/releases/171", + "assets_url": "https://api.github.com/repos/git-tfs/git-tfs/releases/171/assets", + "upload_url": "https://uploads.github.com/repos/git-tfs/git-tfs/releases/171/assets{?name}", + "html_url": "https://github.com/git-tfs/git-tfs/releases/v0.16.0", + "id": 171, + "tag_name": "v0.16.0", + "target_commitish": "master", + "name": "0.16.0", + "body": "- [init-branch](commands/init-branch.md)!! (#232)\r\n- Faster clone (#226) and quick-clone.\r\n- Add `git tfs info` (#219)\r\n- Better metadata processing during rcheckin: remove the flags (#237), ignore whitespace (#238), add `git-tfs-force:` reason (#219).\r\n- Always use CRLF in TFS checkin comments (#239)\r\n- Checkin notes (#245)\r\n- Use authors file more, and save it so you don't have to tell git-tfs about it every time you need it. (#252)", + "draft": false, + "prerelease": false, + "created_at": "2012-12-05T23:02:27Z", + "published_at": "2013-03-08T12:59:16Z", + "assets": [ + { + "url": "https://api.github.com/repos/git-tfs/git-tfs/releases/assets/25", + "id": 25, + "name": "GitTfs-0.16.0.zip", + "label": "GitTfs-0.16.0.zip", + "content_type": "application/zip", + "state": "uploaded", + "size": 778026, + "download_count": 4, + "created_at": "2013-03-08T12:58:22Z", + "updated_at": "2013-07-16T10:12:00Z" + } + ] + }, + { + "url": "https://api.github.com/repos/git-tfs/git-tfs/releases/110", + "assets_url": "https://api.github.com/repos/git-tfs/git-tfs/releases/110/assets", + "upload_url": "https://uploads.github.com/repos/git-tfs/git-tfs/releases/110/assets{?name}", + "html_url": "https://github.com/git-tfs/git-tfs/releases/v0.16.1", + "id": 110, + "tag_name": "v0.16.1", + "target_commitish": "master", + "name": "0.16.1", + "body": "- Fixed `git tfs unshelve` (broken in 0.16.0) (#253).", + "draft": false, + "prerelease": false, + "created_at": "2012-12-06T16:14:15Z", + "published_at": "2013-02-19T23:16:11Z", + "assets": [ + { + "url": "https://api.github.com/repos/git-tfs/git-tfs/releases/assets/12", + "id": 12, + "name": "GitTfs-0.16.1.zip", + "label": "GitTfs-0.16.1.zip", + "content_type": "application/zip", + "state": "uploaded", + "size": 778060, + "download_count": 23, + "created_at": "2013-02-19T23:14:53Z", + "updated_at": "2013-07-16T10:11:33Z" + } + ] + } +] diff --git a/Octokit.Tests/Helpers/Arg.cs b/Octokit.Tests/Helpers/Arg.cs index e14868aa..5b967512 100644 --- a/Octokit.Tests/Helpers/Arg.cs +++ b/Octokit.Tests/Helpers/Arg.cs @@ -30,5 +30,10 @@ namespace Octokit.Tests { get { return Arg.Any(); } } + + public static string String + { + get { return Arg.Any(); } + } } } diff --git a/Octokit.Tests/Helpers/StringExtensionsTests.cs b/Octokit.Tests/Helpers/StringExtensionsTests.cs index a35df2c6..0d363272 100644 --- a/Octokit.Tests/Helpers/StringExtensionsTests.cs +++ b/Octokit.Tests/Helpers/StringExtensionsTests.cs @@ -50,5 +50,17 @@ namespace Octokit.Tests.Helpers Assert.Throws(() => "".ToRubyCase()); } } + + public class TheExpandUriTemplateMethod + { + [Theory] + [InlineData("https://host.com/path?name=other", "https://host.com/path?name=other")] + [InlineData("https://host.com/path?name=example name.txt", "https://host.com/path{?name}")] + [InlineData("https://host.com/path", "https://host.com/path{?other}")] + public void ExpandsUriTemplates(string expected, string template) + { + Assert.Equal(expected, template.ExpandUriTemplate(new { name = "example name.txt" }).ToString()); + } + } } } diff --git a/Octokit.Tests/Http/ApiConnectionTests.cs b/Octokit.Tests/Http/ApiConnectionTests.cs index 583ae635..d4521c0a 100644 --- a/Octokit.Tests/Http/ApiConnectionTests.cs +++ b/Octokit.Tests/Http/ApiConnectionTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; using NSubstitute; using Octokit.Http; @@ -31,7 +32,7 @@ namespace Octokit.Tests.Http public async Task EnsuresArgumentNotNull() { var client = new ApiConnection(Substitute.For()); - AssertEx.Throws(async () => await client.Get(null)); + await AssertEx.Throws(async () => await client.Get(null)); } } @@ -56,7 +57,7 @@ namespace Octokit.Tests.Http public async Task EnsuresArgumentNotNull() { var connection = new ApiConnection(Substitute.For()); - AssertEx.Throws(async () => await connection.GetItem(null, null)); + await AssertEx.Throws(async () => await connection.GetItem(null, null)); } } @@ -81,7 +82,7 @@ namespace Octokit.Tests.Http public async Task EnsuresArgumentNotNull() { var client = new ApiConnection(Substitute.For()); - AssertEx.Throws(async () => await client.GetHtml(null)); + await AssertEx.Throws(async () => await client.GetHtml(null)); } } @@ -112,7 +113,7 @@ namespace Octokit.Tests.Http public async Task EnsuresArgumentNotNull() { var client = new ApiConnection(Substitute.For()); - AssertEx.Throws(async () => await client.GetAll(null)); + await AssertEx.Throws(async () => await client.GetAll(null)); } } @@ -140,8 +141,8 @@ namespace Octokit.Tests.Http { var connection = new ApiConnection(Substitute.For()); var patchUri = new Uri("/", UriKind.Relative); - AssertEx.Throws(async () => await connection.Update(null, new object())); - AssertEx.Throws(async () => await connection.Update(patchUri, null)); + await AssertEx.Throws(async () => await connection.Update(null, new object())); + await AssertEx.Throws(async () => await connection.Update(patchUri, null)); } } @@ -169,8 +170,8 @@ namespace Octokit.Tests.Http { var client = new ApiConnection(Substitute.For()); var postUri = new Uri("/", UriKind.Relative); - AssertEx.Throws(async () => await client.Create(null, new object())); - AssertEx.Throws(async () => await client.Create(postUri, null)); + await AssertEx.Throws(async () => await client.Create(null, new object())); + await AssertEx.Throws(async () => await client.Create(postUri, null)); } } @@ -195,7 +196,35 @@ namespace Octokit.Tests.Http public async Task EnsuresArgumentNotNull() { var connection = new ApiConnection(Substitute.For()); - AssertEx.Throws(async () => await connection.Delete(null)); + await AssertEx.Throws(async () => await connection.Delete(null)); + } + } + + public class TheUploadMethod + { + [Fact] + public async Task MakesUploadRequest() + { + var uploadUrl = new Uri("/anything", UriKind.Relative); + IResponse response = new ApiResponse { BodyAsObject = "the response" }; + var connection = Substitute.For(); + connection.PostRawAsync(Args.Uri, Arg.Any(), Arg.Any>()).Returns(Task.FromResult(response)); + var apiConnection = new ApiConnection(connection); + var rawData = new MemoryStream(); + + await apiConnection.Upload(uploadUrl, rawData, "B"); + + connection.Received().PostRawAsync(uploadUrl, rawData, + Arg.Any>()); + } + + [Fact] + public async Task EnsuresArgumentNotNull() + { + var connection = new ApiConnection(Substitute.For()); + await AssertEx.Throws(async () => await connection.Upload(null, Stream.Null, "some-content-type")); + await AssertEx.Throws(async () => await connection.Upload(new Uri("/ok", UriKind.Relative), null, "some-content-type")); + await AssertEx.Throws(async () => await connection.Upload(new Uri("/ok", UriKind.Relative), null, null)); } } diff --git a/Octokit.Tests/Http/ConnectionTests.cs b/Octokit.Tests/Http/ConnectionTests.cs index 13b20e19..d1eb8511 100644 --- a/Octokit.Tests/Http/ConnectionTests.cs +++ b/Octokit.Tests/Http/ConnectionTests.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net; using System.Net.Http; @@ -233,6 +235,31 @@ namespace Octokit.Tests.Http } } + public class ThePostRawAsyncMethod + { + [Fact] + public async Task RunsConfiguredAppWithAppropriateEnv() + { + var httpClient = Substitute.For(); + IResponse response = new ApiResponse(); + httpClient.Send(Args.Request).Returns(Task.FromResult(response)); + var connection = new Connection(ExampleUri, + Substitute.For(), + httpClient, + Substitute.For()); + + var body = new MemoryStream(new byte[] { 48, 49, 50 }); + var headers = new Dictionary { { "Content-Type", "application/arbitrary" } }; + await connection.PostRawAsync(new Uri("https://other.host.com/path?query=val"), body, headers); + + httpClient.Received().Send(Arg.Is(req => + req.BaseAddress == ExampleUri && + req.Body == body && + req.Method == HttpMethod.Post && + req.Endpoint == new Uri("https://other.host.com/path?query=val"))); + } + } + public class TheDeleteAsyncMethod { [Fact] diff --git a/Octokit.Tests/Http/JsonHttpPipelineTests.cs b/Octokit.Tests/Http/JsonHttpPipelineTests.cs index 9f0f7b93..8b91a1ba 100644 --- a/Octokit.Tests/Http/JsonHttpPipelineTests.cs +++ b/Octokit.Tests/Http/JsonHttpPipelineTests.cs @@ -18,7 +18,7 @@ namespace Octokit.Tests.Http public class TheSerializeRequestMethod { [Fact] - public void SetsRequestHeader() + public void SetsRequestAcceptHeader() { var request = new Request(); var jsonPipeline = new JsonHttpPipeline(); @@ -29,6 +29,19 @@ namespace Octokit.Tests.Http Assert.Equal("application/vnd.github.v3+json; charset=utf-8", request.Headers["Accept"]); } + [Fact] + public void DoesNotChangeExistingAcceptsHeader() + { + var request = new Request(); + request.Headers.Add("Accept", "application/vnd.github.manifold-preview; charset=utf-8"); + var jsonPipeline = new JsonHttpPipeline(); + + jsonPipeline.SerializeRequest(request); + + Assert.Contains("Accept", request.Headers.Keys); + Assert.Equal("application/vnd.github.manifold-preview; charset=utf-8", request.Headers["Accept"]); + } + [Fact] public void LeavesStringBodyAlone() { diff --git a/Octokit.Tests/Octokit.Tests.csproj b/Octokit.Tests/Octokit.Tests.csproj index 1a2d582e..235256d6 100644 --- a/Octokit.Tests/Octokit.Tests.csproj +++ b/Octokit.Tests/Octokit.Tests.csproj @@ -51,6 +51,7 @@ + @@ -92,6 +93,9 @@ + + + diff --git a/Octokit/Clients/ReleasesClient.cs b/Octokit/Clients/ReleasesClient.cs new file mode 100644 index 00000000..7f8c5f01 --- /dev/null +++ b/Octokit/Clients/ReleasesClient.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Octokit.Http; + +namespace Octokit.Clients +{ + public class ReleasesClient : ApiClient, IReleasesClient + { + public ReleasesClient(IApiConnection client) : base(client) + { + } + + public async Task> GetAll(string owner, string name) + { + Ensure.ArgumentNotNullOrEmptyString(owner, "owner"); + Ensure.ArgumentNotNullOrEmptyString(name, "repository"); + + var endpoint = "/repos/{0}/{1}/releases".FormatUri(owner, name); + return await Client.GetAll(endpoint); + } + + + public async Task CreateRelease(string owner, string name, ReleaseUpdate data) + { + Ensure.ArgumentNotNullOrEmptyString(owner, "owner"); + Ensure.ArgumentNotNullOrEmptyString(name, "repository"); + Ensure.ArgumentNotNull(data, "data"); + + var endpoint = "/repos/{0}/{1}/releases".FormatUri(owner, name); + return await Client.Create(endpoint, data); + } + + + public async Task UploadAsset(Release release, ReleaseAssetUpload data) + { + Ensure.ArgumentNotNull(release, "release"); + Ensure.ArgumentNotNull(data, "data"); + + var endpoint = release.UploadUrl.ExpandUriTemplate(new { name = data.FileName }); + return await Client.Upload(endpoint, data.RawData, data.ContentType); + } + } +} diff --git a/Octokit/GitHubClient.cs b/Octokit/GitHubClient.cs index d11a6a9b..0399e50e 100644 --- a/Octokit/GitHubClient.cs +++ b/Octokit/GitHubClient.cs @@ -39,6 +39,7 @@ namespace Octokit AutoComplete = new AutoCompleteClient(connection); Organization = new OrganizationsClient(new ApiConnection(connection)); Repository = new RepositoriesClient(new ApiConnection(connection)); + Releases = new ReleasesClient(new ApiConnection(connection)); User = new UsersClient(new ApiConnection(connection)); SshKey = new SshKeysClient(new ApiConnection(connection)); } @@ -79,6 +80,7 @@ namespace Octokit public IAutoCompleteClient AutoComplete { get; private set; } public IOrganizationsClient Organization { get; private set; } public IRepositoriesClient Repository { get; private set; } + public IReleasesClient Releases { get; private set; } public ISshKeysClient SshKey { get; private set; } public IUsersClient User { get; private set; } } diff --git a/Octokit/GitHubModels.cs b/Octokit/GitHubModels.cs index 8d0895de..9562494b 100644 --- a/Octokit/GitHubModels.cs +++ b/Octokit/GitHubModels.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Text; using System.Threading.Tasks; using Octokit.Http; @@ -454,6 +455,62 @@ namespace Octokit public bool Primary { get; set; } } + public class Release + { + public string Url { get; set; } + public string HtmlUrl { get; set; } + public string AssetsUrl { get; set; } + public string UploadUrl { get; set; } + public int Id { get; set; } + public string TagName { get; set; } + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Commitish")] + public string TargetCommitish { get; set; } + public string Name { get; set; } + public string Body { get; set; } + public bool Draft { get; set; } + public bool Prerelease { get; set; } + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset PublishedAt { get; set; } + } + + public class ReleaseUpdate + { + public ReleaseUpdate(string tagName) + { + Ensure.ArgumentNotNullOrEmptyString(tagName, "tagName"); + TagName = tagName; + } + + public string TagName { get; private set; } + [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Commitish")] + public string TargetCommitish { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public bool Draft { get; set; } + public bool Prerelease { get; set; } + } + + public class ReleaseAsset + { + public string Url { get; set; } + public int Id { get; set; } + public string Name { get; set; } + public string Label { get; set; } + public string State { get; set; } + public string ContentType { get; set; } + public int Size { get; set; } + public int DownloadCount { get; set; } + public DateTimeOffset CreatedAt { get; set; } + public DateTimeOffset UpdatedAt { get; set; } + } + + public class ReleaseAssetUpload + { + public string FileName { get; set; } + public string ContentType { get; set; } + public Stream RawData { get; set; } + } + public class ApiError { public string Message { get; set; } diff --git a/Octokit/Helpers/StringExtensions.cs b/Octokit/Helpers/StringExtensions.cs index 7abaf279..a834271f 100644 --- a/Octokit/Helpers/StringExtensions.cs +++ b/Octokit/Helpers/StringExtensions.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; +using System.Reflection; +using System.Text.RegularExpressions; namespace Octokit { @@ -24,6 +26,31 @@ namespace Octokit return new Uri(string.Format(CultureInfo.InvariantCulture, pattern, args), UriKind.Relative); } + static Regex OptionalQueryStringRegex = new Regex("\\{\\?([^}]+)\\}"); + public static Uri ExpandUriTemplate(this string template, object values) + { + var optionalQueryStringMatch = OptionalQueryStringRegex.Match(template); + if(optionalQueryStringMatch.Success) + { + var expansion = ""; + var parameterName = optionalQueryStringMatch.Groups[1].Value; + var parameterProperty = values.GetType().GetProperty(parameterName); + if(parameterProperty != null) + { + expansion = "?" + parameterName + "=" + Uri.EscapeDataString("" + parameterProperty.GetValue(values, new object[0])); + } + template = OptionalQueryStringRegex.Replace(template, expansion); + } + return new Uri(template); + } + +#if NETFX_CORE + public static PropertyInfo GetProperty(this Type t, string propertyName) + { + return t.GetTypeInfo().GetDeclaredProperty(propertyName); + } +#endif + // :trollface: [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", Justification = "Ruby don't care. Ruby don't play that.")] diff --git a/Octokit/Http/ApiConnection.cs b/Octokit/Http/ApiConnection.cs index 3869f24e..2492b07c 100644 --- a/Octokit/Http/ApiConnection.cs +++ b/Octokit/Http/ApiConnection.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; using Octokit.Clients; @@ -91,6 +92,20 @@ namespace Octokit.Http await Connection.DeleteAsync(endpoint); } + public async Task Upload(Uri uri, Stream rawData, string contentType) + { + Ensure.ArgumentNotNull(uri, "uri"); + Ensure.ArgumentNotNull(rawData, "rawData"); + Ensure.ArgumentNotNull(contentType, "contentType"); + + var response = await Connection.PostRawAsync(uri, rawData, new Dictionary + { + { "Content-Type", contentType }, + { "Accept", "application/vnd.github.manifold-preview" } + }); + return response.BodyAsObject; + } + async Task> GetPage(Uri endpoint, IDictionary parameters) { Ensure.ArgumentNotNull(endpoint, "endpoint"); @@ -98,6 +113,5 @@ namespace Octokit.Http var response = await Connection.GetAsync>(endpoint, parameters); return new ReadOnlyPagedCollection(response, Connection); } - } } diff --git a/Octokit/Http/Connection.cs b/Octokit/Http/Connection.cs index f2d535ba..9e2bcafd 100644 --- a/Octokit/Http/Connection.cs +++ b/Octokit/Http/Connection.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; using System.Net; using System.Net.Http; using System.Threading.Tasks; @@ -102,6 +103,28 @@ namespace Octokit.Http return await SendData(endpoint, HttpMethod.Post, body); } + public async Task> PostRawAsync(Uri endpoint, Stream body, IDictionary headers) + { + Ensure.ArgumentNotNull(endpoint, "endpoint"); + Ensure.ArgumentNotNull(body, "body"); + Ensure.ArgumentNotNull(headers, "headers"); + + var request = new Request + { + Method = HttpMethod.Post, + BaseAddress = BaseAddress, + Endpoint = endpoint, + Body = body + }; + foreach (var header in headers) + { + request.Headers[header.Key] = header.Value; + } + var response = await RunRequest(request); + jsonPipeline.DeserializeResponse(response); + return response; + } + public async Task> PutAsync(Uri endpoint, object body) { return await SendData(endpoint, HttpMethod.Put, body); @@ -178,7 +201,7 @@ namespace Octokit.Http { if (response.StatusCode == HttpStatusCode.Unauthorized) throw new AuthorizationException("You must be authenticated to call this method. Either supply a " + - "login/password or an oauth token."); + "login/password or an oauth token."); if (response.StatusCode == HttpStatusCode.Forbidden) { diff --git a/Octokit/Http/HttpClientAdapter.cs b/Octokit/Http/HttpClientAdapter.cs index b0a1461e..9cfbcd5d 100644 --- a/Octokit/Http/HttpClientAdapter.cs +++ b/Octokit/Http/HttpClientAdapter.cs @@ -58,6 +58,11 @@ namespace Octokit.Http { requestMessage.Content = new StringContent(body, Encoding.UTF8); } + var bodyStream = request.Body as System.IO.Stream; + if (bodyStream != null) + { + requestMessage.Content = new StreamContent(bodyStream); + } } catch (Exception) { diff --git a/Octokit/Http/IApiConnection.cs b/Octokit/Http/IApiConnection.cs index 73e06011..6dc81290 100644 --- a/Octokit/Http/IApiConnection.cs +++ b/Octokit/Http/IApiConnection.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Threading.Tasks; namespace Octokit.Http @@ -20,5 +21,6 @@ namespace Octokit.Http Task Create(Uri endpoint, object data); Task Update(Uri endpoint, object data); Task Delete(Uri endpoint); + Task Upload(Uri uri, Stream rawData, string contentType); } } \ No newline at end of file diff --git a/Octokit/Http/IConnection.cs b/Octokit/Http/IConnection.cs index f89840fa..47a0689c 100644 --- a/Octokit/Http/IConnection.cs +++ b/Octokit/Http/IConnection.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Threading.Tasks; namespace Octokit.Http @@ -11,6 +12,7 @@ namespace Octokit.Http Task> GetAsync(Uri endpoint, IDictionary parameters); Task> PatchAsync(Uri endpoint, object body); Task> PostAsync(Uri endpoint, object body); + Task> PostRawAsync(Uri endpoint, Stream body, IDictionary headers); Task> PutAsync(Uri endpoint, object body); [SuppressMessage("Microsoft.Design", "CA1004:GenericMethodsShouldProvideTypeParameter")] diff --git a/Octokit/Http/JsonHttpPipeline.cs b/Octokit/Http/JsonHttpPipeline.cs index ccc9ee0f..90d90295 100644 --- a/Octokit/Http/JsonHttpPipeline.cs +++ b/Octokit/Http/JsonHttpPipeline.cs @@ -25,7 +25,13 @@ namespace Octokit.Http { Ensure.ArgumentNotNull(request, "request"); - request.Headers["Accept"] = "application/vnd.github.v3+json; charset=utf-8"; + if (!request.Headers.ContainsKey("Accept")) + { + request.Headers["Accept"] = "application/vnd.github.v3+json; charset=utf-8"; + } + + if (request.Endpoint != null && request.Endpoint.ToString().Contains("releases")) + request.Headers["Accept"] = "application/vnd.github.manifold-preview; charset=utf-8"; if (request.Method == HttpMethod.Get || request.Body == null) return; if (request.Body is string) return; diff --git a/Octokit/IGitHubClient.cs b/Octokit/IGitHubClient.cs index a497fbc0..e455e4b9 100644 --- a/Octokit/IGitHubClient.cs +++ b/Octokit/IGitHubClient.cs @@ -10,6 +10,7 @@ namespace Octokit IAutoCompleteClient AutoComplete { get; } IOrganizationsClient Organization { get; } IRepositoriesClient Repository { get; } + IReleasesClient Releases { get; } ISshKeysClient SshKey { get; } IUsersClient User { get; } } diff --git a/Octokit/IReleasesClient.cs b/Octokit/IReleasesClient.cs new file mode 100644 index 00000000..6be68490 --- /dev/null +++ b/Octokit/IReleasesClient.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Octokit +{ + public interface IReleasesClient + { + /// + /// Retrieves every for the specified repository. + /// + /// The owner of the repository. + /// The name of the repository. + /// A of . + Task> GetAll(string owner, string name); + + /// + /// Create a for the specified repository. + /// + /// The owner of the repository. + /// The name of the repository. + /// The data for the release. + /// A new . + Task CreateRelease(string owner, string name, ReleaseUpdate data); + + /// + /// Upload a for the specified release. + /// + /// The to attach the asset to. + /// The asset information. + /// A new . + Task UploadAsset(Release release, ReleaseAssetUpload data); + } +} diff --git a/Octokit/IRepositoriesClient.cs b/Octokit/IRepositoriesClient.cs index 347e1ef8..41200131 100644 --- a/Octokit/IRepositoriesClient.cs +++ b/Octokit/IRepositoriesClient.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.IO; using System.Threading.Tasks; namespace Octokit diff --git a/Octokit/Octokit.csproj b/Octokit/Octokit.csproj index dd4104b5..1672bee8 100644 --- a/Octokit/Octokit.csproj +++ b/Octokit/Octokit.csproj @@ -86,6 +86,7 @@ + @@ -106,6 +107,7 @@ + diff --git a/Octokit/OctokitRT.csproj b/Octokit/OctokitRT.csproj index 3e9254aa..1b1519f0 100644 --- a/Octokit/OctokitRT.csproj +++ b/Octokit/OctokitRT.csproj @@ -110,6 +110,7 @@ + @@ -153,6 +154,7 @@ +