Skip to content

Commit

Permalink
Reintroduce api factories into ado2gh (#202)
Browse files Browse the repository at this point in the history
  • Loading branch information
ArinGhazarian authored Jan 17, 2022
1 parent dd866b4 commit 0332324
Show file tree
Hide file tree
Showing 38 changed files with 390 additions and 128 deletions.
4 changes: 2 additions & 2 deletions RELEASENOTES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Release Notes

- Renamed the CLI from octoshift to ado2gh to indicate that this one is specifically for Azure DevOps -> GitHub migrations (in the future there will be additional CLI's for other migration scenarios)
- Released gei.exe that adds support for Github -> Github migrations (GHEC only for now). In the future this will be exposed as an extension to the Github CLI.
- Released an extension for the official GitHub CLI that adds support for GitHub -> GitHub migrations (GHEC only for now). To install run: `gh extension install github/gh-gei`. To use run: `gh gei --help`
- Automatically remove secrets from log files and console output (previously the verbose logs would contain your PAT's)
- Added --ssh option to generate-script and migrate-repo commands (in both ado2gh and gei). This forces the migration to use an older version of the API's that uses SSH to push the repos into GitHub. The newer API's use HTTPS instead. However some customers have been running into problems with some repos that work fine using the older SSH API's. In the future this option will be deprecated once the issues with the HTTPS-based API's are resolved.
- Added --ssh option to generate-script and migrate-repo commands (in both ado2gh and gh). This forces the migration to use an older version of the API's that uses SSH to push the repos into GitHub. The newer API's use HTTPS instead. However some customers have been running into problems with some repos that work fine using the older SSH API's. In the future this option will be deprecated once the issues with the HTTPS-based API's are resolved.
11 changes: 10 additions & 1 deletion src/Octoshift/AdoClient.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
Expand All @@ -14,10 +16,17 @@ public class AdoClient
private readonly OctoLogger _log;
private double _retryDelay;

public AdoClient(OctoLogger log, HttpClient httpClient)
public AdoClient(OctoLogger log, HttpClient httpClient, string personalAccessToken)
{
_log = log;
_httpClient = httpClient;

if (_httpClient != null)
{
_httpClient.DefaultRequestHeaders.Add("accept", "application/json");
var authToken = Convert.ToBase64String(Encoding.ASCII.GetBytes($":{personalAccessToken}"));
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authToken);
}
}

public virtual async Task<string> GetAsync(string url) => await SendAsync(HttpMethod.Get, url);
Expand Down
48 changes: 24 additions & 24 deletions src/OctoshiftCLI.Tests/AdoApiTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public async Task GetUserId_Should_Return_UserId()
}
};

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);

mockClient.Setup(x => x.GetAsync(endpoint).Result).Returns(userJson.ToJson());

Expand All @@ -55,7 +55,7 @@ public async Task GetUserId_Invalid_Json_Should_Throw_InvalidDataException()
}
};

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);

mockClient.Setup(x => x.GetAsync(endpoint).Result).Returns(userJson.ToJson());

Expand All @@ -81,7 +81,7 @@ public async Task GetOrganizations_Should_Return_All_Orgs()
}
};

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);

mockClient.Setup(x => x.GetAsync(endpoint).Result).Returns(accountsJson.ToJson());

Expand Down Expand Up @@ -115,7 +115,7 @@ public async Task GetOrganizationId_Should_Return_OrgId()

var response = JArray.Parse(accountsJson.ToJson());

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);

mockClient.Setup(x => x.GetWithPagingAsync(endpoint).Result).Returns(response);

Expand Down Expand Up @@ -147,7 +147,7 @@ public async Task GetTeamProjects_Should_Return_All_Team_Projects()
};
var response = JArray.Parse(json.ToJson());

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);

mockClient.Setup(x => x.GetWithPagingAsync(endpoint).Result).Returns(response);

Expand Down Expand Up @@ -186,7 +186,7 @@ public async Task GetRepos_Should_Not_Return_Disabled_Repos()
};
var response = JArray.Parse(json.ToJson());

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);

mockClient.Setup(x => x.GetWithPagingAsync(endpoint).Result).Returns(response);

Expand Down Expand Up @@ -218,7 +218,7 @@ public async Task GetGithubAppId_Should_Skip_Team_Projects_With_No_Endpoints()
};
var response = JArray.Parse(json.ToJson());

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);

mockClient.Setup(x => x.GetWithPagingAsync($"https://dev.azure.com/{adoOrg}/{teamProject1}/_apis/serviceendpoint/endpoints?api-version=6.0-preview.4").Result).Returns(JArray.Parse("[]"));
mockClient.Setup(x => x.GetWithPagingAsync($"https://dev.azure.com/{adoOrg}/{teamProject2}/_apis/serviceendpoint/endpoints?api-version=6.0-preview.4").Result).Returns(response);
Expand Down Expand Up @@ -250,7 +250,7 @@ public async Task GetGithubAppId_Should_Return_Null_When_No_Team_Projects_Have_E
};
var response = JArray.Parse(json.ToJson());

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);

mockClient.Setup(x => x.GetWithPagingAsync($"https://dev.azure.com/{adoOrg}/{teamProject1}/_apis/serviceendpoint/endpoints?api-version=6.0-preview.4").Result).Returns(JArray.Parse("[]"));
mockClient.Setup(x => x.GetWithPagingAsync($"https://dev.azure.com/{adoOrg}/{teamProject2}/_apis/serviceendpoint/endpoints?api-version=6.0-preview.4").Result).Returns(response);
Expand Down Expand Up @@ -301,7 +301,7 @@ public async Task GetGithubHandle_Should_Return_Handle()

var json = $"{{ \"dataProviders\": {{ \"ms.vss-work-web.github-user-data-provider\": {{ \"login\": '{handle}' }} }} }}";

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);

mockClient.Setup(x => x.PostAsync(endpoint, It.Is<object>(y => y.ToJson() == payload.ToJson())).Result).Returns(json);

Expand Down Expand Up @@ -355,7 +355,7 @@ public async Task GetBoardsGithubConnection_Should_Return_Connection_With_All_Re

var json = $"{{ \"dataProviders\": {{ \"ms.vss-work-web.azure-boards-external-connection-data-provider\": {{ \"externalConnections\": [ {{ id: '{connectionId}', serviceEndpoint: {{ id: '{endpointId}' }}, name: '{connectionName}', externalGitRepos: [ {{ id: '{repo1}' }}, {{ id: '{repo2}' }} ] }}, {{ thisIsIgnored: true }} ] }} }} }}";

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);

mockClient.Setup(x => x.PostAsync(endpoint, It.Is<object>(y => y.ToJson() == payload.ToJson())).Result).Returns(json);

Expand Down Expand Up @@ -406,7 +406,7 @@ public async Task CreateBoardsGithubEndpoint_Should_Return_EndpointId()
name = "something"
};

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);
mockClient.Setup(x => x.PostAsync(endpoint, It.Is<object>(y => y.ToJson() == payload.ToJson())).Result).Returns(json.ToJson());

var sut = new AdoApi(mockClient.Object);
Expand Down Expand Up @@ -470,7 +470,7 @@ public async Task AddRepoToBoardsGithubConnection_Should_Send_Correct_Payload()
}
};

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);
var sut = new AdoApi(mockClient.Object);
await sut.AddRepoToBoardsGithubConnection(orgName, orgId, teamProject, connectionId, connectionName, endpointId, new List<string>() { repo1, repo2 });

Expand All @@ -487,7 +487,7 @@ public async Task GetTeamProjectId_Should_Return_TeamProjectId()
var endpoint = $"https://dev.azure.com/{org}/_apis/projects/{teamProject}?api-version=5.0-preview.1";
var response = new { id = teamProjectId };

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);
mockClient.Setup(x => x.GetAsync(endpoint).Result).Returns(response.ToJson());

var sut = new AdoApi(mockClient.Object);
Expand All @@ -507,7 +507,7 @@ public async Task GetRepoId_Should_Return_RepoId()
var endpoint = $"https://dev.azure.com/{org}/{teamProject}/_apis/git/repositories/{repo}?api-version=4.1";
var response = new { id = repoId };

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);
mockClient.Setup(x => x.GetAsync(endpoint).Result).Returns(response.ToJson());

var sut = new AdoApi(mockClient.Object);
Expand Down Expand Up @@ -539,7 +539,7 @@ public async Task GetPipelines_Should_Return_All_Pipelines()
}
};

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);
mockClient.Setup(x => x.GetWithPagingAsync(endpoint).Result).Returns(JArray.Parse(response.ToJson()));

var sut = new AdoApi(mockClient.Object);
Expand Down Expand Up @@ -572,7 +572,7 @@ public async Task GetPipelineId_Should_Return_PipelineId()
}
};

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);
mockClient.Setup(x => x.GetWithPagingAsync(endpoint).Result).Returns(JArray.Parse(response.ToJson()));

var sut = new AdoApi(mockClient.Object);
Expand Down Expand Up @@ -604,7 +604,7 @@ public async Task ShareServiceConnection_Should_Send_Correct_Payload()
}
};

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);
var sut = new AdoApi(mockClient.Object);
await sut.ShareServiceConnection(org, teamProject, teamProjectId, serviceConnectionId);

Expand Down Expand Up @@ -632,7 +632,7 @@ public async Task GetPipeline_Should_Return_Pipeline()
}
};

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);
mockClient.Setup(x => x.GetAsync(endpoint).Result).Returns(response.ToJson());

var sut = new AdoApi(mockClient.Object);
Expand Down Expand Up @@ -710,7 +710,7 @@ public async Task ChangePipelineRepo_Should_Send_Correct_Payload()
oneLastThing = false
};

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);
mockClient.Setup(m => m.GetAsync(endpoint).Result).Returns(oldJson.ToJson());
var sut = new AdoApi(mockClient.Object);
await sut.ChangePipelineRepo(org, teamProject, pipelineId, defaultBranch, clean, checkoutSubmodules, githubOrg, githubRepo, serviceConnectionId);
Expand Down Expand Up @@ -764,7 +764,7 @@ public async Task GetBoardsGithubRepoId_Should_Return_RepoId()
var repoId = Guid.NewGuid().ToString();
var json = $@"{{dataProviders: {{ ""ms.vss-work-web.github-user-repository-data-provider"": {{ additionalProperties: {{ nodeId: '{repoId}' }} }} }} }}";

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);
mockClient.Setup(x => x.PostAsync(endpoint, It.Is<object>(y => y.ToJson() == payload.ToJson())).Result).Returns(json);

var sut = new AdoApi(mockClient.Object);
Expand Down Expand Up @@ -822,7 +822,7 @@ public async Task CreateBoardsGithubConnection_Should_Send_Correct_Payload()
}
};

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);

var sut = new AdoApi(mockClient.Object);
await sut.CreateBoardsGithubConnection(orgName, orgId, teamProject, endpointId, repoId);
Expand All @@ -839,7 +839,7 @@ public async Task DisableRepo_Should_Send_Correct_Payload()

var endpoint = $"https://dev.azure.com/{orgName}/{teamProject}/_apis/git/repositories/{repoId}?api-version=6.1-preview.1";

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);
var sut = new AdoApi(mockClient.Object);
await sut.DisableRepo(orgName, teamProject, repoId);

Expand All @@ -859,7 +859,7 @@ public async Task GetIdentityDescriptor_Should_Return_IdentityDescriptor()
var endpoint = $"https://vssps.dev.azure.com/{orgName}/_apis/identities?searchFilter=General&filterValue={groupName}&queryMembership=None&api-version=6.1-preview.1";
var response = $@"[{{ properties: {{ LocalScopeId: {{ $value: ""wrong"" }} }}, descriptor: ""blah"" }}, {{ descriptor: ""{identityDescriptor}"", properties: {{ LocalScopeId: {{ $value: ""{teamProjectId}"" }} }} }}]";

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);

mockClient.Setup(x => x.GetWithPagingAsync(endpoint).Result).Returns(JArray.Parse(response));

Expand Down Expand Up @@ -902,7 +902,7 @@ public async Task LockRepo_Should_Send_Correct_Payload()
}
};

var mockClient = new Mock<AdoClient>(null, null);
var mockClient = new Mock<AdoClient>(null, null, null);
var sut = new AdoApi(mockClient.Object);
await sut.LockRepo(orgName, teamProjectId, repoId, identityDescriptor);

Expand Down
36 changes: 36 additions & 0 deletions src/OctoshiftCLI.Tests/ado2gh/AdoApiFactoryTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using System;
using System.Net.Http;
using System.Text;
using FluentAssertions;
using Moq;
using OctoshiftCLI.AdoToGithub;
using Xunit;

namespace OctoshiftCLI.Tests.AdoToGithub
{
public class AdoApiFactoryTests
{
private const string ADO_PAT = "ADO_PAT";

[Fact]
public void Create_Should_Create_Ado_Api_With_Ado_Pat()
{
// Arrange
var environmentVariableProviderMock = new Mock<EnvironmentVariableProvider>(null);
environmentVariableProviderMock.Setup(m => m.AdoPersonalAccessToken()).Returns(ADO_PAT);

using var httpClient = new HttpClient();

// Act
var factory = new AdoApiFactory(null, httpClient, environmentVariableProviderMock.Object);
var result = factory.Create();

// Assert
result.Should().NotBeNull();

var authToken = Convert.ToBase64String(Encoding.ASCII.GetBytes($":{ADO_PAT}"));
httpClient.DefaultRequestHeaders.Authorization.Parameter.Should().Be(authToken);
httpClient.DefaultRequestHeaders.Authorization.Scheme.Should().Be("Basic");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
using System.CommandLine;
using System.Threading.Tasks;
using Moq;
using OctoshiftCLI.AdoToGithub;
using OctoshiftCLI.AdoToGithub.Commands;
using Xunit;

Expand Down Expand Up @@ -33,8 +33,10 @@ public async Task Happy_Path()
var role = "maintain";

var mockGithub = new Mock<GithubApi>(null);
var mockGithubApiFactory = new Mock<GithubApiFactory>(null, null, null);
mockGithubApiFactory.Setup(m => m.Create()).Returns(mockGithub.Object);

var command = new AddTeamToRepoCommand(new Mock<OctoLogger>().Object, new Lazy<GithubApi>(mockGithub.Object));
var command = new AddTeamToRepoCommand(new Mock<OctoLogger>().Object, mockGithubApiFactory.Object);
await command.Invoke(githubOrg, githubRepo, team, role);

mockGithub.Verify(x => x.AddTeamToRepo(githubOrg, githubRepo, team, role));
Expand All @@ -49,8 +51,10 @@ public async Task Invalid_Role()
var role = "read"; // read is not a valid role

var mockGithub = new Mock<GithubApi>(null);
var mockGithubApiFactory = new Mock<GithubApiFactory>(null, null, null);
mockGithubApiFactory.Setup(m => m.Create()).Returns(mockGithub.Object);

var command = new AddTeamToRepoCommand(new Mock<OctoLogger>().Object, new Lazy<GithubApi>(mockGithub.Object));
var command = new AddTeamToRepoCommand(new Mock<OctoLogger>().Object, mockGithubApiFactory.Object);

var root = new RootCommand();
root.AddCommand(command);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System;
using System.Threading.Tasks;
using Moq;
using OctoshiftCLI.AdoToGithub;
using OctoshiftCLI.AdoToGithub.Commands;
using Xunit;

Expand Down Expand Up @@ -32,8 +32,10 @@ public async Task Happy_Path()
var adoTeamProject = "foo-ado-tp";

var mockGithub = new Mock<GithubApi>(null);
var mockGithubApiFactory = new Mock<GithubApiFactory>(null, null, null);
mockGithubApiFactory.Setup(m => m.Create()).Returns(mockGithub.Object);

var command = new ConfigureAutoLinkCommand(new Mock<OctoLogger>().Object, new Lazy<GithubApi>(mockGithub.Object));
var command = new ConfigureAutoLinkCommand(new Mock<OctoLogger>().Object, mockGithubApiFactory.Object);
await command.Invoke(githubOrg, githubRepo, adoOrg, adoTeamProject);

mockGithub.Verify(x => x.AddAutoLink(githubOrg, githubRepo, adoOrg, adoTeamProject));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Moq;
using OctoshiftCLI.AdoToGithub;
using OctoshiftCLI.AdoToGithub.Commands;
using Xunit;

Expand Down Expand Up @@ -38,7 +38,10 @@ public async Task Happy_Path()
mockGithub.Setup(x => x.GetIdpGroupId(githubOrg, idpGroup).Result).Returns(idpGroupId);
mockGithub.Setup(x => x.GetTeamSlug(githubOrg, teamName).Result).Returns(teamSlug);

var command = new CreateTeamCommand(new Mock<OctoLogger>().Object, new Lazy<GithubApi>(mockGithub.Object));
var mockGithubApiFactory = new Mock<GithubApiFactory>(null, null, null);
mockGithubApiFactory.Setup(m => m.Create()).Returns(mockGithub.Object);

var command = new CreateTeamCommand(new Mock<OctoLogger>().Object, mockGithubApiFactory.Object);
await command.Invoke(githubOrg, teamName, idpGroup);

mockGithub.Verify(x => x.CreateTeam(githubOrg, teamName));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using Moq;
using OctoshiftCLI.AdoToGithub;
using OctoshiftCLI.AdoToGithub.Commands;
using Xunit;

Expand Down Expand Up @@ -33,7 +34,10 @@ public async Task Happy_Path()
var mockAdo = new Mock<AdoApi>(null);
mockAdo.Setup(x => x.GetRepoId(adoOrg, adoTeamProject, adoRepo).Result).Returns(repoId);

var command = new DisableRepoCommand(new Mock<OctoLogger>().Object, new Lazy<AdoApi>(mockAdo.Object));
var mockAdoApiFactory = new Mock<AdoApiFactory>(null, null, null);
mockAdoApiFactory.Setup(m => m.Create()).Returns(mockAdo.Object);

var command = new DisableRepoCommand(new Mock<OctoLogger>().Object, mockAdoApiFactory.Object);
await command.Invoke(adoOrg, adoTeamProject, adoRepo);

mockAdo.Verify(x => x.DisableRepo(adoOrg, adoTeamProject, repoId));
Expand Down
Loading

0 comments on commit 0332324

Please sign in to comment.