diff options
| author | Danilo M. <danix@danix.xyz> | 2026-11-02 12:36:20 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-11-02 12:36:20 +0100 |
| commit | d5df91b3c63ca0445035c957c2bb07dc156ec2a8 (patch) | |
| tree | 2c1762700a9dd8e81d0a563ab03b93f1fa595c13 /app | |
| parent | 784b0709d657481e40479d13dfb9ad107ec951c6 (diff) | |
| download | HardenedPass-d5df91b3c63ca0445035c957c2bb07dc156ec2a8.tar.gz HardenedPass-d5df91b3c63ca0445035c957c2bb07dc156ec2a8.zip | |
test(git): pin down the JGit behaviour the git operations rely on
Run the command chains of SyncOperation, PushOperation,
BreakOutOfDetached and ResetToRemoteOperation against a local bare
repository and assert the results GitCommandExecutor branches on:
rebase success, push OK / UP_TO_DATE / REJECTED_NONFASTFORWARD, a
stopped rebase with its branch recoverable, abort and push to a
conflict branch, and hard reset to the remote. Passes on JGit 4.5.7;
it is the safety net for the JGit upgrade.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'app')
| -rw-r--r-- | app/src/test/java/app/passwordstore/util/git/JGitBehaviourTest.kt | 158 |
1 files changed, 158 insertions, 0 deletions
diff --git a/app/src/test/java/app/passwordstore/util/git/JGitBehaviourTest.kt b/app/src/test/java/app/passwordstore/util/git/JGitBehaviourTest.kt new file mode 100644 index 00000000..43f8b66a --- /dev/null +++ b/app/src/test/java/app/passwordstore/util/git/JGitBehaviourTest.kt @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2026 Danilo M. <danix@danix.xyz> + * SPDX-License-Identifier: GPL-3.0-only + */ +package app.passwordstore.util.git + +import app.passwordstore.data.repo.PasswordRepository +import java.io.File +import java.nio.file.Files +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import org.eclipse.jgit.api.CreateBranchCommand.SetupUpstreamMode.TRACK +import org.eclipse.jgit.api.Git +import org.eclipse.jgit.api.RebaseCommand +import org.eclipse.jgit.api.ResetCommand +import org.eclipse.jgit.lib.PersonIdent +import org.eclipse.jgit.lib.RepositoryState +import org.eclipse.jgit.transport.RemoteRefUpdate + +/** + * Runs the JGit command chains the git operations in `util/git/operation` use, against a local bare + * repository, and checks the results GitCommandExecutor relies on. Guards JGit upgrades: the + * operations themselves are tied to an Activity and are covered by the device run. + */ +class JGitBehaviourTest { + + private val root = Files.createTempDirectory("aps-jgit").toFile() + private val remote = File(root, "remote.git") + private val identity = PersonIdent("test", "test@example.org") + + init { + Git.init().setBare(true).setDirectory(remote).call().close() + clone("seed").use { git -> + git.write("example.org.gpg", "initial") + git.commitAll("Initial commit") + git.push().setRemote("origin").setPushAll().call() + } + } + + @AfterTest fun cleanUp() = root.deleteRecursively().let {} + + private fun clone(name: String): Git = + Git.cloneRepository().setURI(remote.toURI().toString()).setDirectory(File(root, name)).call() + + private fun Git.write(path: String, content: String) = + File(repository.workTree, path).writeText(content) + + private fun Git.read(path: String) = File(repository.workTree, path).readText() + + /** The add, status, commit steps of SyncOperation; returns the number of changes seen. */ + private fun Git.commitAll(message: String): Int { + add().addFilepattern(".").call() + val changes = status().call().uncommittedChanges.size + if (changes > 0) { + commit().setAll(true).setMessage(message).setAuthor(identity).setCommitter(identity).call() + } + return changes + } + + private fun Git.pushStatuses() = + push().setPushAll().setRemote("origin").call().flatMap { it.remoteUpdates }.map { it.status } + + private fun remoteBranches() = + Git.open(remote).use { git -> + git.branchList().call().map { it.name.removePrefix("refs/heads/") } + } + + @Test + fun syncPullsRebasesAndPushes() { + clone("a").use { a -> + a.write("a.gpg", "from a") + a.commitAll("a") + assertEquals(listOf(RemoteRefUpdate.Status.OK), a.pushStatuses()) + } + clone("b").use { b -> + b.write("b.gpg", "from b") + assertEquals(1, b.commitAll("b")) + val pull = b.pull().setRebase(true).setRemote("origin").call() + assertNotNull(pull.rebaseResult) + assertTrue(pull.rebaseResult.status.isSuccessful) + assertEquals("from a", b.read("a.gpg")) + assertEquals(listOf(RemoteRefUpdate.Status.OK), b.pushStatuses()) + // Nothing left to push: the executor shows "up to date". + assertEquals(listOf(RemoteRefUpdate.Status.UP_TO_DATE), b.pushStatuses()) + // Nothing to commit: the executor skips the commit. + assertEquals(0, b.commitAll("noop")) + } + } + + @Test + fun pushWithoutPullIsRejectedAsNonFastForward() { + val a = clone("a") + val b = clone("b") + a.write("a.gpg", "from a") + a.commitAll("a") + a.pushStatuses() + b.write("b.gpg", "from b") + b.commitAll("b") + assertEquals(listOf(RemoteRefUpdate.Status.REJECTED_NONFASTFORWARD), b.pushStatuses()) + a.close() + b.close() + } + + @Test + fun conflictStopsTheRebaseAndBreakOutOfDetachedRecoversIt() { + val a = clone("a") + val b = clone("b") + a.write("example.org.gpg", "edited by a") + a.commitAll("a") + a.pushStatuses() + b.write("example.org.gpg", "edited by b") + b.commitAll("b") + + val pull = b.pull().setRebase(true).setRemote("origin").call() + assertFalse(pull.rebaseResult.status.isSuccessful) + assertTrue(b.repository.repositoryState.isRebasing) + assertEquals("master", PasswordRepository.rebasedBranch(b.repository.directory)) + + // BreakOutOfDetached, non-merging branch. + b.rebase().setOperation(RebaseCommand.Operation.ABORT).call() + assertEquals(RepositoryState.SAFE, b.repository.repositoryState) + b.checkout().setCreateBranch(true).setName("conflicting-master-1").call() + b.push().setRemote("origin").call() + b.checkout().setName("master").call() + + assertEquals(setOf("master", "conflicting-master-1"), remoteBranches().toSet()) + assertEquals("master", b.repository.branch) + assertEquals("edited by b", b.read("example.org.gpg")) + a.close() + b.close() + } + + @Test + fun resetToRemoteDiscardsLocalCommits() { + val a = clone("a") + val b = clone("b") + a.write("example.org.gpg", "edited by a") + a.commitAll("a") + a.pushStatuses() + b.write("example.org.gpg", "edited by b") + b.commitAll("b") + + // ResetToRemoteOperation + b.fetch().setRemote("origin").setRemoveDeletedRefs(true).call() + b.branchCreate().setName("master").setForce(true).call() + b.checkout().setName("master").setForce(true).setUpstreamMode(TRACK).call() + b.reset().setRef("origin/master").setMode(ResetCommand.ResetType.HARD).call() + + assertEquals("edited by a", b.read("example.org.gpg")) + assertTrue(b.status().call().isClean) + a.close() + b.close() + } +} |
