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 @@ -14,6 +14,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- `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
- Offline unit tests for `BackupService`: cloning and re-fetching run against a local source repository instead of `github.com`, a repository that fails to clone is listed in the failure summary while the others are still backed up, an uncreatable backup directory raises the "Failed to create backup directory" error, `getBackupStatusData` ignores plain files and formats the last-updated time, and `showBackupStatus` output is asserted for the missing-directory, empty-directory and populated cases. Before these, the clone test attempted a real clone from `github.com` and the status tests only checked that nothing was thrown

### Changed

Expand Down
170 changes: 164 additions & 6 deletions src/test/java/com/github/backup/BackupServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,26 @@

import com.github.backup.web.BackupStatusResponse;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.revwalk.RevCommit;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.kohsuke.github.GHRepository;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -53,18 +62,80 @@ void testBackupUserRepositories_NoRepositories() throws IOException {
}

@Test
void testBackupUserRepositories_WithRepositories() throws IOException {
void testBackupUserRepositories_WithRepositories() throws Exception {
Path source = createSourceRepository("source-repo1");
when(mockRepo1.getName()).thenReturn("repo1");
when(mockRepo1.getHttpTransportUrl()).thenReturn("https://github.com/testuser/repo1.git");
when(mockRepo1.getHttpTransportUrl()).thenReturn(source.toUri().toString());

List<GHRepository> repos = Collections.singletonList(mockRepo1);
when(gitHubService.getPublicRepositories("testuser")).thenReturn(repos);

// Note: This test will attempt actual git operations, which may fail in test environment
// In a real scenario, we'd need to mock JGit operations as well
// Clones from a local source repository so no network access is needed
assertDoesNotThrow(() -> backupService.backupUserRepositories("testuser"));

verify(gitHubService).getPublicRepositories("testuser");
assertTrue(Files.isDirectory(tempDir.resolve("testuser").resolve("repo1").resolve(".git")));
}

@Test
void testBackupUserRepositories_ExistingRepositoryIsFetched() throws Exception {
Path source = createSourceRepository("source-repo1");
when(mockRepo1.getName()).thenReturn("repo1");
when(mockRepo1.getHttpTransportUrl()).thenReturn(source.toUri().toString());
when(gitHubService.getPublicRepositories("testuser"))
.thenReturn(Collections.singletonList(mockRepo1));

backupService.backupUserRepositories("testuser");

String newCommit;
String branch;
try (Git sourceGit = Git.open(source.toFile())) {
branch = sourceGit.getRepository().getBranch();
newCommit = commitFile(sourceGit, "second.txt", "second").getName();
}

String output = captureStdout(() -> backupService.backupUserRepositories("testuser"));

assertTrue(output.contains("(repository exists, updating)"), output);
assertTrue(output.contains("✓ Updated successfully"), output);
try (Git backupGit = Git.open(tempDir.resolve("testuser").resolve("repo1").toFile())) {
Ref remoteRef = backupGit.getRepository().findRef("refs/remotes/origin/" + branch);
assertNotNull(remoteRef);
assertEquals(newCommit, remoteRef.getObjectId().getName());
}
}

@Test
void testBackupUserRepositories_FailedRepositoryIsSummarizedAndOthersContinue() throws Exception {
Path source = createSourceRepository("source-repo1");
when(mockRepo1.getName()).thenReturn("repo1");
when(mockRepo1.getHttpTransportUrl()).thenReturn(source.toUri().toString());
when(mockRepo2.getName()).thenReturn("repo2");
when(mockRepo2.getHttpTransportUrl())
.thenReturn(tempDir.resolve("does-not-exist").toUri().toString());
when(gitHubService.getPublicRepositories("testuser"))
.thenReturn(Arrays.asList(mockRepo2, mockRepo1));

String output = captureStdout(() -> backupService.backupUserRepositories("testuser"));

assertTrue(output.contains("Found 2 public repositories"), output);
assertTrue(output.contains("⚠ Warning: 1 repository(ies) failed to backup:"), output);
assertTrue(output.contains(" - repo2"), output);
assertTrue(Files.isDirectory(tempDir.resolve("testuser").resolve("repo1").resolve(".git")));
}

@Test
void testBackupUserRepositories_BackupDirectoryCannotBeCreated() throws IOException {
Path blocker = Files.createFile(tempDir.resolve("blocker"));
BackupService service = new BackupService(gitHubService, blocker.toString());
when(gitHubService.getPublicRepositories("testuser"))
.thenReturn(Collections.singletonList(mockRepo1));

IOException e = assertThrows(IOException.class, () -> service.backupUserRepositories("testuser"));

assertTrue(e.getMessage().startsWith("Failed to create backup directory '"), e.getMessage());
assertTrue(e.getMessage().contains(blocker.resolve("testuser").toString()), e.getMessage());
verify(mockRepo1, never()).getHttpTransportUrl();
}

@Test
Expand Down Expand Up @@ -166,4 +237,91 @@ void testGetBackupStatusData_NonExistentDirectory() {
assertEquals(0, status.getTotalRepositories());
assertTrue(status.getUsers().isEmpty());
}

@Test
void testGetBackupStatusData_IgnoresFilesAndFormatsLastModified() throws IOException {
Path repoDir = Files.createDirectories(tempDir.resolve("user1").resolve("repo1"));
Files.createFile(tempDir.resolve("stray-file.txt"));
Files.createFile(tempDir.resolve("user1").resolve("README.txt"));
long lastModified = 1_700_000_000_000L;
assertTrue(repoDir.toFile().setLastModified(lastModified));

BackupStatusResponse status = backupService.getBackupStatusData();

assertEquals(1, status.getTotalUsers());
assertEquals(1, status.getTotalRepositories());
BackupStatusResponse.RepositoryInfo repo = status.getUsers().get(0).getRepositories().get(0);
assertEquals("repo1", repo.getName());
String expected = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.withZone(ZoneId.systemDefault())
.format(Instant.ofEpochMilli(lastModified));
assertEquals(expected, repo.getLastUpdated());
}

@Test
void testShowBackupStatus_NonExistentDirectoryMessage() {
Path missing = tempDir.resolve("nonexistent");
BackupService service = new BackupService(gitHubService, missing.toString());

String output = captureStdout(service::showBackupStatus);

assertTrue(output.contains("No backups found. Backup directory does not exist: " + missing), output);
}

@Test
void testShowBackupStatus_EmptyDirectoryMessage() {
String output = captureStdout(backupService::showBackupStatus);

assertEquals("No backups found.", output.trim());
}

@Test
void testShowBackupStatus_ListsUsersRepositoriesAndTotals() throws IOException {
Files.createDirectories(tempDir.resolve("user1").resolve("repo1"));
Files.createDirectories(tempDir.resolve("user1").resolve("repo2"));
Files.createDirectories(tempDir.resolve("user2").resolve("repo3"));

String output = captureStdout(backupService::showBackupStatus);

assertTrue(output.contains("Backup directory: " + tempDir.toAbsolutePath()), output);
assertTrue(output.contains("user1/ (2 repositories)"), output);
assertTrue(output.contains("user2/ (1 repositories)"), output);
assertTrue(output.contains(" - repo3 (last updated: "), output);
assertTrue(output.contains("Total: 2 users/organizations, 3 repositories"), output);
}

private Path createSourceRepository(String name) throws Exception {
Path source = tempDir.resolve("sources").resolve(name);
try (Git git = Git.init().setDirectory(source.toFile()).call()) {
commitFile(git, "README.md", "initial");
}
return source;
}

private static RevCommit commitFile(Git git, String fileName, String content) throws Exception {
File workTree = git.getRepository().getWorkTree();
Files.writeString(workTree.toPath().resolve(fileName), content);
git.add().addFilepattern(fileName).call();
PersonIdent ident = new PersonIdent("Test", "test@example.com");
return git.commit().setMessage("Add " + fileName).setAuthor(ident).setCommitter(ident).call();
}

private static String captureStdout(ThrowingRunnable action) {
PrintStream original = System.out;
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
System.setOut(new PrintStream(buffer, true, StandardCharsets.UTF_8));
try {
action.run();
} catch (Exception e) {
throw new AssertionError(e);
} finally {
System.setOut(original);
}
return buffer.toString(StandardCharsets.UTF_8);
}

@FunctionalInterface
private interface ThrowingRunnable {
void run() throws Exception;
}
}
Loading