Skip to content

Commit d0681c1

Browse files
authored
Add BBS client/API plumbing (#583)
The first step in migrating BBS repos is to tell the source instance to generate the archive, then to query the REST API to get the state of the export (until it's done). Additionally, we want to be able to query the server to get its version, to make sure it's one that supports the export API. This PR adds a minimal, naïve implementation of `BbsClient` (which abstracts away talking to the BBS source instance) and `BbsApi`, which formalizes the API endpoints we want to hit as we kick off a migration. Closes #524 - [x] Did you write/update appropriate tests - [x] Appropriate logging output - [x] Issue linked - [ ] ~~Release notes updated (if appropriate)~~ - [ ] ~~Docs updated (or issue created)~~ <!-- For docs we should review the docs at: https://docs.github.com/en/early-access/github/migrating-with-github-enterprise-importer and the README.md in this repo If a doc update is required based on the changes in this PR, it is sufficient to create an issue and link to it here. The doc update can be made later/separately. The process to update the docs can be found here: https://github.com/github/docs-early-access#opening-prs The markdown files are here: https://github.com/github/docs-early-access/tree/main/content/github/migrating-with-github-enterprise-importer -->
1 parent 983803d commit d0681c1

File tree

4 files changed

+423
-21
lines changed

4 files changed

+423
-21
lines changed

src/Octoshift/BbsApi.cs

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,59 @@
1-
using System;
1+
using System.Threading.Tasks;
2+
using Newtonsoft.Json.Linq;
23

3-
namespace OctoshiftCLI
4+
namespace OctoshiftCLI;
5+
6+
public class BbsApi
47
{
5-
public class BbsApi
8+
private readonly BbsClient _client;
9+
private readonly string _bbsBaseUrl;
10+
private readonly OctoLogger _log;
11+
12+
public BbsApi(BbsClient client, string bbsServerUrl, OctoLogger log)
13+
{
14+
_client = client;
15+
_bbsBaseUrl = bbsServerUrl?.TrimEnd('/');
16+
_log = log;
17+
}
18+
19+
public virtual async Task<string> GetServerVersion()
620
{
7-
public BbsApi()
21+
var url = $"{_bbsBaseUrl}/application-properties";
22+
23+
var content = await _client.GetAsync(url);
24+
25+
return (string)JObject.Parse(content)["version"];
26+
}
27+
28+
public virtual async Task<long> StartExport(string projectKey = "*", string slug = "*")
29+
{
30+
var url = $"{_bbsBaseUrl}/migration/exports";
31+
var payload = new
832
{
9-
throw new NotImplementedException();
10-
}
33+
repositoriesRequest = new
34+
{
35+
includes = new[]
36+
{
37+
new
38+
{
39+
projectKey,
40+
slug
41+
}
42+
}
43+
}
44+
};
45+
46+
var content = await _client.PostAsync(url, payload);
47+
48+
return (long)JObject.Parse(content)["id"];
49+
}
50+
51+
public virtual async Task<string> GetExportState(long id)
52+
{
53+
var url = $"{_bbsBaseUrl}/migration/exports/{id}";
54+
55+
var content = await _client.GetAsync(url);
56+
57+
return (string)JObject.Parse(content)["state"];
1158
}
1259
}

src/Octoshift/BbsClient.cs

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,70 @@
11
using System;
2+
using System.Net;
23
using System.Net.Http;
34
using System.Net.Http.Headers;
45
using System.Text;
6+
using System.Threading.Tasks;
57
using OctoshiftCLI.Contracts;
8+
using OctoshiftCLI.Extensions;
69

7-
namespace OctoshiftCLI
10+
namespace OctoshiftCLI;
11+
12+
public class BbsClient
813
{
9-
public class BbsClient
14+
private readonly HttpClient _httpClient;
15+
private readonly OctoLogger _log;
16+
private readonly RetryPolicy _retryPolicy;
17+
18+
public BbsClient(OctoLogger log, HttpClient httpClient, IVersionProvider versionProvider, RetryPolicy retryPolicy, string username, string password)
1019
{
11-
private readonly HttpClient _httpClient;
20+
_log = log;
21+
_httpClient = httpClient;
22+
_retryPolicy = retryPolicy;
1223

13-
public BbsClient(HttpClient httpClient, IVersionProvider versionProvider, string personalAccessToken)
24+
if (_httpClient != null)
1425
{
15-
_httpClient = httpClient;
16-
17-
if (_httpClient != null)
26+
_httpClient.DefaultRequestHeaders.Add("accept", "application/json");
27+
var authCredentials = Convert.ToBase64String(Encoding.ASCII.GetBytes($"{username}:{password}"));
28+
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authCredentials);
29+
_httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("OctoshiftCLI", versionProvider?.GetCurrentVersion()));
30+
if (versionProvider?.GetVersionComments() is { } comments)
1831
{
19-
_httpClient.DefaultRequestHeaders.Add("accept", "application/json");
20-
var authToken = Convert.ToBase64String(Encoding.ASCII.GetBytes($":{personalAccessToken}"));
21-
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", authToken);
22-
_httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("OctoshiftCLI", versionProvider?.GetCurrentVersion()));
23-
if (versionProvider?.GetVersionComments() is { } comments)
24-
{
25-
_httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(comments));
26-
}
32+
_httpClient.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(comments));
2733
}
2834
}
2935
}
36+
37+
public virtual async Task<string> GetAsync(string url)
38+
{
39+
return await _retryPolicy.HttpRetry(async () => await SendAsync(HttpMethod.Get, url),
40+
ex => ex.StatusCode == HttpStatusCode.ServiceUnavailable);
41+
}
42+
43+
public virtual async Task<string> PostAsync(string url, object body) => await SendAsync(HttpMethod.Post, url, body);
44+
45+
private async Task<string> SendAsync(HttpMethod httpMethod, string url, object body = null)
46+
{
47+
_log.LogVerbose($"HTTP {httpMethod}: {url}");
48+
49+
if (body != null)
50+
{
51+
_log.LogVerbose($"HTTP BODY: {body.ToJson()}");
52+
}
53+
54+
using var payload = body?.ToJson().ToStringContent();
55+
var response = httpMethod.ToString() switch
56+
{
57+
"GET" => await _httpClient.GetAsync(url),
58+
"DELETE" => await _httpClient.DeleteAsync(url),
59+
"POST" => await _httpClient.PostAsync(url, payload),
60+
"PUT" => await _httpClient.PutAsync(url, payload),
61+
"PATCH" => await _httpClient.PatchAsync(url, payload),
62+
_ => throw new ArgumentOutOfRangeException($"{httpMethod} is not supported.")
63+
};
64+
var content = await response.Content.ReadAsStringAsync();
65+
_log.LogVerbose($"RESPONSE ({response.StatusCode}): {content}");
66+
67+
return content;
68+
69+
}
3070
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
using System.Threading.Tasks;
2+
using FluentAssertions;
3+
using Moq;
4+
using OctoshiftCLI.Extensions;
5+
using Xunit;
6+
7+
namespace OctoshiftCLI.Tests;
8+
9+
public class BbsApiTests
10+
{
11+
12+
private readonly Mock<OctoLogger> _mockOctoLogger = TestHelpers.CreateMock<OctoLogger>();
13+
private readonly Mock<BbsClient> _mockBbsClient = TestHelpers.CreateMock<BbsClient>();
14+
15+
private readonly BbsApi sut;
16+
17+
private const string BBS_SERVICE_URL = "http://localhost:7990/rest/api/1.0";
18+
private const string PROJECT_KEY = "TEST";
19+
private const string SLUG = "test-repo";
20+
private const long EXPORT_ID = 12345;
21+
22+
public BbsApiTests()
23+
{
24+
sut = new BbsApi(_mockBbsClient.Object, BBS_SERVICE_URL, _mockOctoLogger.Object);
25+
}
26+
27+
[Fact]
28+
public async Task StartExport_Should_Return_ExportId()
29+
{
30+
var endpoint = $"{BBS_SERVICE_URL}/migration/exports";
31+
var requestPayload = new
32+
{
33+
repositoriesRequest = new
34+
{
35+
includes = new[]
36+
{
37+
new
38+
{
39+
projectKey = PROJECT_KEY,
40+
slug = SLUG
41+
}
42+
}
43+
}
44+
};
45+
46+
var responsePayload = new
47+
{
48+
id = EXPORT_ID
49+
};
50+
51+
_mockBbsClient.Setup(x => x.PostAsync(endpoint, It.Is<object>(y => y.ToJson() == requestPayload.ToJson()))).ReturnsAsync(responsePayload.ToJson());
52+
53+
var result = await sut.StartExport(PROJECT_KEY, SLUG);
54+
55+
result.Should().Be(EXPORT_ID);
56+
}
57+
58+
[Fact]
59+
public async Task GetExportState_Returns_Export_State()
60+
{
61+
var endpoint = $"{BBS_SERVICE_URL}/migration/exports/{EXPORT_ID}";
62+
var state = "INITIALISING";
63+
64+
var responsePayload = new
65+
{
66+
id = EXPORT_ID,
67+
state
68+
};
69+
70+
_mockBbsClient.Setup(x => x.GetAsync(endpoint)).ReturnsAsync(responsePayload.ToJson());
71+
72+
var result = await sut.GetExportState(EXPORT_ID);
73+
74+
result.Should().Be(state);
75+
}
76+
77+
[Fact]
78+
public async Task GetServerVersion_Returns_Server_Version()
79+
{
80+
var endpoint = $"{BBS_SERVICE_URL}/application-properties";
81+
var version = "8.3.0";
82+
83+
var responsePayload = new
84+
{
85+
version,
86+
buildNumber = "8003000",
87+
buildDate = "1659066041797"
88+
};
89+
90+
_mockBbsClient.Setup(x => x.GetAsync(endpoint)).ReturnsAsync(responsePayload.ToJson());
91+
92+
var result = await sut.GetServerVersion();
93+
94+
result.Should().Be(version);
95+
}
96+
}

0 commit comments

Comments
 (0)