From 12a55c34753669365d9e644e592d9d8c10e742f1 Mon Sep 17 00:00:00 2001 From: Dave Borowitz Date: Fri, 27 Feb 2015 15:08:50 -0800 Subject: [PATCH] Add an in-process pack transport for use in tests This allows for testing arbitrary sets of push/fetch hooks (e.g. PreReceiveHook) without depending on either an external protocol (e.g. HTTP) or the local filesystem. Change-Id: I4ba2fff9c8a484f990dea05e14b0772deddb7411 --- org.eclipse.jgit.test/META-INF/MANIFEST.MF | 1 + .../jgit/transport/TestProtocolTest.java | 248 ++++++++++++++++++ .../eclipse/jgit/internal/JGitText.properties | 1 + .../org/eclipse/jgit/internal/JGitText.java | 1 + .../eclipse/jgit/transport/TestProtocol.java | 194 ++++++++++++++ 5 files changed, 445 insertions(+) create mode 100644 org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TestProtocolTest.java create mode 100644 org.eclipse.jgit/src/org/eclipse/jgit/transport/TestProtocol.java diff --git a/org.eclipse.jgit.test/META-INF/MANIFEST.MF b/org.eclipse.jgit.test/META-INF/MANIFEST.MF index f1c610b4f..199c586a2 100644 --- a/org.eclipse.jgit.test/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.test/META-INF/MANIFEST.MF @@ -41,6 +41,7 @@ Import-Package: com.googlecode.javaewah;version="[0.7.9,0.8.0)", org.eclipse.jgit.submodule;version="[4.0.0,4.1.0)", org.eclipse.jgit.transport;version="[4.0.0,4.1.0)", org.eclipse.jgit.transport.http;version="[4.0.0,4.1.0)", + org.eclipse.jgit.transport.resolver;version="[4.0.0,4.1.0)", org.eclipse.jgit.treewalk;version="[4.0.0,4.1.0)", org.eclipse.jgit.treewalk.filter;version="[4.0.0,4.1.0)", org.eclipse.jgit.util;version="[4.0.0,4.1.0)", diff --git a/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TestProtocolTest.java b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TestProtocolTest.java new file mode 100644 index 000000000..7f865263d --- /dev/null +++ b/org.eclipse.jgit.test/tst/org/eclipse/jgit/transport/TestProtocolTest.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2015, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.transport; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.InvalidRemoteException; +import org.eclipse.jgit.api.errors.TransportException; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription; +import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; +import org.eclipse.jgit.junit.TestRepository; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.resolver.ReceivePackFactory; +import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException; +import org.eclipse.jgit.transport.resolver.UploadPackFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class TestProtocolTest { + private static final RefSpec HEADS = new RefSpec("+refs/heads/*:refs/heads/*"); + + private static class User { + private final String name; + + private User(String name) { + this.name = name; + } + } + + private static class DefaultUpload implements UploadPackFactory { + @Override + public UploadPack create(User req, Repository db) { + return new UploadPack(db); + } + } + + private static class DefaultReceive implements ReceivePackFactory { + @Override + public ReceivePack create(User req, Repository db) { + return new ReceivePack(db); + } + } + + private List protos; + private TestRepository local; + private TestRepository remote; + + @Before + public void setUp() throws Exception { + protos = new ArrayList(); + local = new TestRepository( + new InMemoryRepository(new DfsRepositoryDescription("local"))); + remote = new TestRepository( + new InMemoryRepository(new DfsRepositoryDescription("remote"))); + } + + @After + public void tearDown() { + for (TransportProtocol proto : protos) { + Transport.unregister(proto); + } + } + + @Test + public void testFetch() throws Exception { + ObjectId master = remote.branch("master").commit().create(); + + TestProtocol proto = registerDefault(); + URIish uri = proto.register(new User("user"), remote.getRepository()); + + try (Git git = new Git(local.getRepository())) { + git.fetch() + .setRemote(uri.toString()) + .setRefSpecs(HEADS) + .call(); + assertEquals(master, + local.getRepository().getRef("master").getObjectId()); + } + } + + @Test + public void testPush() throws Exception { + ObjectId master = local.branch("master").commit().create(); + + TestProtocol proto = registerDefault(); + URIish uri = proto.register(new User("user"), remote.getRepository()); + + try (Git git = new Git(local.getRepository())) { + git.push() + .setRemote(uri.toString()) + .setRefSpecs(HEADS) + .call(); + assertEquals(master, + remote.getRepository().getRef("master").getObjectId()); + } + } + + @Test + public void testUploadPackFactory() throws Exception { + ObjectId master = remote.branch("master").commit().create(); + + final AtomicInteger rejected = new AtomicInteger(); + TestProtocol proto = registerProto(new UploadPackFactory() { + @Override + public UploadPack create(User req, Repository db) + throws ServiceNotAuthorizedException { + if (!"user2".equals(req.name)) { + rejected.incrementAndGet(); + throw new ServiceNotAuthorizedException(); + } + return new UploadPack(db); + } + }, new DefaultReceive()); + + // Same repository, different users. + URIish user1Uri = proto.register(new User("user1"), remote.getRepository()); + URIish user2Uri = proto.register(new User("user2"), remote.getRepository()); + + try (Git git = new Git(local.getRepository())) { + try { + git.fetch() + .setRemote(user1Uri.toString()) + .setRefSpecs(HEADS) + .call(); + } catch (InvalidRemoteException expected) { + // Expected. + } + assertEquals(1, rejected.get()); + assertNull(local.getRepository().getRef("master")); + + git.fetch() + .setRemote(user2Uri.toString()) + .setRefSpecs(HEADS) + .call(); + assertEquals(1, rejected.get()); + assertEquals(master, + local.getRepository().getRef("master").getObjectId()); + } + } + + @Test + public void testReceivePackFactory() throws Exception { + ObjectId master = local.branch("master").commit().create(); + + final AtomicInteger rejected = new AtomicInteger(); + TestProtocol proto = registerProto(new DefaultUpload(), + new ReceivePackFactory() { + @Override + public ReceivePack create(User req, Repository db) + throws ServiceNotAuthorizedException { + if (!"user2".equals(req.name)) { + rejected.incrementAndGet(); + throw new ServiceNotAuthorizedException(); + } + return new ReceivePack(db); + } + }); + + // Same repository, different users. + URIish user1Uri = proto.register(new User("user1"), remote.getRepository()); + URIish user2Uri = proto.register(new User("user2"), remote.getRepository()); + + try (Git git = new Git(local.getRepository())) { + try { + git.push() + .setRemote(user1Uri.toString()) + .setRefSpecs(HEADS) + .call(); + } catch (TransportException expected) { + assertTrue(expected.getMessage().contains( + JGitText.get().pushNotPermitted)); + } + assertEquals(1, rejected.get()); + assertNull(remote.getRepository().getRef("master")); + + git.push() + .setRemote(user2Uri.toString()) + .setRefSpecs(HEADS) + .call(); + assertEquals(1, rejected.get()); + assertEquals(master, + remote.getRepository().getRef("master").getObjectId()); + } + } + + private TestProtocol registerDefault() { + return registerProto(new DefaultUpload(), new DefaultReceive()); + } + + private TestProtocol registerProto(UploadPackFactory upf, + ReceivePackFactory rpf) { + TestProtocol proto = new TestProtocol(upf, rpf); + protos.add(proto); + Transport.register(proto); + return proto; + } +} diff --git a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties index 17ac8102d..109027d93 100644 --- a/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties +++ b/org.eclipse.jgit/resources/org/eclipse/jgit/internal/JGitText.properties @@ -525,6 +525,7 @@ transportProtoHTTP=HTTP transportProtoLocal=Local Git Repository transportProtoSFTP=SFTP transportProtoSSH=SSH +transportProtoTest=Test transportSSHRetryInterrupt=Interrupted while waiting for retry treeEntryAlreadyExists=Tree entry "{0}" already exists. treeFilterMarkerTooManyFilters=Too many markTreeFilters passed, maximum number is {0} (passed {1}) diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java index bbcb69899..d567a2474 100644 --- a/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java +++ b/org.eclipse.jgit/src/org/eclipse/jgit/internal/JGitText.java @@ -584,6 +584,7 @@ public class JGitText extends TranslationBundle { /***/ public String transportProtoLocal; /***/ public String transportProtoSFTP; /***/ public String transportProtoSSH; + /***/ public String transportProtoTest; /***/ public String transportSSHRetryInterrupt; /***/ public String treeEntryAlreadyExists; /***/ public String treeFilterMarkerTooManyFilters; diff --git a/org.eclipse.jgit/src/org/eclipse/jgit/transport/TestProtocol.java b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TestProtocol.java new file mode 100644 index 000000000..524301050 --- /dev/null +++ b/org.eclipse.jgit/src/org/eclipse/jgit/transport/TestProtocol.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2015, Google Inc. + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.transport; + +import java.net.URISyntaxException; +import java.text.MessageFormat; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Set; + +import org.eclipse.jgit.errors.NotSupportedException; +import org.eclipse.jgit.errors.TransportException; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.transport.resolver.ReceivePackFactory; +import org.eclipse.jgit.transport.resolver.UploadPackFactory; + +/** + * Protocol for transport between manually-specified repositories in tests. + *

+ * Remote repositories are registered using {@link #register(Object, + * Repository)}, after which they can be accessed using the returned URI. As + * this class provides both the client side (the protocol) and the server side, + * the caller is responsible for setting up and passing the connection context, + * whatever form that may take. + *

+ * Unlike the other built-in protocols, which are automatically-registered + * singletons, callers are expected to register/unregister specific protocol + * instances on demand with {@link Transport#register(TransportProtocol)}. + * + * @param + * the connection type + * @since 4.0 + */ +public class TestProtocol extends TransportProtocol { + private static final String SCHEME = "test"; //$NON-NLS-1$ + + private class Handle { + private final C req; + private final Repository remote; + + private Handle(C req, Repository remote) { + this.req = req; + this.remote = remote; + } + } + + private final UploadPackFactory uploadPackFactory; + private final ReceivePackFactory receivePackFactory; + private final HashMap handles; + + /** + * @param uploadPackFactory + * factory for creating {@link UploadPack} used by all connections + * from this protocol instance. + * @param receivePackFactory + * factory for creating {@link ReceivePack} used by all connections + * from this protocol instance. + */ + public TestProtocol(UploadPackFactory uploadPackFactory, + ReceivePackFactory receivePackFactory) { + this.uploadPackFactory = uploadPackFactory; + this.receivePackFactory = receivePackFactory; + this.handles = new HashMap(); + } + + @Override + public String getName() { + return JGitText.get().transportProtoTest; + } + + @Override + public Set getSchemes() { + return Collections.singleton(SCHEME); + } + + @Override + public Transport open(URIish uri, Repository local, String remoteName) + throws NotSupportedException, TransportException { + Handle h = handles.get(uri); + if (h == null) { + throw new NotSupportedException(MessageFormat.format( + JGitText.get().URINotSupported, uri)); + } + return new TransportInternal(local, uri, h); + } + + @Override + public Set getRequiredFields() { + return EnumSet.of(URIishField.HOST, URIishField.PATH); + } + + @Override + public Set getOptionalFields() { + return Collections.emptySet(); + } + + /** + * Register a repository connection over the internal test protocol. + * + * @param req + * connection context. This instance is reused for all connections + * made using this protocol; if it is stateful and usable only for + * one connection, the same repository should be registered + * multiple times. + * @param remote + * remote repository to connect to. + * @return a URI that can be used to connect to this repository for both fetch + * and push. + */ + public synchronized URIish register(C req, Repository remote) { + URIish uri; + try { + int n = handles.size(); + uri = new URIish(SCHEME + "://test/conn" + n); //$NON-NLS-1$ + } catch (URISyntaxException e) { + throw new IllegalStateException(); + } + handles.put(uri, new Handle(req, remote)); + return uri; + } + + private class TransportInternal extends Transport implements PackTransport { + private final Handle handle; + + private TransportInternal(Repository local, URIish uri, Handle handle) { + super(local, uri); + this.handle = handle; + } + + @Override + public FetchConnection openFetch() throws NotSupportedException, + TransportException { + handle.remote.incrementOpen(); + return new InternalFetchConnection( + this, uploadPackFactory, handle.req, handle.remote); + } + + @Override + public PushConnection openPush() throws NotSupportedException, + TransportException { + handle.remote.incrementOpen(); + return new InternalPushConnection( + this, receivePackFactory, handle.req, handle.remote); + } + + @Override + public void close() { + // Resources must be established per-connection. + } + } +}