Files
guice/core/test/com/google/inject/RestrictedBindingSourceTest.java
T

904 lines
28 KiB
Java

package com.google.inject;
import static com.google.common.truth.Truth.assertThat;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE_USE;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableSet;
import com.google.common.testing.TestLogHandler;
import com.google.inject.RestrictedBindingSource.RestrictionLevel;
import com.google.inject.spi.ElementSource;
import com.google.inject.spi.Elements;
import com.google.inject.spi.InjectionPoint;
import com.google.inject.spi.ModuleAnnotatedMethodScanner;
import com.google.inject.util.Modules;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import javax.inject.Named;
import javax.inject.Qualifier;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/**
* Tests for {@link RestrictedBindingSource}.
*
* @author vzm@google.com (Vladimir Makaric)
*/
@RunWith(JUnit4.class)
public class RestrictedBindingSourceTest {
// --------------------------------------------------------------------------
// Core Functionality Tests
// --------------------------------------------------------------------------
private static final String BINDING_PERMISSION_ERROR = "Unable to bind key";
private static final String USE_NETWORK_MODULE = "Please install NetworkModule.";
private static final String USE_ROUTING_MODULE = "Please install RoutingModule.";
private static final String NETWORK_ANNOTATION_IS_RESTRICTED =
"The @Network annotation can only be used to annotate Network library Keys.";
@RestrictedBindingSource.Permit
@Retention(RetentionPolicy.RUNTIME)
@interface NetworkLibrary {}
@Qualifier
@RestrictedBindingSource(
explanation = USE_NETWORK_MODULE,
permits = {NetworkLibrary.class})
@Retention(RetentionPolicy.RUNTIME)
@interface GatewayIpAdress {}
@Qualifier
@RestrictedBindingSource(
explanation = USE_NETWORK_MODULE,
permits = {NetworkLibrary.class})
@Retention(RetentionPolicy.RUNTIME)
@interface Hostname {}
@NetworkLibrary
private static class NetworkModule extends AbstractModule {
@Provides
@GatewayIpAdress
int provideIpAddress() {
return 21321566;
}
@Override
protected void configure() {
bind(String.class).annotatedWith(Hostname.class).toInstance("google.com");
}
}
@Test
public void networkLibraryCanProvideItsBindings() {
Guice.createInjector(new NetworkModule());
}
@RestrictedBindingSource(
explanation = USE_ROUTING_MODULE,
permits = {NetworkLibrary.class})
@ImplementedBy(RoutingTableImpl.class) // For testing untargetted bindings.
interface RoutingTable {
int getNextHopIpAddress(int destinationIpAddress);
}
@NetworkLibrary
private static class RoutingModule extends AbstractModule {
@Provides
RoutingTable provideRoutingTable(@GatewayIpAdress int gateway) {
return destinationIp -> gateway;
}
}
@Test
public void networkBindingCantBeProvidedByOtherModules() {
AbstractModule rogueModule =
new AbstractModule() {
// This will fail, the gateway IP can only be provided by the network library.
@Provides
@GatewayIpAdress
int provideGatewayIp() {
return 42;
}
@Override
protected void configure() {
install(new RoutingModule());
}
};
CreationException expected = assertThatInjectorCreationFails(rogueModule);
assertThat(expected).hasMessageThat().contains(BINDING_PERMISSION_ERROR);
assertThat(expected).hasMessageThat().contains(USE_NETWORK_MODULE);
}
@Test
public void missingImplementationErrorForRestrictedBindingIncludesExplanation() {
CreationException expected = assertThatInjectorCreationFails(new RoutingModule());
assertThat(expected)
.hasMessageThat()
.contains(
"Hint: This key is restricted and cannot be bound directly. Restriction explanation:"
+ " Please install NetworkModule.");
}
@Test
public void canBindRestrictedTypeWithUnrestrictedQualifierAnnotation() {
Guice.createInjector(
new AbstractModule() {
@Provides
@Named("custom")
RoutingTable provideRoutingTable() {
return ip -> ip;
}
});
}
@Test
public void twoRogueNetworkBindingsYieldTwoErrorMessages() {
AbstractModule rogueModule =
new AbstractModule() {
@Provides
@GatewayIpAdress
int provideGatewayIp() {
return 42;
}
@Provides
RoutingTable provideRoutingTable() {
return destinationIp -> 0;
}
};
CreationException expected = assertThatInjectorCreationFails(rogueModule);
assertThat(expected).hasMessageThat().contains(BINDING_PERMISSION_ERROR);
assertThat(expected).hasMessageThat().contains(USE_NETWORK_MODULE);
assertThat(expected).hasMessageThat().contains(USE_ROUTING_MODULE);
}
@RestrictedBindingSource.Permit
@Retention(RetentionPolicy.RUNTIME)
@interface NetworkTestLibrary {}
@Qualifier
@RestrictedBindingSource(
explanation = USE_NETWORK_MODULE,
permits = {NetworkLibrary.class, NetworkTestLibrary.class})
@Retention(RetentionPolicy.RUNTIME)
@interface MacAddress {}
@NetworkTestLibrary
private static class TestMacAddressModule extends AbstractModule {
@Provides
@MacAddress
String provideMacAddress() {
return "deadbeef";
}
}
@Test
public void bindingWithTwoPermitsAllowedIfOnePresent() {
Guice.createInjector(new TestMacAddressModule());
}
private static class RoutingTableImpl implements RoutingTable {
@Inject
RoutingTableImpl() {}
@Override
public int getNextHopIpAddress(int destinationIpAddress) {
return destinationIpAddress + 2;
}
}
@Test
public void untargettedBindingAllowedWithPermit() {
@NetworkLibrary
class PermittedNetworkModule extends AbstractModule {
@Override
protected void configure() {
bind(RoutingTable.class);
}
}
Guice.createInjector(new PermittedNetworkModule());
}
@Test
public void untargettedBindingDisallowedWithoutPermit() {
AbstractModule rogueModule =
new AbstractModule() {
@Override
protected void configure() {
bind(RoutingTable.class);
}
};
CreationException expected = assertThatInjectorCreationFails(rogueModule);
assertThat(expected).hasMessageThat().contains(BINDING_PERMISSION_ERROR);
assertThat(expected).hasMessageThat().contains(USE_ROUTING_MODULE);
}
@RestrictedBindingSource.Permit
@Retention(RetentionPolicy.RUNTIME)
@Target(TYPE_USE)
@interface FooPermit {}
@Qualifier
@RestrictedBindingSource(
explanation = "Only modules with FooPermit can bind @Foo bindings.",
permits = {FooPermit.class})
@Retention(RetentionPolicy.RUNTIME)
@interface Foo {}
@Test
public void permitOnAnonymousClassWorks() {
Guice.createInjector(
new @FooPermit AbstractModule() {
@Provides
@Foo
String provideFooString() {
return "foo";
}
});
}
@Qualifier
@RestrictedBindingSource(
explanation = USE_NETWORK_MODULE,
permits = {NetworkLibrary.class},
restrictionLevel = RestrictionLevel.WARNING)
@Retention(RetentionPolicy.RUNTIME)
@interface HostIp {}
@Test
public void rogueBindingWithWarningRestrictionLevel() {
Logger logger = Logger.getLogger(RestrictedBindingSource.class.getName());
TestLogHandler testLogHandler = new TestLogHandler();
logger.addHandler(testLogHandler);
Guice.createInjector(
new AbstractModule() {
@Provides
@HostIp
int provideRogueHostIp() {
return 4;
}
});
List<LogRecord> logs = testLogHandler.getStoredLogRecords();
assertThat(logs).hasSize(1);
assertThat(logs.get(0).getLevel()).isEqualTo(Level.WARNING);
assertThat(logs.get(0).getMessage()).contains(USE_NETWORK_MODULE);
assertThat(logs.get(0).getMessage()).contains("provideRogueHostIp");
logger.removeHandler(testLogHandler);
}
// --------------------------------------------------------------------------
// Module Exemption Tests
// --------------------------------------------------------------------------
private static final String USE_DNS_MODULE = "Use the official DNS module";
@Qualifier
@RestrictedBindingSource(
explanation = USE_DNS_MODULE,
permits = {NetworkLibrary.class},
exemptModules =
"com.google.inject.RestrictedBindingSourceTest\\$FooRogueDnsModule"
+ "|com.google.inject.RestrictedBindingSourceTest\\$BarRogueDnsModule"
+ "|com.google.inject.RestrictedBindingSourceTest\\$TopLevelModulePrivatelyBindingDnsAddress")
@Retention(RetentionPolicy.RUNTIME)
@interface DnsAddress {}
static class FooRogueDnsModule extends AbstractModule {
@Provides
@DnsAddress
int rogueDns() {
return 4;
}
}
static class BarRogueDnsModule extends AbstractModule {
@Provides
@DnsAddress
int rogueDns() {
return 5;
}
}
// Non-exempt
static class BazRogueDnsModule extends AbstractModule {
@Provides
@DnsAddress
int rogueDns() {
return 5;
}
}
static class TopLevelModulePrivatelyBindingDnsAddress extends AbstractModule {
@Override
protected void configure() {
install(
new PrivateModule() {
@Override
protected void configure() {
// Non-exempt module.
install(new BazRogueDnsModule());
}
});
}
}
@Test
public void exemptModulesCanCreateRestrictedBinding() {
Guice.createInjector(new FooRogueDnsModule());
Guice.createInjector(new BarRogueDnsModule());
}
@Test
public void nonExemptModuleCantCreateRestrictedBinding() {
CreationException expected = assertThatInjectorCreationFails(new BazRogueDnsModule());
assertThat(expected).hasMessageThat().contains(BINDING_PERMISSION_ERROR);
assertThat(expected).hasMessageThat().contains(USE_DNS_MODULE);
}
@Test
public void parentModuleExeptionAppliesToChildPrivateModule() {
Guice.createInjector(new TopLevelModulePrivatelyBindingDnsAddress());
}
@Test
public void exemptModuleCanBeOverridenIfRestrictedBindingIsNotOverriden() {
// This tests that we check for exemptions on the module stack of the original element source.
Guice.createInjector(
Modules.override(
new AbstractModule() {
@Override
protected void configure() {
install(new BarRogueDnsModule());
}
@Provides
String random() {
return "foo";
}
})
.with(
new AbstractModule() {
@Provides
String random() {
return "bar";
}
}));
}
// --------------------------------------------------------------------------
// Binder.withSource Tests
// --------------------------------------------------------------------------
@NetworkLibrary
private static class PermittedModule extends AbstractModule {
@Override
protected void configure() {
Method userUnpermittedModuleMethod;
try {
userUnpermittedModuleMethod = UnpermittedModule.class.getMethod("foo");
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
binder()
.withSource(userUnpermittedModuleMethod)
.bind(String.class)
.annotatedWith(Hostname.class)
.toInstance("google.com");
}
}
private static class UnpermittedModule extends AbstractModule {
public String foo() {
return "bar";
}
}
@Test
public void permittedModuleCanWithSourceAnUnpermittedModuleMethod() {
Guice.createInjector(new PermittedModule());
}
@Test
public void unpermittedModuleCantWithSourceAPermittedModule() {
AbstractModule rogueModule =
new AbstractModule() {
@Override
protected void configure() {
binder()
.withSource(PermittedModule.class)
.bindConstant()
.annotatedWith(GatewayIpAdress.class)
.to(0);
}
};
CreationException expected = assertThatInjectorCreationFails(rogueModule);
assertThat(expected).hasMessageThat().contains(BINDING_PERMISSION_ERROR);
assertThat(expected).hasMessageThat().contains(USE_NETWORK_MODULE);
}
@Test
public void getElements_getModule_works() {
Guice.createInjector(Elements.getModule(Elements.getElements(new NetworkModule())));
}
// --------------------------------------------------------------------------
// ModuleAnnotatedMethodScanner tests
// --------------------------------------------------------------------------
@Target(METHOD)
@Retention(RetentionPolicy.RUNTIME)
private @interface NetworkProvides {}
@Qualifier
@RestrictedBindingSource(
explanation = NETWORK_ANNOTATION_IS_RESTRICTED,
permits = {NetworkLibrary.class})
@Retention(RetentionPolicy.RUNTIME)
@interface Network {}
// Adds the NetworkLibrary-owned Network annotation to keys produced by @NetworkProvides methods.
private static class NetworkProvidesScanner extends ModuleAnnotatedMethodScanner {
@Override
public String toString() {
return "NetworkProvidesScanner";
}
@Override
public Set<? extends Class<? extends Annotation>> annotationClasses() {
return ImmutableSet.of(NetworkProvides.class);
}
@Override
public <T> Key<T> prepareMethod(
Binder binder, Annotation annotation, Key<T> key, InjectionPoint injectionPoint) {
return key.withAnnotation(Network.class);
}
}
@Test
public void rogueBindingByMethodScannerDenied() {
AbstractModule rogueModule =
new AbstractModule() {
@NetworkProvides
String provideNetworkString() {
return "lorem ipsum";
}
};
CreationException expected =
assertThatInjectorCreationFails(rogueModule, scannerModule(new NetworkProvidesScanner()));
assertThat(expected).hasMessageThat().contains(BINDING_PERMISSION_ERROR);
assertThat(expected).hasMessageThat().contains(NETWORK_ANNOTATION_IS_RESTRICTED);
}
@Test
public void bindingsAddedByMethodScannerAllowedByNetworkLib() {
@NetworkLibrary
class NetworkModuleWithCustomProvides extends AbstractModule {
@NetworkProvides
String provideNetworkString() {
return "the real network string";
}
}
Guice.createInjector(
new NetworkModuleWithCustomProvides(), scannerModule(new NetworkProvidesScanner()));
}
@Test
public void scannerWithPermitCanCreateRestrictedBinding() {
@NetworkLibrary
class NetworkProvidesScannerWithPermit extends NetworkProvidesScanner {}
AbstractModule moduleWithNetworkProvidesMethod =
new AbstractModule() {
@NetworkProvides
String provideNetworkString() {
return "lorem ipsum";
}
};
Guice.createInjector(
moduleWithNetworkProvidesMethod, scannerModule(new NetworkProvidesScannerWithPermit()));
}
@Test
public void scannerWithPermitCanCreateRestrictedBindings() {
@NetworkLibrary
class NetworkProvidesScannerWithPermit extends NetworkProvidesScanner {
@Override
public <T> Key<T> prepareMethod(
Binder binder, Annotation annotation, Key<T> key, InjectionPoint injectionPoint) {
binder.install(
new AbstractModule() {
@Provides
@GatewayIpAdress
int provideGatewayIp() {
return 42;
}
});
return Key.get(key.getTypeLiteral(), Network.class);
}
}
AbstractModule moduleWithNetworkProvidesMethod =
new AbstractModule() {
@NetworkProvides
String provideNetworkString() {
return "lorem ipsum";
}
};
Injector injector =
Guice.createInjector(
moduleWithNetworkProvidesMethod, scannerModule(new NetworkProvidesScannerWithPermit()));
assertThat(injector.getInstance(Key.get(Integer.class, GatewayIpAdress.class))).isEqualTo(42);
}
@Test
public void moduleInstalledByScannerInheritsMethodModulePermit() {
class NetworkProvidesScannerWithoutPermit extends NetworkProvidesScanner {
@Override
public <T> Key<T> prepareMethod(
Binder binder, Annotation annotation, Key<T> key, InjectionPoint injectionPoint) {
binder.install(
new AbstractModule() {
@Provides
@GatewayIpAdress
int provideGatewayIp() {
return 42;
}
});
return key.withAnnotation(Network.class);
}
}
@NetworkLibrary
class ScannedModuleWithPermit extends AbstractModule {
@NetworkProvides
String provideNetworkString() {
return "lorem ipsum";
}
}
Injector injector =
Guice.createInjector(
new ScannedModuleWithPermit(),
scannerModule(new NetworkProvidesScannerWithoutPermit()));
assertThat(injector.getInstance(Key.get(Integer.class, GatewayIpAdress.class))).isEqualTo(42);
}
private static Module scannerModule(ModuleAnnotatedMethodScanner scanner) {
return new AbstractModule() {
@Override
protected void configure() {
binder().scanModulesForAnnotatedMethods(scanner);
}
};
}
// --------------------------------------------------------------------------
// Modules.override tests
// --------------------------------------------------------------------------
@Test
public void modulesOverrideCantOverrideRestrictedBinding() {
Module rogueModule =
Modules.override(new NetworkModule())
.with(
new AbstractModule() {
@Provides
@GatewayIpAdress
int provideRogueGatewayIp() {
return 12345;
}
});
CreationException expected = assertThatInjectorCreationFails(rogueModule);
assertThat(expected).hasMessageThat().contains(BINDING_PERMISSION_ERROR);
assertThat(expected).hasMessageThat().contains(USE_NETWORK_MODULE);
}
@Test
public void modulesOverrideRestrictedBindingAllowedIfParentHasPermit() {
@NetworkLibrary
class NetworkModuleVersion2 extends AbstractModule {
@Override
protected void configure() {
install(
Modules.override(new NetworkModule())
.with(
new AbstractModule() {
@Provides
@GatewayIpAdress
int provideGatewayIpV2() {
return 2;
}
}));
}
}
assertThat(
Guice.createInjector(new NetworkModuleVersion2())
.getInstance(Key.get(Integer.class, GatewayIpAdress.class)))
.isEqualTo(2);
}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@interface UnrestrictedQualifier {}
@Test
public void modulesOverrideCanOverrideUnrestrictedBinding() {
Module overrideModule =
Modules.override(
new NetworkModule(),
new AbstractModule() {
@Override
protected void configure() {
bindConstant().annotatedWith(UnrestrictedQualifier.class).to("foo");
}
})
.with(
new AbstractModule() {
@Provides
@UnrestrictedQualifier
String provideRogueGatewayIp() {
return "bar";
}
});
assertThat(
Guice.createInjector(overrideModule)
.getInstance(Key.get(String.class, UnrestrictedQualifier.class)))
.isEqualTo("bar");
}
@Test
public void nestedModulesOverrideCanOverrideUnrestrictedBindings() {
Module overrideModule =
Modules.override(
Modules.override(
new NetworkModule(),
new AbstractModule() {
@Override
protected void configure() {
bindConstant().annotatedWith(UnrestrictedQualifier.class).to("foo");
bindConstant().annotatedWith(UnrestrictedQualifier.class).to(42);
}
})
.with(
new AbstractModule() {
@Provides
@UnrestrictedQualifier
String overrideString() {
return "bar";
}
}))
.with(
new AbstractModule() {
@Provides
@UnrestrictedQualifier
int overrideLong() {
return 45;
}
});
Injector injector = Guice.createInjector(overrideModule);
assertThat(injector.getInstance(Key.get(String.class, UnrestrictedQualifier.class)))
.isEqualTo("bar");
assertThat(injector.getInstance(Key.get(Integer.class, UnrestrictedQualifier.class)))
.isEqualTo(45);
}
@Test
public void modulesOverridePrivateModule() {
Guice.createInjector(
Modules.override(
new PrivateModule() {
@Override
protected void configure() {
install(new NetworkModule());
expose(Key.get(String.class, Hostname.class));
}
@Provides
@Exposed
@Named("custom-gateway")
int customGateway(@GatewayIpAdress int gateway) {
return gateway + 4;
}
})
.with(
new AbstractModule() {
@Provides
@Named("custom-gateway")
int provideCustomGatewayOverride() {
return 12345;
}
}));
}
@Test
public void originalElementSourceNotTrustedIfSetExternally() {
ElementSource networkElementSource =
(ElementSource) Elements.getElements(new NetworkModule()).get(0).getSource();
AbstractModule rogueModule =
new AbstractModule() {
@Override
protected void configure() {
binder()
.withSource(networkElementSource)
.bindConstant()
.annotatedWith(GatewayIpAdress.class)
.to(12);
}
};
// Confirm that the original element source was spoofed.
@SuppressWarnings("unchecked")
Binding<Integer> rogueGatewayBinding =
(Binding<Integer>) Elements.getElements(rogueModule).get(0);
assertThat(((ElementSource) rogueGatewayBinding.getSource()).getOriginalElementSource())
.isEqualTo(networkElementSource);
// Will fail because the original element source isn't trusted, becase it wasn't set by Guice
// internals.
CreationException expected = assertThatInjectorCreationFails(rogueModule);
assertThat(expected).hasMessageThat().contains(BINDING_PERMISSION_ERROR);
assertThat(expected).hasMessageThat().contains(USE_NETWORK_MODULE);
}
// --------------------------------------------------------------------------
// PrivateModule tests
// --------------------------------------------------------------------------
@NetworkLibrary
private static class NetworkModuleThatInstalls extends AbstractModule {
final Module module;
NetworkModuleThatInstalls(Module module) {
this.module = module;
}
@Override
protected void configure() {
install(module);
}
}
private static class PrivateModuleCreatesUnexposedNetworkBinding extends PrivateModule {
@Override
protected void configure() {
bindConstant().annotatedWith(GatewayIpAdress.class).to(0);
}
}
@Test
public void parentHasPermit_childPrivateModuleCanBind() {
Guice.createInjector(
new NetworkModuleThatInstalls(
// Allowed because the parent has the @NetworkLibrary permit.
new PrivateModuleCreatesUnexposedNetworkBinding()));
}
@Test
public void noPermitOnStack_privateModuleCantBind() {
AbstractModule rogueModule =
new AbstractModule() {
@Override
protected void configure() {
// Disallowed because there's no permit on the module stack.
install(new PrivateModuleCreatesUnexposedNetworkBinding());
}
};
CreationException expected = assertThatInjectorCreationFails(rogueModule);
assertThat(expected).hasMessageThat().contains(BINDING_PERMISSION_ERROR);
assertThat(expected).hasMessageThat().contains(USE_NETWORK_MODULE);
}
private static class PrivateModuleExposesNetworkBinding extends PrivateModule {
@Override
protected void configure() {
install(
new AbstractModule() {
@Override
protected void configure() {
bindConstant().annotatedWith(GatewayIpAdress.class).to(0);
}
});
expose(Key.get(Integer.class, GatewayIpAdress.class));
}
}
@Test
public void parentHasPermit_childPrivateModuleCanExposeBinding() {
Guice.createInjector(
new NetworkModuleThatInstalls(
// Allowed because the parent has the @NetworkLibrary permit.
new PrivateModuleExposesNetworkBinding()));
}
@Test
public void noPermitOnStack_childPrivateModuleCantExposeBinding() {
AbstractModule rogueModule =
new AbstractModule() {
@Override
protected void configure() {
// Disallowed because there's no permit on the module stack.
install(new PrivateModuleExposesNetworkBinding());
}
};
CreationException expected = assertThatInjectorCreationFails(rogueModule);
assertThat(expected).hasMessageThat().contains(BINDING_PERMISSION_ERROR);
assertThat(expected).hasMessageThat().contains(USE_NETWORK_MODULE);
}
// --------------------------------------------------------------------------
// Child Injector tests
// --------------------------------------------------------------------------
@Test
public void childInjectorCantBindRestrictedBindingWithoutPermit() {
Injector parent = Guice.createInjector(new NetworkModule());
AbstractModule rogueModule =
new AbstractModule() {
@Provides
RoutingTable provideRoutingTable() {
return destinationIp -> 0;
}
};
CreationException expected =
assertThrows(CreationException.class, () -> parent.createChildInjector(rogueModule));
assertThat(expected).hasMessageThat().contains(BINDING_PERMISSION_ERROR);
assertThat(expected).hasMessageThat().contains(USE_ROUTING_MODULE);
}
@Test
public void childInjectorCanBindRestrictedBindingWithPermit() {
Injector parent = Guice.createInjector(new NetworkModule());
parent.createChildInjector(new RoutingModule());
}
CreationException assertThatInjectorCreationFails(Module... modules) {
return assertThrows(CreationException.class, () -> Guice.createInjector(modules));
}
}