diff --git a/Octokit.Tests/Octokit.Tests.csproj b/Octokit.Tests/Octokit.Tests.csproj
index af4759db..1b1e35de 100644
--- a/Octokit.Tests/Octokit.Tests.csproj
+++ b/Octokit.Tests/Octokit.Tests.csproj
@@ -104,6 +104,7 @@
+
diff --git a/Octokit.Tests/Reactive/ObservableMilestonesClientTests.cs b/Octokit.Tests/Reactive/ObservableMilestonesClientTests.cs
new file mode 100644
index 00000000..baa18f30
--- /dev/null
+++ b/Octokit.Tests/Reactive/ObservableMilestonesClientTests.cs
@@ -0,0 +1,264 @@
+using System;
+using System.Collections.Generic;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using NSubstitute;
+using Octokit;
+using Octokit.Internal;
+using Octokit.Reactive.Clients;
+using Octokit.Tests.Helpers;
+using Xunit;
+
+namespace Octokit.Tests.Reactive
+{
+ public class ObservableMilestonesClientTests
+ {
+ public class TheGetMethod
+ {
+ [Fact]
+ public void GetsFromClientIssueMilestone()
+ {
+ var gitHubClient = Substitute.For();
+ var client = new ObservableMilestonesClient(gitHubClient);
+
+ client.Get("fake", "repo", 42);
+
+ gitHubClient.Issue.Milestone.Received().Get("fake", "repo", 42);
+ }
+
+ [Fact]
+ public async Task EnsuresNonNullArguments()
+ {
+ var client = new ObservableMilestonesClient(Substitute.For());
+
+ await AssertEx.Throws(async () => await client.Get(null, "name", 1));
+ await AssertEx.Throws(async () => await client.Get("owner", null, 1));
+ await AssertEx.Throws(async () => await client.Get(null, "", 1));
+ await AssertEx.Throws(async () => await client.Get("", null, 1));
+ }
+ }
+
+ public class TheGetForRepositoryMethod
+ {
+ [Fact]
+ public void ReturnsEveryPageOfMilestones()
+ {
+ var firstPageUrl = new Uri("repos/fake/repo/milestones", UriKind.Relative);
+ var secondPageUrl = new Uri("https://example.com/page/2");
+ var firstPageLinks = new Dictionary {{"next", secondPageUrl}};
+ var firstPageResponse = new ApiResponse>
+ {
+ BodyAsObject = new List
+ {
+ new Milestone {Number = 1},
+ new Milestone {Number = 2},
+ new Milestone {Number = 3},
+ },
+ ApiInfo = CreateApiInfo(firstPageLinks)
+ };
+ var thirdPageUrl = new Uri("https://example.com/page/3");
+ var secondPageLinks = new Dictionary {{"next", thirdPageUrl}};
+ var secondPageResponse = new ApiResponse>
+ {
+ BodyAsObject = new List
+ {
+ new Milestone {Number = 4},
+ new Milestone {Number = 5},
+ new Milestone {Number = 6},
+ },
+ ApiInfo = CreateApiInfo(secondPageLinks)
+ };
+ var lastPageResponse = new ApiResponse>
+ {
+ BodyAsObject = new List
+ {
+ new Milestone {Number = 7},
+ },
+ ApiInfo = CreateApiInfo(new Dictionary())
+ };
+ var gitHubClient = Substitute.For();
+ gitHubClient.Connection.GetAsync>(firstPageUrl, null, null)
+ .Returns(Task.Factory.StartNew>>(() => firstPageResponse));
+ gitHubClient.Connection.GetAsync>(secondPageUrl, null, null)
+ .Returns(Task.Factory.StartNew>>(() => secondPageResponse));
+ gitHubClient.Connection.GetAsync>(thirdPageUrl, null, null)
+ .Returns(Task.Factory.StartNew>>(() => lastPageResponse));
+ var client = new ObservableMilestonesClient(gitHubClient);
+
+ var results = client.GetForRepository("fake", "repo").ToArray().Wait();
+
+ Assert.Equal(7, results.Length);
+ Assert.Equal(firstPageResponse.BodyAsObject[0].Number, results[0].Number);
+ Assert.Equal(secondPageResponse.BodyAsObject[1].Number, results[4].Number);
+ Assert.Equal(lastPageResponse.BodyAsObject[0].Number, results[6].Number);
+ }
+
+ [Fact]
+ public void SendsAppropriateParameters()
+ {
+ var firstPageUrl = new Uri("repos/fake/repo/milestones", UriKind.Relative);
+ var secondPageUrl = new Uri("https://example.com/page/2");
+ var firstPageLinks = new Dictionary {{"next", secondPageUrl}};
+ var firstPageResponse = new ApiResponse>
+ {
+ BodyAsObject = new List
+ {
+ new Milestone {Number = 1},
+ new Milestone {Number = 2},
+ new Milestone {Number = 3},
+ },
+ ApiInfo = CreateApiInfo(firstPageLinks)
+ };
+ var thirdPageUrl = new Uri("https://example.com/page/3");
+ var secondPageLinks = new Dictionary {{"next", thirdPageUrl}};
+ var secondPageResponse = new ApiResponse>
+ {
+ BodyAsObject = new List
+ {
+ new Milestone {Number = 4},
+ new Milestone {Number = 5},
+ new Milestone {Number = 6},
+ },
+ ApiInfo = CreateApiInfo(secondPageLinks)
+ };
+ var lastPageResponse = new ApiResponse>
+ {
+ BodyAsObject = new List
+ {
+ new Milestone {Number = 7},
+ },
+ ApiInfo = CreateApiInfo(new Dictionary())
+ };
+ var gitHubClient = Substitute.For();
+ gitHubClient.Connection.GetAsync>(Arg.Is(firstPageUrl),
+ Arg.Is>(d => d.Count == 3
+ && d["direction"] == "desc"
+ && d["state"] == "open"
+ && d["sort"] == "due_date"), Arg.Any())
+ .Returns(Task.Factory.StartNew>>(() => firstPageResponse));
+ gitHubClient.Connection.GetAsync>(secondPageUrl, null, null)
+ .Returns(Task.Factory.StartNew>>(() => secondPageResponse));
+ gitHubClient.Connection.GetAsync>(thirdPageUrl, null, null)
+ .Returns(Task.Factory.StartNew>>(() => lastPageResponse));
+ var client = new ObservableMilestonesClient(gitHubClient);
+
+ var results = client.GetForRepository("fake", "repo", new MilestoneRequest { SortDirection = SortDirection.Descending }).ToArray().Wait();
+
+ Assert.Equal(7, results.Length);
+ Assert.Equal(firstPageResponse.BodyAsObject[0].Number, results[0].Number);
+ Assert.Equal(secondPageResponse.BodyAsObject[1].Number, results[4].Number);
+ Assert.Equal(lastPageResponse.BodyAsObject[0].Number, results[6].Number);
+ }
+ }
+
+ public class TheCreateMethod
+ {
+ [Fact]
+ public void CreatesFromClientIssueMilestone()
+ {
+ var newMilestone = new NewMilestone("some title");
+ var gitHubClient = Substitute.For();
+ var client = new ObservableMilestonesClient(gitHubClient);
+
+ client.Create("fake", "repo", newMilestone);
+
+ gitHubClient.Issue.Milestone.Received().Create("fake", "repo", newMilestone);
+ }
+
+ [Fact]
+ public async Task EnsuresArgumentsNotNull()
+ {
+ var gitHubClient = Substitute.For();
+ var client = new ObservableMilestonesClient(gitHubClient);
+
+ AssertEx.Throws(async () => await
+ client.Create(null, "name", new NewMilestone("title")));
+ AssertEx.Throws(async () => await
+ client.Create("", "name", new NewMilestone("x")));
+ AssertEx.Throws(async () => await
+ client.Create("owner", null, new NewMilestone("x")));
+ AssertEx.Throws(async () => await
+ client.Create("owner", "", new NewMilestone("x")));
+ AssertEx.Throws(async () => await
+ client.Create("owner", "name", null));
+ }
+ }
+
+ public class TheUpdateMethod
+ {
+ [Fact]
+ public void UpdatesClientIssueMilestone()
+ {
+ var milestoneUpdate = new MilestoneUpdate();
+ var gitHubClient = Substitute.For();
+ var client = new ObservableMilestonesClient(gitHubClient);
+
+ client.Update("fake", "repo", 42, milestoneUpdate);
+
+ gitHubClient.Issue.Milestone.Received().Update("fake", "repo", 42, milestoneUpdate);
+ }
+
+ [Fact]
+ public async Task EnsuresArgumentsNotNull()
+ {
+ var gitHubClient = Substitute.For();
+ var client = new ObservableMilestonesClient(gitHubClient);
+
+ AssertEx.Throws(async () => await
+ client.Create(null, "name", new NewMilestone("title")));
+ AssertEx.Throws(async () => await
+ client.Create("", "name", new NewMilestone("x")));
+ AssertEx.Throws(async () => await
+ client.Create("owner", null, new NewMilestone("x")));
+ AssertEx.Throws(async () => await
+ client.Create("owner", "", new NewMilestone("x")));
+ AssertEx.Throws(async () => await
+ client.Create("owner", "name", null));
+ }
+ }
+
+ public class TheDeleteMethod
+ {
+ [Fact]
+ public void DeletesFromClientIssueMilestone()
+ {
+ var gitHubClient = Substitute.For();
+ var client = new ObservableMilestonesClient(gitHubClient);
+
+ client.Delete("fake", "repo", 42);
+
+ gitHubClient.Issue.Milestone.Received().Delete("fake", "repo", 42);
+ }
+
+ [Fact]
+ public async Task EnsuresArgumentsNotNull()
+ {
+ var gitHubClient = Substitute.For();
+ var client = new ObservableMilestonesClient(gitHubClient);
+
+ AssertEx.Throws(async () => await
+ client.Delete(null, "name", 42));
+ AssertEx.Throws(async () => await
+ client.Delete("", "name", 42));
+ AssertEx.Throws(async () => await
+ client.Delete("owner", null, 42));
+ AssertEx.Throws(async () => await
+ client.Delete("owner", "", 42));
+ }
+ }
+
+ public class TheCtor
+ {
+ [Fact]
+ public void EnsuresArgument()
+ {
+ Assert.Throws(() => new MilestonesClient(null));
+ }
+ }
+
+ static ApiInfo CreateApiInfo(IDictionary links)
+ {
+ return new ApiInfo(links, new List(), new List(), "etag", new RateLimit(new Dictionary()));
+ }
+ }
+}