mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-09 15:05:44 +01:00
Implement file lock for git (#1838)
Adds a "file lock" command that can be used to mark files as locked by a specific user. This command is implemented for git using a store to keep the locks. Additionally, the Git LFS locking API is implemented. To display locks, the scm-manager/scm-file-lock-plugin can be used. Co-authored-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com>
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Answers;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.Changeset;
|
||||
import sonia.scm.repository.Modifications;
|
||||
import sonia.scm.repository.Modified;
|
||||
import sonia.scm.repository.PreReceiveRepositoryHookEvent;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryHookEvent;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.repository.api.HookChangesetBuilder;
|
||||
import sonia.scm.repository.api.HookContext;
|
||||
import sonia.scm.repository.api.ModificationsCommandBuilder;
|
||||
import sonia.scm.repository.api.RepositoryService;
|
||||
import sonia.scm.repository.api.RepositoryServiceFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static java.util.Arrays.asList;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static sonia.scm.repository.RepositoryHookType.PRE_RECEIVE;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class FileLockPreCommitHookTest {
|
||||
|
||||
private static final Repository REPOSITORY = RepositoryTestData.createHeartOfGold();
|
||||
|
||||
@Mock
|
||||
private GitFileLockStoreFactory fileLockStoreFactory;
|
||||
@Mock
|
||||
private GitFileLockStoreFactory.GitFileLockStore fileLockStore;
|
||||
@Mock
|
||||
private RepositoryServiceFactory serviceFactory;
|
||||
@Mock
|
||||
private RepositoryService service;
|
||||
|
||||
@InjectMocks
|
||||
private FileLockPreCommitHook hook;
|
||||
|
||||
@Mock
|
||||
private HookContext context;
|
||||
|
||||
@BeforeEach
|
||||
void initLockStore() {
|
||||
when(fileLockStoreFactory.create(REPOSITORY))
|
||||
.thenReturn(fileLockStore);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIgnoreRepositoriesWithoutLockSupport() {
|
||||
PreReceiveRepositoryHookEvent event = new PreReceiveRepositoryHookEvent(new RepositoryHookEvent(context, REPOSITORY, PRE_RECEIVE));
|
||||
|
||||
hook.checkForLocks(event);
|
||||
|
||||
verify(serviceFactory, never()).create(any(Repository.class));
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithLocks {
|
||||
|
||||
@Mock
|
||||
private HookChangesetBuilder changesetBuilder;
|
||||
@Mock(answer = Answers.RETURNS_SELF)
|
||||
private ModificationsCommandBuilder modificationsCommand;
|
||||
|
||||
private String currentChangesetId;
|
||||
|
||||
@BeforeEach
|
||||
void initService() {
|
||||
when(serviceFactory.create(REPOSITORY))
|
||||
.thenReturn(service);
|
||||
when(service.getModificationsCommand())
|
||||
.thenReturn(modificationsCommand);
|
||||
when(context.getChangesetProvider())
|
||||
.thenReturn(changesetBuilder);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void initLocks() {
|
||||
when(fileLockStore.hasLocks()).thenReturn(true);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void initModifications() throws IOException {
|
||||
when(modificationsCommand.revision(anyString()))
|
||||
.thenAnswer(invocation -> {
|
||||
currentChangesetId = invocation.getArgument(0, String.class);
|
||||
return modificationsCommand;
|
||||
});
|
||||
when(modificationsCommand.getModifications())
|
||||
.thenAnswer(invocation ->
|
||||
new Modifications(
|
||||
currentChangesetId,
|
||||
new Modified("path-1-" + currentChangesetId),
|
||||
new Modified("path-2-" + currentChangesetId)
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCheckAllPathsForLocks() {
|
||||
PreReceiveRepositoryHookEvent event = new PreReceiveRepositoryHookEvent(new RepositoryHookEvent(context, REPOSITORY, PRE_RECEIVE));
|
||||
when(changesetBuilder.getChangesets())
|
||||
.thenReturn(asList(
|
||||
new Changeset("1", null, null),
|
||||
new Changeset("2", null, null)
|
||||
));
|
||||
|
||||
hook.checkForLocks(event);
|
||||
|
||||
verify(fileLockStore).assertModifiable("path-1-1");
|
||||
verify(fileLockStore).assertModifiable("path-2-1");
|
||||
verify(fileLockStore).assertModifiable("path-1-2");
|
||||
verify(fileLockStore).assertModifiable("path-2-2");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.assertj.core.api.AbstractObjectAssert;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.repository.api.FileLock;
|
||||
import sonia.scm.repository.api.LockCommandResult;
|
||||
import sonia.scm.repository.api.UnlockCommandResult;
|
||||
import sonia.scm.repository.spi.GitFileLockStoreFactory.GitFileLockStore;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Optional;
|
||||
|
||||
import static java.util.Optional.of;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class GitFileLockCommandTest {
|
||||
|
||||
private static final Repository REPOSITORY = RepositoryTestData.createHeartOfGold();
|
||||
private static final Instant NOW = Instant.ofEpochSecond(-562031958);
|
||||
|
||||
@Mock
|
||||
private GitContext context;
|
||||
@Mock
|
||||
private GitFileLockStoreFactory lockStoreFactory;
|
||||
@Mock
|
||||
private GitFileLockStore lockStore;
|
||||
|
||||
@InjectMocks
|
||||
private GitFileLockCommand lockCommand;
|
||||
|
||||
@BeforeEach
|
||||
void initContext() {
|
||||
when(context.getRepository()).thenReturn(REPOSITORY);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void initStoreFactory() {
|
||||
when(lockStoreFactory.create(REPOSITORY)).thenReturn(lockStore);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldSetLockOnLockRequest() {
|
||||
LockCommandRequest request = new LockCommandRequest();
|
||||
request.setFile("some/file.txt");
|
||||
|
||||
LockCommandResult lock = lockCommand.lock(request);
|
||||
|
||||
assertThat(lock.isSuccessful()).isTrue();
|
||||
verify(lockStore).put("some/file.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUnlockOnUnlockRequest() {
|
||||
UnlockCommandRequest request = new UnlockCommandRequest();
|
||||
request.setFile("some/file.txt");
|
||||
|
||||
UnlockCommandResult lock = lockCommand.unlock(request);
|
||||
|
||||
assertThat(lock.isSuccessful()).isTrue();
|
||||
verify(lockStore).remove("some/file.txt", false);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUnlockWithForceOnUnlockRequestWithForce() {
|
||||
UnlockCommandRequest request = new UnlockCommandRequest();
|
||||
request.setFile("some/file.txt");
|
||||
request.setForce(true);
|
||||
|
||||
UnlockCommandResult lock = lockCommand.unlock(request);
|
||||
|
||||
assertThat(lock.isSuccessful()).isTrue();
|
||||
verify(lockStore).remove("some/file.txt", true);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetStatus() {
|
||||
when(lockStore.getLock("some/file.txt"))
|
||||
.thenReturn(of(new FileLock("some/file.txt", "42", "dent", NOW)));
|
||||
|
||||
LockStatusCommandRequest request = new LockStatusCommandRequest();
|
||||
request.setFile("some/file.txt");
|
||||
|
||||
Optional<FileLock> status = lockCommand.status(request);
|
||||
|
||||
AbstractObjectAssert<?, FileLock> statusAssert = assertThat(status).get();
|
||||
statusAssert
|
||||
.extracting("id")
|
||||
.isEqualTo("42");
|
||||
statusAssert
|
||||
.extracting("path")
|
||||
.isEqualTo("some/file.txt");
|
||||
statusAssert
|
||||
.extracting("userId")
|
||||
.isEqualTo("dent");
|
||||
statusAssert
|
||||
.extracting("timestamp")
|
||||
.isEqualTo(NOW);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetAll() {
|
||||
ArrayList<FileLock> existingLocks = new ArrayList<>();
|
||||
when(lockStore.getAll()).thenReturn(existingLocks);
|
||||
|
||||
Collection<FileLock> all = lockCommand.getAll();
|
||||
|
||||
assertThat(all).isSameAs(existingLocks);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.repository.spi;
|
||||
|
||||
import org.assertj.core.api.AbstractObjectAssert;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.RepositoryTestData;
|
||||
import sonia.scm.repository.api.FileLock;
|
||||
import sonia.scm.repository.api.FileLockedException;
|
||||
import sonia.scm.store.DataStoreFactory;
|
||||
import sonia.scm.store.InMemoryByteDataStoreFactory;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
class GitFileLockStoreFactoryTest {
|
||||
|
||||
private static final Instant NOW = Instant.ofEpochSecond(-562031958);
|
||||
|
||||
private final DataStoreFactory dataStoreFactory = new InMemoryByteDataStoreFactory();
|
||||
private final Clock clock = mock(Clock.class);
|
||||
private String currentUser = "dent";
|
||||
private int nextId = 0;
|
||||
private final GitFileLockStoreFactory gitFileLockStoreFactory =
|
||||
new GitFileLockStoreFactory(dataStoreFactory, () -> "id-" + (nextId++), clock, () -> currentUser);
|
||||
|
||||
private final Repository repository = RepositoryTestData.createHeartOfGold();
|
||||
|
||||
@BeforeEach
|
||||
void setClock() {
|
||||
when(clock.instant()).thenReturn(NOW);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHaveNoLockOnStartup() {
|
||||
GitFileLockStoreFactory.GitFileLockStore gitFileLockStore = gitFileLockStoreFactory.create(repository);
|
||||
|
||||
assertThat(gitFileLockStore.getLock("some/file.txt"))
|
||||
.isEmpty();
|
||||
assertThat(gitFileLockStore.getAll())
|
||||
.isEmpty();
|
||||
assertThat(gitFileLockStore.hasLocks())
|
||||
.isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotFailOnRemovingNonExistingLock() {
|
||||
GitFileLockStoreFactory.GitFileLockStore gitFileLockStore = gitFileLockStoreFactory.create(repository);
|
||||
|
||||
gitFileLockStore.remove("some/file.txt", false);
|
||||
|
||||
assertThat(gitFileLockStore.getLock("some/file.txt"))
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldStoreAndRetrieveLock() {
|
||||
GitFileLockStoreFactory.GitFileLockStore gitFileLockStore = gitFileLockStoreFactory.create(repository);
|
||||
|
||||
FileLock createdLock = gitFileLockStore.put("some/file.txt");
|
||||
|
||||
Optional<FileLock> retrievedLock = gitFileLockStore.getLock("some/file.txt");
|
||||
|
||||
AbstractObjectAssert<?, FileLock> lockAssert = assertThat(retrievedLock)
|
||||
.get();
|
||||
lockAssert
|
||||
.extracting("userId")
|
||||
.isEqualTo("dent");
|
||||
lockAssert
|
||||
.extracting("id")
|
||||
.isEqualTo("id-0");
|
||||
lockAssert
|
||||
.extracting("timestamp")
|
||||
.isEqualTo(NOW);
|
||||
lockAssert
|
||||
.usingRecursiveComparison()
|
||||
.isEqualTo(createdLock);
|
||||
|
||||
assertThat(gitFileLockStore.getAll())
|
||||
.extracting("userId")
|
||||
.containsExactly("dent");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRetrieveLockById() {
|
||||
GitFileLockStoreFactory.GitFileLockStore gitFileLockStore = gitFileLockStoreFactory.create(repository);
|
||||
|
||||
FileLock createdLock = gitFileLockStore.put("some/file.txt");
|
||||
|
||||
Optional<FileLock> retrievedLock = gitFileLockStore.getById(createdLock.getId());
|
||||
|
||||
assertThat(retrievedLock)
|
||||
.get()
|
||||
.usingRecursiveComparison()
|
||||
.isEqualTo(createdLock);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHaveLock() {
|
||||
GitFileLockStoreFactory.GitFileLockStore gitFileLockStore = gitFileLockStoreFactory.create(repository);
|
||||
|
||||
gitFileLockStore.put("some/file.txt");
|
||||
|
||||
assertThat(gitFileLockStore.hasLocks())
|
||||
.isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBeModifiableWithoutLock() {
|
||||
GitFileLockStoreFactory.GitFileLockStore gitFileLockStore = gitFileLockStoreFactory.create(repository);
|
||||
|
||||
gitFileLockStore.assertModifiable("some/file.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBeModifiableWithOwnLock() {
|
||||
GitFileLockStoreFactory.GitFileLockStore gitFileLockStore = gitFileLockStoreFactory.create(repository);
|
||||
gitFileLockStore.put("some/file.txt");
|
||||
|
||||
gitFileLockStore.assertModifiable("some/file.txt");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRemoveLock() {
|
||||
GitFileLockStoreFactory.GitFileLockStore gitFileLockStore = gitFileLockStoreFactory.create(repository);
|
||||
FileLock createdLock = gitFileLockStore.put("some/file.txt");
|
||||
|
||||
gitFileLockStore.remove("some/file.txt", false);
|
||||
|
||||
assertThat(gitFileLockStore.getLock("some/file.txt"))
|
||||
.isEmpty();
|
||||
assertThat(gitFileLockStore.getById(createdLock.getId()))
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRemoveLockById() {
|
||||
GitFileLockStoreFactory.GitFileLockStore gitFileLockStore = gitFileLockStoreFactory.create(repository);
|
||||
FileLock createdLock = gitFileLockStore.put("some/file.txt");
|
||||
|
||||
gitFileLockStore.removeById(createdLock.getId(), false);
|
||||
|
||||
assertThat(gitFileLockStore.getLock("some/file.txt"))
|
||||
.isEmpty();
|
||||
assertThat(gitFileLockStore.getById(createdLock.getId()))
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithExistingLockFromOtherUser {
|
||||
|
||||
@BeforeEach
|
||||
void setLock() {
|
||||
currentUser = "trillian";
|
||||
GitFileLockStoreFactory.GitFileLockStore gitFileLockStore = gitFileLockStoreFactory.create(repository);
|
||||
gitFileLockStore.put("some/file.txt");
|
||||
currentUser = "dent";
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotRemoveExistingLockWithoutForce() {
|
||||
GitFileLockStoreFactory.GitFileLockStore gitFileLockStore = gitFileLockStoreFactory.create(repository);
|
||||
|
||||
assertThrows(FileLockedException.class, () -> gitFileLockStore.remove("some/file.txt", false));
|
||||
|
||||
assertThat(gitFileLockStore.getLock("some/file.txt"))
|
||||
.get()
|
||||
.extracting("userId")
|
||||
.isEqualTo("trillian");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRemoveExistingLockWithForce() {
|
||||
GitFileLockStoreFactory.GitFileLockStore gitFileLockStore = gitFileLockStoreFactory.create(repository);
|
||||
|
||||
gitFileLockStore.remove("some/file.txt", true);
|
||||
|
||||
assertThat(gitFileLockStore.getLock("some/file.txt"))
|
||||
.isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotBeModifiableWithOwnLock() {
|
||||
GitFileLockStoreFactory.GitFileLockStore gitFileLockStore = gitFileLockStoreFactory.create(repository);
|
||||
|
||||
assertThrows(FileLockedException.class, () -> gitFileLockStore.assertModifiable("some/file.txt"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotOverrideExistingLock() {
|
||||
GitFileLockStoreFactory.GitFileLockStore gitFileLockStore = gitFileLockStoreFactory.create(repository);
|
||||
|
||||
assertThrows(FileLockedException.class, () -> gitFileLockStore.put("some/file.txt"));
|
||||
|
||||
assertThat(gitFileLockStore.getLock("some/file.txt"))
|
||||
.get()
|
||||
.extracting("userId")
|
||||
.isEqualTo("trillian");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.web;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.WriteListener;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class CapturingServletOutputStream extends ServletOutputStream {
|
||||
|
||||
private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
baos.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
baos.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return baos.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setWriteListener(WriteListener writeListener) {
|
||||
}
|
||||
|
||||
public JsonNode getContentAsJson() {
|
||||
try {
|
||||
return new ObjectMapper().readTree(toString());
|
||||
} catch (JsonProcessingException e) {
|
||||
throw new RuntimeException("could not unmarshal json content", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,11 +32,8 @@ import sonia.scm.config.ScmConfiguration;
|
||||
import sonia.scm.repository.spi.ScmProviderHttpServlet;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.WriteListener;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
@@ -133,34 +130,4 @@ public class GitPermissionFilterTest {
|
||||
return request;
|
||||
}
|
||||
|
||||
private static class CapturingServletOutputStream extends ServletOutputStream {
|
||||
|
||||
private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
baos.write(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
baos.close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return baos.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setWriteListener(WriteListener writeListener) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,535 @@
|
||||
/*
|
||||
* MIT License
|
||||
*
|
||||
* Copyright (c) 2020-present Cloudogu GmbH and Contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
||||
|
||||
package sonia.scm.web;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import org.assertj.core.api.Condition;
|
||||
import org.github.sdorra.jse.ShiroExtension;
|
||||
import org.github.sdorra.jse.SubjectAware;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import sonia.scm.repository.Repository;
|
||||
import sonia.scm.repository.api.FileLock;
|
||||
import sonia.scm.repository.api.FileLockedException;
|
||||
import sonia.scm.repository.spi.GitFileLockStoreFactory.GitFileLockStore;
|
||||
import sonia.scm.user.DisplayUser;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserDisplayManager;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
|
||||
import static java.time.temporal.ChronoUnit.DAYS;
|
||||
import static java.util.Arrays.asList;
|
||||
import static java.util.Collections.singletonList;
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.Optional.of;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.lenient;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@ExtendWith(ShiroExtension.class)
|
||||
class LfsLockingProtocolServletTest {
|
||||
|
||||
private static final Repository REPOSITORY = new Repository("23", "git", "hitchhiker", "hog");
|
||||
private static final Instant NOW = Instant.ofEpochSecond(-562031958);
|
||||
|
||||
@Mock
|
||||
private GitFileLockStore lockStore;
|
||||
@Mock
|
||||
private UserDisplayManager userDisplayManager;
|
||||
|
||||
private LfsLockingProtocolServlet servlet;
|
||||
|
||||
@Mock
|
||||
private HttpServletRequest request;
|
||||
@Mock
|
||||
private HttpServletResponse response;
|
||||
private final CapturingServletOutputStream responseStream = new CapturingServletOutputStream();
|
||||
|
||||
@BeforeEach
|
||||
void setUpServlet() throws IOException {
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
|
||||
mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
|
||||
mapper.configure(SerializationFeature.WRITE_DATES_WITH_ZONE_ID, true);
|
||||
servlet = new LfsLockingProtocolServlet(REPOSITORY, lockStore, userDisplayManager, mapper, 3, 2);
|
||||
lenient().when(response.getOutputStream()).thenReturn(responseStream);
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUpUserDisplayManager() {
|
||||
lenient().when( userDisplayManager.get("dent"))
|
||||
.thenReturn(of(DisplayUser.from(new User("dent", "Arthur Dent", "irrelevant"))));
|
||||
lenient().when(userDisplayManager.get("trillian"))
|
||||
.thenReturn(of(DisplayUser.from(new User("trillian", "Tricia McMillan", "irrelevant"))));
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithValidLocksPath {
|
||||
|
||||
@BeforeEach
|
||||
void mockValidPath() {
|
||||
when(request.getPathInfo()).thenReturn("repo/hitchhiker/hog.git/info/lfs/locks");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotBeAuthorizedToReadLocks() {
|
||||
servlet.doGet(request, response);
|
||||
|
||||
verify(response).setStatus(403);
|
||||
verify(lockStore, never()).getAll();
|
||||
}
|
||||
|
||||
@Nested
|
||||
@SubjectAware(value = "trillian", permissions = "repository:read,pull:23")
|
||||
class WithReadPermission {
|
||||
|
||||
@Test
|
||||
void shouldGetEmptyArrayForNoFileLocks() {
|
||||
servlet.doGet(request, response);
|
||||
|
||||
verify(response).setStatus(200);
|
||||
JsonNode locks = responseStream.getContentAsJson().get("locks");
|
||||
assertThat(locks).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetAllExistingFileLocks() {
|
||||
when(lockStore.getAll())
|
||||
.thenReturn(
|
||||
asList(
|
||||
new FileLock("some/file", "42", "dent", NOW),
|
||||
new FileLock("other/file", "1337", "trillian", NOW.plus(42, DAYS))
|
||||
));
|
||||
|
||||
servlet.doGet(request, response);
|
||||
|
||||
verify(response).setStatus(200);
|
||||
JsonNode locks = responseStream.getContentAsJson().get("locks");
|
||||
assertThat(locks.get(0))
|
||||
.is(lockNodeWith("42", "some/file", "Arthur Dent", "1952-03-11T00:00:42Z"));
|
||||
assertThat(locks.get(1))
|
||||
.is(lockNodeWith("1337", "other/file", "Tricia McMillan", "1952-04-22T00:00:42Z"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetExistingLockByPath() {
|
||||
when(request.getParameter("path")).thenReturn("some/file");
|
||||
when(lockStore.getLock("some/file"))
|
||||
.thenReturn(of(new FileLock("some/file", "42", "dent", NOW)));
|
||||
|
||||
servlet.doGet(request, response);
|
||||
|
||||
verify(response).setStatus(200);
|
||||
JsonNode locks = responseStream.getContentAsJson().get("locks");
|
||||
assertThat(locks.get(0))
|
||||
.is(lockNodeWith("42", "some/file", "Arthur Dent", "1952-03-11T00:00:42Z"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetEmptyListForNotExistingLockByPath() {
|
||||
when(request.getParameter("path")).thenReturn("some/file");
|
||||
when(lockStore.getLock("some/file"))
|
||||
.thenReturn(empty());
|
||||
|
||||
servlet.doGet(request, response);
|
||||
|
||||
verify(response).setStatus(200);
|
||||
JsonNode locks = responseStream.getContentAsJson().get("locks");
|
||||
assertThat(locks).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetExistingLockById() {
|
||||
when(request.getParameter("path")).thenReturn(null);
|
||||
when(request.getParameter("id")).thenReturn("42");
|
||||
when(lockStore.getById("42"))
|
||||
.thenReturn(of(new FileLock("some/file", "42", "dent", NOW)));
|
||||
|
||||
servlet.doGet(request, response);
|
||||
|
||||
verify(response).setStatus(200);
|
||||
JsonNode locks = responseStream.getContentAsJson().get("locks");
|
||||
assertThat(locks.get(0))
|
||||
.is(lockNodeWith("42", "some/file", "Arthur Dent", "1952-03-11T00:00:42Z"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetEmptyListForNotExistingLockById() {
|
||||
when(request.getParameter("path")).thenReturn(null);
|
||||
when(request.getParameter("id")).thenReturn("42");
|
||||
when(lockStore.getById("42"))
|
||||
.thenReturn(empty());
|
||||
|
||||
servlet.doGet(request, response);
|
||||
|
||||
verify(response).setStatus(200);
|
||||
JsonNode locks = responseStream.getContentAsJson().get("locks");
|
||||
assertThat(locks).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseUserIdIfUserIsUnknown() {
|
||||
when(lockStore.getAll())
|
||||
.thenReturn(
|
||||
singletonList(
|
||||
new FileLock("some/file", "42", "marvin", NOW)
|
||||
));
|
||||
|
||||
servlet.doGet(request, response);
|
||||
|
||||
JsonNode locks = responseStream.getContentAsJson().get("locks");
|
||||
assertThat(locks.get(0))
|
||||
.is(lockNodeWith("42", "some/file", "marvin", "1952-03-11T00:00:42Z"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotBeAuthorizedToCreateNewLock() {
|
||||
servlet.doPost(request, response);
|
||||
|
||||
verify(response).setStatus(403);
|
||||
verify(lockStore, never()).put(any());
|
||||
}
|
||||
|
||||
@Nested
|
||||
class WithLimiting {
|
||||
|
||||
@BeforeEach
|
||||
void mockManyResults() {
|
||||
when(lockStore.getAll())
|
||||
.thenReturn(
|
||||
asList(
|
||||
new FileLock("empty/file", "2", "zaphod", NOW),
|
||||
new FileLock("some/file", "23", "dent", NOW),
|
||||
new FileLock("any/file", "42", "marvin", NOW),
|
||||
new FileLock("other/file", "1337", "trillian", NOW.plus(42, DAYS))
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLimitFileLocksByDefault() {
|
||||
servlet.doGet(request, response);
|
||||
|
||||
verify(response).setStatus(200);
|
||||
JsonNode contentAsJson = responseStream.getContentAsJson();
|
||||
JsonNode locks = contentAsJson.get("locks");
|
||||
assertThat(locks).hasSize(3);
|
||||
assertThat(locks.get(0).get("id").asText()).isEqualTo("2");
|
||||
assertThat(contentAsJson.get("next_cursor").asText()).isEqualTo("3");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseLimitFromRequest() {
|
||||
lenient().doReturn("2").when(request).getParameter("limit");
|
||||
|
||||
servlet.doGet(request, response);
|
||||
|
||||
verify(response).setStatus(200);
|
||||
JsonNode contentAsJson = responseStream.getContentAsJson();
|
||||
JsonNode locks = contentAsJson.get("locks");
|
||||
assertThat(locks).hasSize(2);
|
||||
assertThat(locks.get(0).get("id").asText()).isEqualTo("2");
|
||||
assertThat(contentAsJson.get("next_cursor").asText()).isEqualTo("2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseCursorFromRequest() {
|
||||
lenient().doReturn("3").when(request).getParameter("cursor");
|
||||
|
||||
servlet.doGet(request, response);
|
||||
|
||||
verify(response).setStatus(200);
|
||||
JsonNode contentAsJson = responseStream.getContentAsJson();
|
||||
JsonNode locks = contentAsJson.get("locks");
|
||||
assertThat(locks).hasSize(1);
|
||||
assertThat(locks.get(0).get("id").asText()).isEqualTo("1337");
|
||||
assertThat(contentAsJson.get("next_cursor")).isNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@SubjectAware(value = "trillian", permissions = "repository:read,write,pull,push:23")
|
||||
class WithWritePermission {
|
||||
|
||||
@Test
|
||||
void shouldCreateNewLock() throws IOException {
|
||||
when(request.getInputStream()).thenReturn(new BufferedServletInputStream("{\n" +
|
||||
" \"path\": \"some/file.txt\"\n" +
|
||||
"}"));
|
||||
when(lockStore.put("some/file.txt"))
|
||||
.thenReturn(new FileLock("some/file.txt", "42", "Tricia", NOW));
|
||||
|
||||
servlet.doPost(request, response);
|
||||
|
||||
verify(response).setStatus(201);
|
||||
assertThat(responseStream.getContentAsJson().get("lock"))
|
||||
.is(lockNodeWith("42", "some/file.txt", "Tricia", "1952-03-11T00:00:42Z"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldIgnoreUnknownAttributed() throws IOException {
|
||||
when(request.getInputStream()).thenReturn(new BufferedServletInputStream("{\n" +
|
||||
" \"path\": \"some/file.txt\",\n" +
|
||||
" \"unknown\": \"attribute\"\n" +
|
||||
"}"));
|
||||
when(lockStore.put("some/file.txt"))
|
||||
.thenReturn(new FileLock("some/file.txt", "42", "Tricia", NOW));
|
||||
|
||||
servlet.doPost(request, response);
|
||||
|
||||
verify(response).setStatus(201);
|
||||
assertThat(responseStream.getContentAsJson().get("lock"))
|
||||
.is(lockNodeWith("42", "some/file.txt", "Tricia", "1952-03-11T00:00:42Z"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldHandleInvalidInput() throws IOException {
|
||||
when(request.getInputStream()).thenReturn(new BufferedServletInputStream("{\n" +
|
||||
" \"invalidAttribute\": \"some value\"\n" +
|
||||
"}"));
|
||||
|
||||
servlet.doPost(request, response);
|
||||
|
||||
verify(response).setStatus(400);
|
||||
verify(lockStore, never()).put(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailToCreateExistingLock() throws IOException {
|
||||
when(request.getInputStream()).thenReturn(new BufferedServletInputStream("{\n" +
|
||||
" \"path\": \"some/file.txt\"\n" +
|
||||
"}"));
|
||||
when(lockStore.put("some/file.txt"))
|
||||
.thenThrow(new FileLockedException(REPOSITORY.getNamespaceAndName(), new FileLock("some/file.txt", "42", "Tricia", NOW)));
|
||||
|
||||
servlet.doPost(request, response);
|
||||
|
||||
verify(response).setStatus(409);
|
||||
JsonNode contentAsJson = responseStream.getContentAsJson();
|
||||
assertThat(contentAsJson.get("lock"))
|
||||
.is(lockNodeWith("42", "some/file.txt", "Tricia", "1952-03-11T00:00:42Z"));
|
||||
assertThat(contentAsJson.get("message")).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetVerifyResult() throws IOException {
|
||||
when(request.getPathInfo()).thenReturn("repo/hitchhiker/hog.git/info/lfs/locks/verify");
|
||||
when(request.getInputStream()).thenReturn(new BufferedServletInputStream("{}"));
|
||||
when(lockStore.getAll())
|
||||
.thenReturn(
|
||||
asList(
|
||||
new FileLock("some/file", "42", "dent", NOW),
|
||||
new FileLock("other/file", "1337", "trillian", NOW.plus(42, DAYS))
|
||||
));
|
||||
|
||||
servlet.doPost(request, response);
|
||||
|
||||
verify(response).setStatus(200);
|
||||
JsonNode ourLocks = responseStream.getContentAsJson().get("ours");
|
||||
assertThat(ourLocks.get(0))
|
||||
.is(lockNodeWith("1337", "other/file", "Tricia McMillan", "1952-04-22T00:00:42Z"));
|
||||
JsonNode theirLocks = responseStream.getContentAsJson().get("theirs");
|
||||
assertThat(theirLocks.get(0))
|
||||
.is(lockNodeWith("42", "some/file", "Arthur Dent", "1952-03-11T00:00:42Z"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldGetVerifyResultForNoFileLocks() throws IOException {
|
||||
when(request.getPathInfo()).thenReturn("repo/hitchhiker/hog.git/info/lfs/locks/verify");
|
||||
when(request.getInputStream()).thenReturn(new BufferedServletInputStream("{}"));
|
||||
|
||||
servlet.doPost(request, response);
|
||||
|
||||
verify(response).setStatus(200);
|
||||
JsonNode ourLocks = responseStream.getContentAsJson().get("ours");
|
||||
assertThat(ourLocks).isEmpty();
|
||||
JsonNode theirLocks = responseStream.getContentAsJson().get("theirs");
|
||||
assertThat(theirLocks).isEmpty();
|
||||
}
|
||||
|
||||
@Nested
|
||||
class VerifyWithLimiting {
|
||||
|
||||
@BeforeEach
|
||||
void mockManyResults() {
|
||||
when(lockStore.getAll())
|
||||
.thenReturn(
|
||||
asList(
|
||||
new FileLock("empty/file", "2", "zaphod", NOW),
|
||||
new FileLock("some/file", "23", "dent", NOW),
|
||||
new FileLock("any/file", "42", "marvin", NOW),
|
||||
new FileLock("other/file", "1337", "trillian", NOW.plus(42, DAYS))
|
||||
));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldLimitVerifyByDefault() throws IOException {
|
||||
when(request.getPathInfo()).thenReturn("repo/hitchhiker/hog.git/info/lfs/locks/verify");
|
||||
when(request.getInputStream()).thenReturn(new BufferedServletInputStream("{}"));
|
||||
|
||||
servlet.doPost(request, response);
|
||||
|
||||
verify(response).setStatus(200);
|
||||
JsonNode contentAsJson = responseStream.getContentAsJson();
|
||||
JsonNode ourLocks = contentAsJson.get("ours");
|
||||
assertThat(ourLocks).isEmpty();
|
||||
JsonNode theirLocks = contentAsJson.get("theirs");
|
||||
assertThat(theirLocks).hasSize(3);
|
||||
assertThat(theirLocks.get(0).get("id").asText()).isEqualTo("2");
|
||||
assertThat(contentAsJson.get("next_cursor").asText()).isEqualTo("3");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseLimitFromRequest() throws IOException {
|
||||
when(request.getPathInfo()).thenReturn("repo/hitchhiker/hog.git/info/lfs/locks/verify");
|
||||
when(request.getInputStream()).thenReturn(new BufferedServletInputStream("{\"limit\":2}"));
|
||||
|
||||
servlet.doPost(request, response);
|
||||
|
||||
verify(response).setStatus(200);
|
||||
JsonNode contentAsJson = responseStream.getContentAsJson();
|
||||
JsonNode ourLocks = contentAsJson.get("ours");
|
||||
assertThat(ourLocks).isEmpty();
|
||||
JsonNode theirLocks = contentAsJson.get("theirs");
|
||||
assertThat(theirLocks).hasSize(2);
|
||||
assertThat(theirLocks.get(0).get("id").asText()).isEqualTo("2");
|
||||
assertThat(contentAsJson.get("next_cursor").asText()).isEqualTo("2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseCursorFromRequest() throws IOException {
|
||||
when(request.getPathInfo()).thenReturn("repo/hitchhiker/hog.git/info/lfs/locks/verify");
|
||||
when(request.getInputStream()).thenReturn(new BufferedServletInputStream("{\"cursor\":\"3\"}"));
|
||||
|
||||
servlet.doPost(request, response);
|
||||
|
||||
verify(response).setStatus(200);
|
||||
JsonNode contentAsJson = responseStream.getContentAsJson();
|
||||
JsonNode ourLocks = contentAsJson.get("ours");
|
||||
assertThat(ourLocks).hasSize(1);
|
||||
assertThat(ourLocks.get(0).get("id").asText()).isEqualTo("1337");
|
||||
JsonNode theirLocks = contentAsJson.get("theirs");
|
||||
assertThat(theirLocks).isEmpty();
|
||||
assertThat(contentAsJson.get("next_cursor")).isNull();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteExistingFileLock() throws IOException {
|
||||
when(request.getPathInfo()).thenReturn("repo/hitchhiker/hog.git/info/lfs/locks/42/unlock");
|
||||
when(request.getInputStream()).thenReturn(new BufferedServletInputStream("{}"));
|
||||
FileLock expectedLock = new FileLock("some/file.txt", "42", "trillian", NOW);
|
||||
when(lockStore.removeById("42", false))
|
||||
.thenReturn(of(expectedLock));
|
||||
|
||||
servlet.doPost(request, response);
|
||||
|
||||
verify(response).setStatus(200);
|
||||
JsonNode deletedLock = responseStream.getContentAsJson().get("lock");
|
||||
assertThat(deletedLock).is(lockNodeWith(expectedLock, "Tricia McMillan"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailToDeleteFileLockByAnotherUser() throws IOException {
|
||||
when(request.getPathInfo()).thenReturn("repo/hitchhiker/hog.git/info/lfs/locks/42/unlock");
|
||||
when(request.getInputStream()).thenReturn(new BufferedServletInputStream("{}"));
|
||||
when(lockStore.removeById("42", false))
|
||||
.thenThrow(new FileLockedException(REPOSITORY.getNamespaceAndName(), new FileLock("some/file.txt", "42", "dent", NOW)));
|
||||
|
||||
servlet.doPost(request, response);
|
||||
|
||||
verify(response).setStatus(403);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDeleteExistingLockWithForceFlag() throws IOException {
|
||||
when(request.getPathInfo()).thenReturn("repo/hitchhiker/hog.git/info/lfs/locks/42/unlock");
|
||||
when(request.getInputStream()).thenReturn(new BufferedServletInputStream("{\"force\":true}"));
|
||||
FileLock expectedLock = new FileLock("some/file.txt", "42", "dent", NOW);
|
||||
when(lockStore.removeById("42", true))
|
||||
.thenReturn(of(expectedLock));
|
||||
|
||||
servlet.doPost(request, response);
|
||||
|
||||
verify(response).setStatus(200);
|
||||
JsonNode deletedLock = responseStream.getContentAsJson().get("lock");
|
||||
assertThat(deletedLock).is(lockNodeWith(expectedLock, "Arthur Dent"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldFailForIllegalPath() {
|
||||
when(request.getPathInfo()).thenReturn("repo/hitchhiker/hog.git/info/lfs/other");
|
||||
|
||||
servlet.doGet(request, response);
|
||||
|
||||
verify(response).setStatus(400);
|
||||
}
|
||||
|
||||
private Condition<? super Iterable<? extends JsonNode>> lockNodeWith(FileLock lock, String expectedName) {
|
||||
return new Condition<Iterable<? extends JsonNode>>() {
|
||||
@Override
|
||||
public boolean matches(Iterable<? extends JsonNode> value) {
|
||||
JsonNode node = (JsonNode) value;
|
||||
assertThat(node.get("id").asText()).isEqualTo(lock.getId());
|
||||
assertThat(node.get("path").asText()).isEqualTo(lock.getPath());
|
||||
assertThat(node.get("owner").get("name").asText()).isEqualTo(expectedName);
|
||||
assertThat(node.get("locked_at").asText()).isEqualTo(lock.getTimestamp().toString());
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private Condition<? super Iterable<? extends JsonNode>> lockNodeWith(String expectedId, String expectedPath, String expectedName, String expectedTimestamp) {
|
||||
return new Condition<Iterable<? extends JsonNode>>() {
|
||||
@Override
|
||||
public boolean matches(Iterable<? extends JsonNode> value) {
|
||||
JsonNode node = (JsonNode) value;
|
||||
assertThat(node.get("id").asText()).isEqualTo(expectedId);
|
||||
assertThat(node.get("path").asText()).isEqualTo(expectedPath);
|
||||
assertThat(node.get("owner").get("name").asText()).isEqualTo(expectedName);
|
||||
assertThat(node.get("locked_at").asText()).isEqualTo(expectedTimestamp);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user