Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `BACKUP_INTERVAL_MS` environment variable for the Docker daemon image, mapped by `docker-entrypoint.sh` to `-Dbackup.scheduled.interval.ms`, so the backup interval can be configured from `.env`/`docker-compose.yml` without overriding the entrypoint
- `docker-entrypoint-test.sh`, a shell test that runs `docker-entrypoint.sh` against a stub `java` and asserts the argument list built for each combination of `BACKUP_DIRECTORY`, `SCHEDULED_USERS` and `BACKUP_INTERVAL_MS`, including empty values and values containing spaces; it runs in the `docker-build` CI job, so entrypoint changes are no longer merged unexecuted
- `DocumentationVersionTest`, which fails `mvn test` when a `gh-backup-<version>.jar` reference in `README.md`, `USER_GUIDE.md`, `CONFIG.md`, `COMMANDS.md` or `Dockerfile` does not match `<version>` in `pom.xml`, so a version bump can no longer leave some of them stale; `CONTRIBUTING.md` notes the check
- Offline unit tests for `GitHubService.getPublicRepositories`, run against a mocked GitHub client: private repositories are filtered out, a name that is not an organization falls back to the user lookup, a user lookup failing with a 404 is rewrapped as the "not found as GitHub organization or user" message, and any other failure is rethrown unchanged. Before these, the method was exercised only by a test that calls the live GitHub API

### Changed

Expand Down
121 changes: 119 additions & 2 deletions src/test/java/com/github/backup/GitHubServiceTest.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,53 @@
package com.github.backup;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.kohsuke.github.GHOrganization;
import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GHUser;
import org.kohsuke.github.GitHub;
import org.kohsuke.github.GitHubBuilder;
import org.kohsuke.github.PagedIterable;
import org.mockito.Mock;
import org.mockito.MockedConstruction;
import org.mockito.MockedStatic;
import org.mockito.MockitoAnnotations;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

class GitHubServiceTest {

@Mock
private GitHub github;

@Mock
private GHOrganization organization;

@Mock
private GHUser user;

@Mock
private PagedIterable<GHRepository> pagedRepositories;

@Mock
private GHRepository publicRepo;

@Mock
private GHRepository privateRepo;

@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
when(publicRepo.isPrivate()).thenReturn(false);
when(privateRepo.isPrivate()).thenReturn(true);
}

@Test
void testGitHubServiceInitialization_Anonymous() {
assertDoesNotThrow(() -> {
Expand All @@ -29,10 +68,88 @@ void testGitHubServiceInitialization_WithToken() {
@Test
void testGetPublicRepositories_InvalidUser() throws IOException {
GitHubService service = new GitHubService();

// Test with a very unlikely username
assertThrows(IOException.class, () ->
assertThrows(IOException.class, () ->
service.getPublicRepositories("this-user-definitely-does-not-exist-12345678")
);
}

@Test
void testGetPublicRepositories_OrganizationFiltersPrivateRepositories() throws IOException {
when(github.getOrganization("test-org")).thenReturn(organization);
when(organization.listRepositories()).thenReturn(pagedRepositories);
when(pagedRepositories.toList()).thenReturn(Arrays.asList(publicRepo, privateRepo));

List<GHRepository> repos = newServiceBackedBy(github).getPublicRepositories("test-org");

assertEquals(List.of(publicRepo), repos);
verify(github, never()).getUser(anyString());
}

@Test
void testGetPublicRepositories_FallsBackToUserWhenNotAnOrganization() throws IOException {
when(github.getOrganization("test-user")).thenThrow(new IOException("not an org"));
when(github.getUser("test-user")).thenReturn(user);
when(user.listRepositories()).thenReturn(pagedRepositories);
when(pagedRepositories.toList()).thenReturn(Arrays.asList(privateRepo, publicRepo));

List<GHRepository> repos = newServiceBackedBy(github).getPublicRepositories("test-user");

assertEquals(List.of(publicRepo), repos);
}

@Test
void testGetPublicRepositories_NotFoundAsOrganizationOrUser() throws IOException {
IOException userNotFound = new IOException("{\"message\":\"Not Found\",\"status\":\"404\"}");
when(github.getOrganization("missing")).thenThrow(new IOException("org lookup failed"));
when(github.getUser("missing")).thenThrow(userNotFound);

GitHubService service = newServiceBackedBy(github);
IOException thrown = assertThrows(IOException.class, () -> service.getPublicRepositories("missing"));

assertEquals("'missing' not found as GitHub organization or user. Please verify the name is correct.",
thrown.getMessage());
assertSame(userNotFound, thrown.getCause());
}

@Test
void testGetPublicRepositories_NonNotFoundErrorIsRethrownUnchanged() throws IOException {
IOException rateLimited = new IOException("API rate limit exceeded");
when(github.getOrganization("test-user")).thenThrow(new IOException("org lookup failed"));
when(github.getUser("test-user")).thenThrow(rateLimited);

GitHubService service = newServiceBackedBy(github);
IOException thrown = assertThrows(IOException.class, () -> service.getPublicRepositories("test-user"));

assertSame(rateLimited, thrown);
}

@Test
void testGetPublicRepositories_NullErrorMessageIsRethrownUnchanged() throws IOException {
IOException noMessage = new IOException();
when(github.getOrganization("test-user")).thenThrow(new IOException("org lookup failed"));
when(github.getUser("test-user")).thenThrow(noMessage);

GitHubService service = newServiceBackedBy(github);
IOException thrown = assertThrows(IOException.class, () -> service.getPublicRepositories("test-user"));

assertSame(noMessage, thrown);
}

/**
* Builds a GitHubService whose underlying client is the given mock, whichever
* construction path (anonymous or GITHUB_TOKEN) the current environment takes.
*/
private GitHubService newServiceBackedBy(GitHub client) throws IOException {
try (MockedStatic<GitHub> gitHubStatic = mockStatic(GitHub.class);
MockedConstruction<GitHubBuilder> builders = mockConstruction(GitHubBuilder.class,
(builder, context) -> {
when(builder.withOAuthToken(anyString())).thenReturn(builder);
when(builder.build()).thenReturn(client);
})) {
gitHubStatic.when(GitHub::connectAnonymously).thenReturn(client);
return new GitHubService();
}
}
}
Loading