Files

449 lines
20 KiB
C#

using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Octokit;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Common.Tests;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.System;
using Tgstation.Server.Host.Tests;
namespace Tgstation.Server.Host.Utils.GitHub.Tests
{
[TestClass]
public sealed class TestGitHubClientFactory
{
const string FakePrivateKey = @"-----BEGIN RSA PRIVATE KEY-----
MIICWgIBAAKBgFGAf2dPjiPMmX0tD4SXtp7Mc6q4Z1uiCUuL3ms9lwAjz29d/yVv
TlJIxcv8OQBamV0XTFJK+leAjBR/8ZwEumKvV/GgZzZsNny7xFgDLIugLaan2SSp
uNab05d9q+btGw6f8VlSmz8EbCUm0vc803uSLW14tHTMw8g3pqsPaYWrAgMBAAEC
gYAR5EaTtGg8vaaYTzMP5YpGlBV0TeUKvaiSuBLMsgmbE0FvblAbtLKZ57XcUPNm
e30dbYboreMFIIgf2/J7UKJl1hNCZUeh8FU2dgArDjx7Ffl1GKyiv/j+/YBgPs0O
+fZerbQSFybN/XQnb2PRt6KyXdGTxRRszY66VUxkcrYOCQJBAJm/UOqGMpZxWKVJ
8kU6+bX7nNO/uRR5KNnjW9Yu7C2jWlbw5RHSzreCwKdh/75l0fLymQ4G/j2lSLw4
v5vdtZUCQQCHtNcmaB6mpjgMd3779qKvsotUaltcqzmygS+odiIAMgeqcgASt3Fq
trxIb5zO8CmfKN2D9YxNroX1aToG8P4/AkBcWLF9bEWOX15jSVsfgiDi0dKMzSeZ
yHxlA07yAxURBIEKn363ietoBj05TH+UGQxV5KlR55ll5ZUemOdd83lRAkAJrSJe
MbRly5pZgTfDvY1SG9gFd+P10pu1l7KPP4UjIG4dgC1zKDNlGYyJWbQDqWo+WAqb
eym67EPPAObLUem/AkAbebRFp6RCTJBIFkI2GlJGabab96pEno4gdmn0fyUGhf+9
IDFvEHaDd7T08wT0pD0vTs0+UH/r/AjmXnWmrCPi
-----END RSA PRIVATE KEY-----";
ILoggerFactory loggerFactory;
[TestInitialize]
public void Initialize()
{
loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddConsole();
builder.SetMinimumLevel(LogLevel.Trace);
});
}
[TestCleanup]
public void Cleanup()
{
loggerFactory.Dispose();
}
[TestMethod]
public void TestContructionThrows()
{
Assert.ThrowsExactly<ArgumentNullException>(() => new GitHubClientFactory(null, null, null, null));
Assert.ThrowsExactly<ArgumentNullException>(() => new GitHubClientFactory(Mock.Of<IAssemblyInformationProvider>(), null, null, null));
Assert.ThrowsExactly<ArgumentNullException>(() => new GitHubClientFactory(Mock.Of<IAssemblyInformationProvider>(), Mock.Of<IHttpMessageHandlerFactory>(), null, null));
Assert.ThrowsExactly<ArgumentNullException>(() => new GitHubClientFactory(Mock.Of<IAssemblyInformationProvider>(), Mock.Of<IHttpMessageHandlerFactory>(), Mock.Of<IOptionsMonitor<GeneralConfiguration>>(), null));
}
[TestMethod]
public async Task TestCreateBasicClient()
{
var mockApp = new Mock<IAssemblyInformationProvider>();
mockApp.SetupGet(x => x.ProductInfoHeaderValue).Returns(new ProductInfoHeaderValue("TGSTests", "1.2.3")).Verifiable();
var mockOptions = new Mock<IOptionsMonitor<GeneralConfiguration>>();
var gc = new GeneralConfiguration();
Assert.IsNull(gc.GitHubAccessToken);
mockOptions.SetupGet(x => x.CurrentValue).Returns(gc);
var factory = new GitHubClientFactory(mockApp.Object, new BasicHttpMessageHandlerFactory(), mockOptions.Object, loggerFactory.CreateLogger<GitHubClientFactory>());
var client = await factory.CreateClient(CancellationToken.None);
Assert.IsNotNull(client);
var credentials = await client.Connection.CredentialStore.GetCredentials();
Assert.AreEqual(AuthenticationType.Anonymous, credentials.AuthenticationType);
gc.GitHubAccessToken = "asdfasdfasdfasdfasdfasdf";
client = await factory.CreateClient(CancellationToken.None);
Assert.IsNotNull(client);
credentials = await client.Connection.CredentialStore.GetCredentials();
Assert.AreEqual(AuthenticationType.Oauth, credentials.AuthenticationType);
mockApp.VerifyAll();
}
[TestMethod]
public async Task TestCreateTokenClient()
{
var mockApp = new Mock<IAssemblyInformationProvider>();
mockApp.SetupGet(x => x.ProductInfoHeaderValue).Returns(new ProductInfoHeaderValue("TGSTests", "1.2.3")).Verifiable();
var mockOptions = new Mock<IOptionsMonitor<GeneralConfiguration>>();
mockOptions.SetupGet(x => x.CurrentValue).Returns(new GeneralConfiguration());
var factory = new GitHubClientFactory(mockApp.Object, new BasicHttpMessageHandlerFactory(), mockOptions.Object, loggerFactory.CreateLogger<GitHubClientFactory>());
await Assert.ThrowsExactlyAsync<ArgumentNullException>(() => factory.CreateClient(null, CancellationToken.None).AsTask());
var client = await factory.CreateClient("asdf", CancellationToken.None);
Assert.IsNotNull(client);
var credentials = await client.Connection.CredentialStore.GetCredentials();
Assert.AreEqual(AuthenticationType.Oauth, credentials.AuthenticationType);
mockApp.VerifyAll();
}
[TestMethod]
public async Task TestCreateEncodedClientRealKey()
{
var mockApp = new Mock<IAssemblyInformationProvider>();
mockApp.SetupGet(x => x.ProductInfoHeaderValue).Returns(new ProductInfoHeaderValue("TGSTests", "1.2.3")).Verifiable();
var mockOptions = new Mock<IOptionsMonitor<GeneralConfiguration>>();
mockOptions.SetupGet(x => x.CurrentValue).Returns(new GeneralConfiguration());
var factory = new GitHubClientFactory(mockApp.Object, new BasicHttpMessageHandlerFactory(), mockOptions.Object, loggerFactory.CreateLogger<GitHubClientFactory>());
var appID = Environment.GetEnvironmentVariable("TGS_TEST_APP_ID");
var privateKey = Environment.GetEnvironmentVariable("TGS_TEST_APP_PRIVATE_KEY");
var repoSlug = Environment.GetEnvironmentVariable("TGS_TEST_REPO_SLUG");
if (String.IsNullOrWhiteSpace(appID))
Assert.Inconclusive("Missing App ID");
if (String.IsNullOrWhiteSpace(privateKey))
Assert.Inconclusive("Missing App Private Key");
if (String.IsNullOrWhiteSpace(repoSlug))
Assert.Inconclusive("Missing repo slug");
var fakeAccessString = $"{RepositorySettings.TgsAppPrivateKeyPrefix}{appID}:{Convert.ToBase64String(Encoding.UTF8.GetBytes(privateKey))}";
var slugSplits = repoSlug.Split('/');
Assert.AreEqual(2, slugSplits.Length);
var client = await factory.CreateClientForRepository(fakeAccessString, new RepositoryIdentifier(slugSplits[0], slugSplits[1]), CancellationToken.None);
Assert.IsNotNull(client);
var credentials = await client.Connection.CredentialStore.GetCredentials();
Assert.AreEqual(AuthenticationType.Oauth, credentials.AuthenticationType);
mockApp.VerifyAll();
}
[TestMethod]
public async Task TestCreateEncodedClientFakeKey()
{
var mockApp = new Mock<IAssemblyInformationProvider>();
mockApp.SetupGet(x => x.ProductInfoHeaderValue).Returns(new ProductInfoHeaderValue("TGSTests", "1.2.3")).Verifiable();
var mockOptions = new Mock<IOptionsMonitor<GeneralConfiguration>>();
mockOptions.SetupGet(x => x.CurrentValue).Returns(new GeneralConfiguration());
var factory = new GitHubClientFactory(mockApp.Object, new BasicHttpMessageHandlerFactory(), mockOptions.Object, loggerFactory.CreateLogger<GitHubClientFactory>());
await Assert.ThrowsExactlyAsync<ArgumentNullException>(() => factory.CreateClient(null, CancellationToken.None).AsTask());
var fakeAccessString = $"{RepositorySettings.TgsAppPrivateKeyPrefix}123:{Convert.ToBase64String(Encoding.UTF8.GetBytes(FakePrivateKey))}";
var client = await factory.CreateClientForRepository(fakeAccessString, new RepositoryIdentifier("fake_owner", "fake_name"), CancellationToken.None);
Assert.IsNull(client);
mockApp.VerifyAll();
}
[TestMethod]
public async Task TestBasicClientCaching()
{
var mockApp = new Mock<IAssemblyInformationProvider>();
mockApp.SetupGet(x => x.ProductInfoHeaderValue).Returns(new ProductInfoHeaderValue("TGSTests", "1.2.3")).Verifiable();
var mockOptions = new Mock<IOptionsMonitor<GeneralConfiguration>>();
mockOptions.SetupGet(x => x.CurrentValue).Returns(new GeneralConfiguration());
var factory = new GitHubClientFactory(mockApp.Object, new BasicHttpMessageHandlerFactory(), mockOptions.Object, loggerFactory.CreateLogger<GitHubClientFactory>());
var client1 = await factory.CreateClient(CancellationToken.None);
var client2 = await factory.CreateClient("asdf", CancellationToken.None);
var client3 = await factory.CreateClient(CancellationToken.None);
var client4 = await factory.CreateClient("asdf", CancellationToken.None);
Assert.AreSame(client1, client3);
Assert.AreSame(client2, client4);
}
[TestMethod]
public async Task TestAppClientCaching()
{
var mockApp = new Mock<IAssemblyInformationProvider>();
mockApp.SetupGet(x => x.ProductInfoHeaderValue).Returns(new ProductInfoHeaderValue("TGSTests", "1.2.3")).Verifiable();
const int MockAppId = 123;
const int MockRepoId = 42;
const int MockInstallationId = 542;
const string MockAccessToken = "asdf_2134_im_the_installation_access_token";
DateTimeOffset? mockExpiry = null;
var mockMessageHandler = new MockHttpMessageHandler(
(request, cancellationToken) =>
{
string json;
string path = request.RequestUri.AbsolutePath;
if (path == $"/repositories/{MockRepoId}/installation")
json = @"{
""id"": " + MockInstallationId + @",
""account"": {
""login"": ""octocat"",
""id"": 1,
""node_id"": ""MDQ6VXNlcjE="",
""avatar_url"": ""https://github.com/images/error/octocat_happy.gif"",
""gravatar_id"": """",
""url"": ""https://api.github.com/users/octocat"",
""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"",
""repos_url"": ""https://api.github.com/users/octocat/repos"",
""events_url"": ""https://api.github.com/users/octocat/events{/privacy}"",
""received_events_url"": ""https://api.github.com/users/octocat/received_events"",
""type"": ""User"",
""site_admin"": false
},
""access_tokens_url"": ""https://api.github.com/app/installations/1/access_tokens"",
""repositories_url"": ""https://api.github.com/installation/repositories"",
""html_url"": ""https://github.com/organizations/github/settings/installations/1"",
""app_id"": 1,
""target_id"": 1,
""target_type"": ""Organization"",
""permissions"": {
""checks"": ""write"",
""metadata"": ""read"",
""contents"": ""read""
},
""events"": [
""push"",
""pull_request""
],
""single_file_name"": ""config.yaml"",
""has_multiple_single_files"": true,
""single_file_paths"": [
""config.yml"",
"".github/issue_TEMPLATE.md""
],
""repository_selection"": ""selected"",
""created_at"": ""2017-07-08T16:18:44-04:00"",
""updated_at"": ""2017-07-08T16:18:44-04:00"",
""app_slug"": ""github-actions"",
""suspended_at"": null,
""suspended_by"": null
}";
else if (path == $"/app/installations/{MockInstallationId}/access_tokens")
json = @"{
""token"": """ + MockAccessToken + @""",
""expires_at"": """ + (mockExpiry = DateTime.UtcNow.AddMinutes(15).AddSeconds(1)).Value.ToString("O") + @""",
""permissions"": {
""issues"": ""write"",
""contents"": ""read""
},
""repository_selection"": ""selected"",
""repositories"": [
{
""id"": 1296269,
""node_id"": ""MDEwOlJlcG9zaXRvcnkxMjk2MjY5"",
""name"": ""Hello-World"",
""full_name"": ""octocat/Hello-World"",
""owner"": {
""login"": ""octocat"",
""id"": 1,
""node_id"": ""MDQ6VXNlcjE="",
""avatar_url"": ""https://github.com/images/error/octocat_happy.gif"",
""gravatar_id"": """",
""url"": ""https://api.github.com/users/octocat"",
""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"",
""repos_url"": ""https://api.github.com/users/octocat/repos"",
""events_url"": ""https://api.github.com/users/octocat/events{/privacy}"",
""received_events_url"": ""https://api.github.com/users/octocat/received_events"",
""type"": ""User"",
""site_admin"": false
},
""private"": false,
""html_url"": ""https://github.com/octocat/Hello-World"",
""description"": ""This your first repo!"",
""fork"": false,
""url"": ""https://api.github.com/repos/octocat/Hello-World"",
""archive_url"": ""https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}"",
""assignees_url"": ""https://api.github.com/repos/octocat/Hello-World/assignees{/user}"",
""blobs_url"": ""https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}"",
""branches_url"": ""https://api.github.com/repos/octocat/Hello-World/branches{/branch}"",
""collaborators_url"": ""https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}"",
""comments_url"": ""https://api.github.com/repos/octocat/Hello-World/comments{/number}"",
""commits_url"": ""https://api.github.com/repos/octocat/Hello-World/commits{/sha}"",
""compare_url"": ""https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}"",
""contents_url"": ""https://api.github.com/repos/octocat/Hello-World/contents/{+path}"",
""contributors_url"": ""https://api.github.com/repos/octocat/Hello-World/contributors"",
""deployments_url"": ""https://api.github.com/repos/octocat/Hello-World/deployments"",
""downloads_url"": ""https://api.github.com/repos/octocat/Hello-World/downloads"",
""events_url"": ""https://api.github.com/repos/octocat/Hello-World/events"",
""forks_url"": ""https://api.github.com/repos/octocat/Hello-World/forks"",
""git_commits_url"": ""https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}"",
""git_refs_url"": ""https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}"",
""git_tags_url"": ""https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}"",
""git_url"": ""git:github.com/octocat/Hello-World.git"",
""issue_comment_url"": ""https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}"",
""issue_events_url"": ""https://api.github.com/repos/octocat/Hello-World/issues/events{/number}"",
""issues_url"": ""https://api.github.com/repos/octocat/Hello-World/issues{/number}"",
""keys_url"": ""https://api.github.com/repos/octocat/Hello-World/keys{/key_id}"",
""labels_url"": ""https://api.github.com/repos/octocat/Hello-World/labels{/name}"",
""languages_url"": ""https://api.github.com/repos/octocat/Hello-World/languages"",
""merges_url"": ""https://api.github.com/repos/octocat/Hello-World/merges"",
""milestones_url"": ""https://api.github.com/repos/octocat/Hello-World/milestones{/number}"",
""notifications_url"": ""https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}"",
""pulls_url"": ""https://api.github.com/repos/octocat/Hello-World/pulls{/number}"",
""releases_url"": ""https://api.github.com/repos/octocat/Hello-World/releases{/id}"",
""ssh_url"": ""git@github.com:octocat/Hello-World.git"",
""stargazers_url"": ""https://api.github.com/repos/octocat/Hello-World/stargazers"",
""statuses_url"": ""https://api.github.com/repos/octocat/Hello-World/statuses/{sha}"",
""subscribers_url"": ""https://api.github.com/repos/octocat/Hello-World/subscribers"",
""subscription_url"": ""https://api.github.com/repos/octocat/Hello-World/subscription"",
""tags_url"": ""https://api.github.com/repos/octocat/Hello-World/tags"",
""teams_url"": ""https://api.github.com/repos/octocat/Hello-World/teams"",
""trees_url"": ""https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}"",
""clone_url"": ""https://github.com/octocat/Hello-World.git"",
""mirror_url"": ""git:git.example.com/octocat/Hello-World"",
""hooks_url"": ""https://api.github.com/repos/octocat/Hello-World/hooks"",
""svn_url"": ""https://svn.github.com/octocat/Hello-World"",
""homepage"": ""https://github.com"",
""language"": null,
""forks_count"": 9,
""stargazers_count"": 80,
""watchers_count"": 80,
""size"": 108,
""default_branch"": ""master"",
""open_issues_count"": 0,
""is_template"": true,
""topics"": [
""octocat"",
""atom"",
""electron"",
""api""
],
""has_issues"": true,
""has_projects"": true,
""has_wiki"": true,
""has_pages"": false,
""has_downloads"": true,
""archived"": false,
""disabled"": false,
""visibility"": ""public"",
""pushed_at"": ""2011-01-26T19:06:43Z"",
""created_at"": ""2011-01-26T19:01:12Z"",
""updated_at"": ""2011-01-26T19:14:43Z"",
""permissions"": {
""admin"": false,
""push"": false,
""pull"": true
},
""allow_rebase_merge"": true,
""template_repository"": null,
""temp_clone_token"": ""ABTLWHOULUVAXGTRYU7OC2876QJ2O"",
""allow_squash_merge"": true,
""allow_auto_merge"": false,
""delete_branch_on_merge"": true,
""allow_merge_commit"": true,
""subscribers_count"": 42,
""network_count"": 0,
""license"": {
""key"": ""mit"",
""name"": ""MIT License"",
""url"": ""https://api.github.com/licenses/mit"",
""spdx_id"": ""MIT"",
""node_id"": ""MDc6TGljZW5zZW1pdA=="",
""html_url"": ""https://github.com/licenses/mit""
},
""forks"": 1,
""open_issues"": 1,
""watchers"": 1
}
]
}";
else
{
Assert.Fail($"Unrecognized request path: {request.RequestUri}");
return Task.FromResult(new HttpResponseMessage());
}
var result = new HttpResponseMessage();
result.StatusCode = HttpStatusCode.OK;
result.Content = new StringContent(json);
result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
return Task.FromResult(result);
});
var mockMessageHandlerFactory = new Mock<IHttpMessageHandlerFactory>();
mockMessageHandlerFactory.Setup(x => x.CreateHandler(It.IsAny<string>())).Returns(mockMessageHandler);
var mockOptions = new Mock<IOptionsMonitor<GeneralConfiguration>>();
mockOptions.SetupGet(x => x.CurrentValue).Returns(new GeneralConfiguration());
var factory = new GitHubClientFactory(mockApp.Object, mockMessageHandlerFactory.Object, mockOptions.Object, loggerFactory.CreateLogger<GitHubClientFactory>());
var fakeAccessString = $"{RepositorySettings.TgsAppPrivateKeyPrefix}{MockAppId}:{Convert.ToBase64String(Encoding.UTF8.GetBytes(FakePrivateKey))}";
var repoIdentifier = new RepositoryIdentifier(MockRepoId);
var client1 = await factory.CreateClientForRepository(fakeAccessString, repoIdentifier, CancellationToken.None);
Assert.IsNotNull(client1);
Assert.AreEqual(MockAccessToken, client1.Connection.Credentials.GetToken());
var client2 = await factory.CreateClientForRepository(fakeAccessString, repoIdentifier, CancellationToken.None);
Assert.AreSame(client1, client2);
IGitHubClient client3 = null;
for (var i = 0; i < 100; ++i)
{
await Task.Delay(TimeSpan.FromMilliseconds(50));
client3 = await factory.CreateClientForRepository(fakeAccessString, repoIdentifier, CancellationToken.None);
if (client3 != client2)
break;
}
Assert.AreNotSame(client2, client3);
Assert.IsNotNull(client3);
Assert.AreEqual(MockAccessToken, client3.Connection.Credentials.GetToken());
}
}
}