aboutsummaryrefslogtreecommitdiffstats
path: root/app/src
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-11-02 12:36:20 +0100
committerDanilo M. <danix@danix.xyz>2026-11-02 12:36:20 +0100
commitd5df91b3c63ca0445035c957c2bb07dc156ec2a8 (patch)
tree2c1762700a9dd8e81d0a563ab03b93f1fa595c13 /app/src
parent784b0709d657481e40479d13dfb9ad107ec951c6 (diff)
downloadHardenedPass-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/src')
-rw-r--r--app/src/test/java/app/passwordstore/util/git/JGitBehaviourTest.kt158
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()
+ }
+}