Skip to content

Commit c8e241d

Browse files
authored
Handle GHES 404 errors when URLs have expired
- Add tests
1 parent fb2205b commit c8e241d

File tree

2 files changed

+262
-2
lines changed

2 files changed

+262
-2
lines changed

src/OctoshiftCLI.Tests/gei/Commands/MigrateRepo/MigrateRepoCommandHandlerTests.cs

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2033,10 +2033,270 @@ public async Task Both_Archives_Download_Retry_On_403_Error()
20332033
_mockHttpDownloadService.Verify(x => x.DownloadToFile(freshMetadataArchiveUrl, METADATA_ARCHIVE_FILE_PATH), Times.Once);
20342034
}
20352035

2036+
[Fact]
2037+
public async Task Git_Archive_Download_Retries_On_404_Error()
2038+
{
2039+
// Arrange
2040+
var freshGitArchiveUrl = "https://example.com/1/fresh";
2041+
2042+
_mockTargetGithubApi.Setup(x => x.GetOrganizationId(TARGET_ORG).Result).Returns(GITHUB_ORG_ID);
2043+
_mockTargetGithubApi.Setup(x => x.CreateGhecMigrationSource(GITHUB_ORG_ID).Result).Returns(MIGRATION_SOURCE_ID);
2044+
_mockTargetGithubApi.Setup(x => x.DoesOrgExist(TARGET_ORG).Result).Returns(true);
2045+
_mockTargetGithubApi.Setup(x => x.StartMigration(
2046+
MIGRATION_SOURCE_ID,
2047+
GHES_REPO_URL,
2048+
GITHUB_ORG_ID,
2049+
TARGET_REPO,
2050+
GITHUB_SOURCE_PAT,
2051+
GITHUB_TARGET_PAT,
2052+
AUTHENTICATED_GIT_ARCHIVE_URL,
2053+
AUTHENTICATED_METADATA_ARCHIVE_URL,
2054+
false,
2055+
null,
2056+
false).Result).Returns(MIGRATION_ID);
2057+
_mockTargetGithubApi.Setup(x => x.GetMigration(MIGRATION_ID).Result).Returns((State: RepositoryMigrationStatus.Succeeded, TARGET_REPO, 0, null, null));
2058+
2059+
_mockSourceGithubApi.Setup(x => x.StartGitArchiveGeneration(SOURCE_ORG, SOURCE_REPO).Result).Returns(GIT_ARCHIVE_ID);
2060+
_mockSourceGithubApi.Setup(x => x.StartMetadataArchiveGeneration(SOURCE_ORG, SOURCE_REPO, false, false).Result).Returns(METADATA_ARCHIVE_ID);
2061+
_mockSourceGithubApi.Setup(x => x.GetArchiveMigrationStatus(SOURCE_ORG, GIT_ARCHIVE_ID).Result).Returns(ArchiveMigrationStatus.Exported);
2062+
_mockSourceGithubApi.Setup(x => x.GetArchiveMigrationStatus(SOURCE_ORG, METADATA_ARCHIVE_ID).Result).Returns(ArchiveMigrationStatus.Exported);
2063+
2064+
// Setup GetArchiveMigrationUrl to return original URL first, then fresh URL on retry
2065+
_mockSourceGithubApi.SetupSequence(x => x.GetArchiveMigrationUrl(SOURCE_ORG, GIT_ARCHIVE_ID).Result)
2066+
.Returns(GIT_ARCHIVE_URL)
2067+
.Returns(freshGitArchiveUrl);
2068+
2069+
_mockSourceGithubApi.Setup(x => x.GetArchiveMigrationUrl(SOURCE_ORG, METADATA_ARCHIVE_ID).Result).Returns(METADATA_ARCHIVE_URL);
2070+
2071+
// Setup HttpDownloadService to fail first time with 404, succeed on retry
2072+
_mockHttpDownloadService
2073+
.SetupSequence(x => x.DownloadToFile(It.IsAny<string>(), GIT_ARCHIVE_FILE_PATH))
2074+
.ThrowsAsync(CreateHttpNotFoundException())
2075+
.Returns(Task.CompletedTask);
2076+
2077+
_mockHttpDownloadService
2078+
.Setup(x => x.DownloadToFile(METADATA_ARCHIVE_URL, METADATA_ARCHIVE_FILE_PATH))
2079+
.Returns(Task.CompletedTask);
2080+
2081+
_mockAzureApi
2082+
.SetupSequence(x => x.UploadToBlob(It.IsAny<string>(), It.IsAny<FileStream>()).Result)
2083+
.Returns(new Uri(AUTHENTICATED_GIT_ARCHIVE_URL))
2084+
.Returns(new Uri(AUTHENTICATED_METADATA_ARCHIVE_URL));
2085+
2086+
_mockFileSystemProvider
2087+
.SetupSequence(m => m.GetTempFileName())
2088+
.Returns(GIT_ARCHIVE_FILE_PATH)
2089+
.Returns(METADATA_ARCHIVE_FILE_PATH);
2090+
2091+
_mockEnvironmentVariableProvider.Setup(m => m.SourceGithubPersonalAccessToken(It.IsAny<bool>())).Returns(GITHUB_SOURCE_PAT);
2092+
_mockEnvironmentVariableProvider.Setup(m => m.TargetGithubPersonalAccessToken(It.IsAny<bool>())).Returns(GITHUB_TARGET_PAT);
2093+
_mockGhesVersionChecker.Setup(m => m.AreBlobCredentialsRequired(GHES_API_URL)).ReturnsAsync(true);
2094+
2095+
// Act
2096+
var args = new MigrateRepoCommandArgs
2097+
{
2098+
GithubSourceOrg = SOURCE_ORG,
2099+
SourceRepo = SOURCE_REPO,
2100+
GithubTargetOrg = TARGET_ORG,
2101+
TargetRepo = TARGET_REPO,
2102+
TargetApiUrl = TARGET_API_URL,
2103+
GhesApiUrl = GHES_API_URL,
2104+
AzureStorageConnectionString = AZURE_CONNECTION_STRING,
2105+
};
2106+
await _handler.Handle(args);
2107+
2108+
// Assert
2109+
// Verify that GetArchiveMigrationUrl was called twice for git archive (once during generation, once during retry)
2110+
_mockSourceGithubApi.Verify(x => x.GetArchiveMigrationUrl(SOURCE_ORG, GIT_ARCHIVE_ID), Times.Exactly(2));
2111+
_mockSourceGithubApi.Verify(x => x.GetArchiveMigrationUrl(SOURCE_ORG, METADATA_ARCHIVE_ID), Times.Once);
2112+
2113+
// Verify that DownloadToFile was called twice for git archive (original URL failed, fresh URL succeeded)
2114+
_mockHttpDownloadService.Verify(x => x.DownloadToFile(GIT_ARCHIVE_URL, GIT_ARCHIVE_FILE_PATH), Times.Once);
2115+
_mockHttpDownloadService.Verify(x => x.DownloadToFile(freshGitArchiveUrl, GIT_ARCHIVE_FILE_PATH), Times.Once);
2116+
_mockHttpDownloadService.Verify(x => x.DownloadToFile(METADATA_ARCHIVE_URL, METADATA_ARCHIVE_FILE_PATH), Times.Once);
2117+
}
2118+
2119+
[Fact]
2120+
public async Task Metadata_Archive_Download_Retries_On_404_Error()
2121+
{
2122+
// Arrange
2123+
var freshMetadataArchiveUrl = "https://example.com/2/fresh";
2124+
2125+
_mockTargetGithubApi.Setup(x => x.GetOrganizationId(TARGET_ORG).Result).Returns(GITHUB_ORG_ID);
2126+
_mockTargetGithubApi.Setup(x => x.CreateGhecMigrationSource(GITHUB_ORG_ID).Result).Returns(MIGRATION_SOURCE_ID);
2127+
_mockTargetGithubApi.Setup(x => x.DoesOrgExist(TARGET_ORG).Result).Returns(true);
2128+
_mockTargetGithubApi.Setup(x => x.StartMigration(
2129+
MIGRATION_SOURCE_ID,
2130+
GHES_REPO_URL,
2131+
GITHUB_ORG_ID,
2132+
TARGET_REPO,
2133+
GITHUB_SOURCE_PAT,
2134+
GITHUB_TARGET_PAT,
2135+
AUTHENTICATED_GIT_ARCHIVE_URL,
2136+
AUTHENTICATED_METADATA_ARCHIVE_URL,
2137+
false,
2138+
null,
2139+
false).Result).Returns(MIGRATION_ID);
2140+
_mockTargetGithubApi.Setup(x => x.GetMigration(MIGRATION_ID).Result).Returns((State: RepositoryMigrationStatus.Succeeded, TARGET_REPO, 0, null, null));
2141+
2142+
_mockSourceGithubApi.Setup(x => x.StartGitArchiveGeneration(SOURCE_ORG, SOURCE_REPO).Result).Returns(GIT_ARCHIVE_ID);
2143+
_mockSourceGithubApi.Setup(x => x.StartMetadataArchiveGeneration(SOURCE_ORG, SOURCE_REPO, false, false).Result).Returns(METADATA_ARCHIVE_ID);
2144+
_mockSourceGithubApi.Setup(x => x.GetArchiveMigrationStatus(SOURCE_ORG, GIT_ARCHIVE_ID).Result).Returns(ArchiveMigrationStatus.Exported);
2145+
_mockSourceGithubApi.Setup(x => x.GetArchiveMigrationStatus(SOURCE_ORG, METADATA_ARCHIVE_ID).Result).Returns(ArchiveMigrationStatus.Exported);
2146+
2147+
_mockSourceGithubApi.Setup(x => x.GetArchiveMigrationUrl(SOURCE_ORG, GIT_ARCHIVE_ID).Result).Returns(GIT_ARCHIVE_URL);
2148+
2149+
// Setup GetArchiveMigrationUrl to return original URL first, then fresh URL on retry
2150+
_mockSourceGithubApi.SetupSequence(x => x.GetArchiveMigrationUrl(SOURCE_ORG, METADATA_ARCHIVE_ID).Result)
2151+
.Returns(METADATA_ARCHIVE_URL)
2152+
.Returns(freshMetadataArchiveUrl);
2153+
2154+
_mockHttpDownloadService
2155+
.Setup(x => x.DownloadToFile(GIT_ARCHIVE_URL, GIT_ARCHIVE_FILE_PATH))
2156+
.Returns(Task.CompletedTask);
2157+
2158+
// Setup HttpDownloadService to fail first time with 404, succeed on retry
2159+
_mockHttpDownloadService
2160+
.SetupSequence(x => x.DownloadToFile(It.IsAny<string>(), METADATA_ARCHIVE_FILE_PATH))
2161+
.ThrowsAsync(CreateHttpNotFoundException())
2162+
.Returns(Task.CompletedTask);
2163+
2164+
_mockAzureApi
2165+
.SetupSequence(x => x.UploadToBlob(It.IsAny<string>(), It.IsAny<FileStream>()).Result)
2166+
.Returns(new Uri(AUTHENTICATED_GIT_ARCHIVE_URL))
2167+
.Returns(new Uri(AUTHENTICATED_METADATA_ARCHIVE_URL));
2168+
2169+
_mockFileSystemProvider
2170+
.SetupSequence(m => m.GetTempFileName())
2171+
.Returns(GIT_ARCHIVE_FILE_PATH)
2172+
.Returns(METADATA_ARCHIVE_FILE_PATH);
2173+
2174+
_mockEnvironmentVariableProvider.Setup(m => m.SourceGithubPersonalAccessToken(It.IsAny<bool>())).Returns(GITHUB_SOURCE_PAT);
2175+
_mockEnvironmentVariableProvider.Setup(m => m.TargetGithubPersonalAccessToken(It.IsAny<bool>())).Returns(GITHUB_TARGET_PAT);
2176+
_mockGhesVersionChecker.Setup(m => m.AreBlobCredentialsRequired(GHES_API_URL)).ReturnsAsync(true);
2177+
2178+
// Act
2179+
var args = new MigrateRepoCommandArgs
2180+
{
2181+
GithubSourceOrg = SOURCE_ORG,
2182+
SourceRepo = SOURCE_REPO,
2183+
GithubTargetOrg = TARGET_ORG,
2184+
TargetRepo = TARGET_REPO,
2185+
TargetApiUrl = TARGET_API_URL,
2186+
GhesApiUrl = GHES_API_URL,
2187+
AzureStorageConnectionString = AZURE_CONNECTION_STRING,
2188+
};
2189+
await _handler.Handle(args);
2190+
2191+
// Assert
2192+
_mockSourceGithubApi.Verify(x => x.GetArchiveMigrationUrl(SOURCE_ORG, GIT_ARCHIVE_ID), Times.Once);
2193+
// Verify that GetArchiveMigrationUrl was called twice for metadata archive (once during generation, once during retry)
2194+
_mockSourceGithubApi.Verify(x => x.GetArchiveMigrationUrl(SOURCE_ORG, METADATA_ARCHIVE_ID), Times.Exactly(2));
2195+
2196+
_mockHttpDownloadService.Verify(x => x.DownloadToFile(GIT_ARCHIVE_URL, GIT_ARCHIVE_FILE_PATH), Times.Once);
2197+
// Verify that DownloadToFile was called twice for metadata archive (original URL failed, fresh URL succeeded)
2198+
_mockHttpDownloadService.Verify(x => x.DownloadToFile(METADATA_ARCHIVE_URL, METADATA_ARCHIVE_FILE_PATH), Times.Once);
2199+
_mockHttpDownloadService.Verify(x => x.DownloadToFile(freshMetadataArchiveUrl, METADATA_ARCHIVE_FILE_PATH), Times.Once);
2200+
}
2201+
2202+
[Fact]
2203+
public async Task Both_Archives_Download_Retry_On_404_Error()
2204+
{
2205+
// Arrange
2206+
var freshGitArchiveUrl = "https://example.com/1/fresh";
2207+
var freshMetadataArchiveUrl = "https://example.com/2/fresh";
2208+
2209+
_mockTargetGithubApi.Setup(x => x.GetOrganizationId(TARGET_ORG).Result).Returns(GITHUB_ORG_ID);
2210+
_mockTargetGithubApi.Setup(x => x.CreateGhecMigrationSource(GITHUB_ORG_ID).Result).Returns(MIGRATION_SOURCE_ID);
2211+
_mockTargetGithubApi.Setup(x => x.DoesOrgExist(TARGET_ORG).Result).Returns(true);
2212+
_mockTargetGithubApi.Setup(x => x.StartMigration(
2213+
MIGRATION_SOURCE_ID,
2214+
GHES_REPO_URL,
2215+
GITHUB_ORG_ID,
2216+
TARGET_REPO,
2217+
GITHUB_SOURCE_PAT,
2218+
GITHUB_TARGET_PAT,
2219+
AUTHENTICATED_GIT_ARCHIVE_URL,
2220+
AUTHENTICATED_METADATA_ARCHIVE_URL,
2221+
false,
2222+
null,
2223+
false).Result).Returns(MIGRATION_ID);
2224+
_mockTargetGithubApi.Setup(x => x.GetMigration(MIGRATION_ID).Result).Returns((State: RepositoryMigrationStatus.Succeeded, TARGET_REPO, 0, null, null));
2225+
2226+
_mockSourceGithubApi.Setup(x => x.StartGitArchiveGeneration(SOURCE_ORG, SOURCE_REPO).Result).Returns(GIT_ARCHIVE_ID);
2227+
_mockSourceGithubApi.Setup(x => x.StartMetadataArchiveGeneration(SOURCE_ORG, SOURCE_REPO, false, false).Result).Returns(METADATA_ARCHIVE_ID);
2228+
_mockSourceGithubApi.Setup(x => x.GetArchiveMigrationStatus(SOURCE_ORG, GIT_ARCHIVE_ID).Result).Returns(ArchiveMigrationStatus.Exported);
2229+
_mockSourceGithubApi.Setup(x => x.GetArchiveMigrationStatus(SOURCE_ORG, METADATA_ARCHIVE_ID).Result).Returns(ArchiveMigrationStatus.Exported);
2230+
2231+
// Setup GetArchiveMigrationUrl to return original URLs first, then fresh URLs on retry
2232+
_mockSourceGithubApi.SetupSequence(x => x.GetArchiveMigrationUrl(SOURCE_ORG, GIT_ARCHIVE_ID).Result)
2233+
.Returns(GIT_ARCHIVE_URL)
2234+
.Returns(freshGitArchiveUrl);
2235+
2236+
_mockSourceGithubApi.SetupSequence(x => x.GetArchiveMigrationUrl(SOURCE_ORG, METADATA_ARCHIVE_ID).Result)
2237+
.Returns(METADATA_ARCHIVE_URL)
2238+
.Returns(freshMetadataArchiveUrl);
2239+
2240+
// Setup HttpDownloadService to fail first time for both archives with 404, succeed on retry
2241+
_mockHttpDownloadService
2242+
.SetupSequence(x => x.DownloadToFile(It.IsAny<string>(), GIT_ARCHIVE_FILE_PATH))
2243+
.ThrowsAsync(CreateHttpNotFoundException())
2244+
.Returns(Task.CompletedTask);
2245+
2246+
_mockHttpDownloadService
2247+
.SetupSequence(x => x.DownloadToFile(It.IsAny<string>(), METADATA_ARCHIVE_FILE_PATH))
2248+
.ThrowsAsync(CreateHttpNotFoundException())
2249+
.Returns(Task.CompletedTask);
2250+
2251+
_mockAzureApi
2252+
.SetupSequence(x => x.UploadToBlob(It.IsAny<string>(), It.IsAny<FileStream>()).Result)
2253+
.Returns(new Uri(AUTHENTICATED_GIT_ARCHIVE_URL))
2254+
.Returns(new Uri(AUTHENTICATED_METADATA_ARCHIVE_URL));
2255+
2256+
_mockFileSystemProvider
2257+
.SetupSequence(m => m.GetTempFileName())
2258+
.Returns(GIT_ARCHIVE_FILE_PATH)
2259+
.Returns(METADATA_ARCHIVE_FILE_PATH);
2260+
2261+
_mockEnvironmentVariableProvider.Setup(m => m.SourceGithubPersonalAccessToken(It.IsAny<bool>())).Returns(GITHUB_SOURCE_PAT);
2262+
_mockEnvironmentVariableProvider.Setup(m => m.TargetGithubPersonalAccessToken(It.IsAny<bool>())).Returns(GITHUB_TARGET_PAT);
2263+
_mockGhesVersionChecker.Setup(m => m.AreBlobCredentialsRequired(GHES_API_URL)).ReturnsAsync(true);
2264+
2265+
// Act
2266+
var args = new MigrateRepoCommandArgs
2267+
{
2268+
GithubSourceOrg = SOURCE_ORG,
2269+
SourceRepo = SOURCE_REPO,
2270+
GithubTargetOrg = TARGET_ORG,
2271+
TargetRepo = TARGET_REPO,
2272+
TargetApiUrl = TARGET_API_URL,
2273+
GhesApiUrl = GHES_API_URL,
2274+
AzureStorageConnectionString = AZURE_CONNECTION_STRING,
2275+
};
2276+
await _handler.Handle(args);
2277+
2278+
// Assert
2279+
// Verify that GetArchiveMigrationUrl was called twice for both archives (once during generation, once during retry)
2280+
_mockSourceGithubApi.Verify(x => x.GetArchiveMigrationUrl(SOURCE_ORG, GIT_ARCHIVE_ID), Times.Exactly(2));
2281+
_mockSourceGithubApi.Verify(x => x.GetArchiveMigrationUrl(SOURCE_ORG, METADATA_ARCHIVE_ID), Times.Exactly(2));
2282+
2283+
// Verify that DownloadToFile was called twice for each archive (original URL failed, fresh URL succeeded)
2284+
_mockHttpDownloadService.Verify(x => x.DownloadToFile(GIT_ARCHIVE_URL, GIT_ARCHIVE_FILE_PATH), Times.Once);
2285+
_mockHttpDownloadService.Verify(x => x.DownloadToFile(freshGitArchiveUrl, GIT_ARCHIVE_FILE_PATH), Times.Once);
2286+
_mockHttpDownloadService.Verify(x => x.DownloadToFile(METADATA_ARCHIVE_URL, METADATA_ARCHIVE_FILE_PATH), Times.Once);
2287+
_mockHttpDownloadService.Verify(x => x.DownloadToFile(freshMetadataArchiveUrl, METADATA_ARCHIVE_FILE_PATH), Times.Once);
2288+
}
2289+
20362290
private static HttpRequestException CreateHttpForbiddenException()
20372291
{
20382292
// Use the constructor that sets the StatusCode property (available in .NET 5+)
20392293
return new HttpRequestException("Response status code does not indicate success: 403 (Forbidden).", null, HttpStatusCode.Forbidden);
20402294
}
2295+
2296+
private static HttpRequestException CreateHttpNotFoundException()
2297+
{
2298+
// Use the constructor that sets the StatusCode property (available in .NET 5+)
2299+
return new HttpRequestException("Response status code does not indicate success: 404 (Not Found).", null, HttpStatusCode.NotFound);
2300+
}
20412301
}
20422302
}

src/gei/Commands/MigrateRepo/MigrateRepoCommandHandler.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ private string ExtractGhesBaseUrl(string ghesApiUrl)
267267
{
268268
await _httpDownloadService.DownloadToFile(gitArchiveUrl, gitArchiveDownloadFilePath);
269269
}
270-
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Forbidden)
270+
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Forbidden or HttpStatusCode.NotFound)
271271
{
272272
// URL likely expired, regenerate and retry
273273
_log.LogInformation("Git archive URL appears to have expired, regenerating fresh URL and retrying download...");
@@ -282,7 +282,7 @@ private string ExtractGhesBaseUrl(string ghesApiUrl)
282282
{
283283
await _httpDownloadService.DownloadToFile(metadataArchiveUrl, metadataArchiveDownloadFilePath);
284284
}
285-
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Forbidden)
285+
catch (HttpRequestException ex) when (ex.StatusCode is HttpStatusCode.Forbidden or HttpStatusCode.NotFound)
286286
{
287287
// URL likely expired, regenerate and retry
288288
_log.LogInformation("Metadata archive URL appears to have expired, regenerating fresh URL and retrying download...");

0 commit comments

Comments
 (0)