Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Injected Test: Require a Stapler verb annotation on web method looking methods #133

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,11 @@ THE SOFTWARE.
<version>1.0.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.github.classgraph</groupId>
<artifactId>classgraph</artifactId>
<version>4.4.7</version>
</dependency>
</dependencies>

<build>
Expand Down
107 changes: 105 additions & 2 deletions src/main/java/org/jvnet/hudson/test/PluginAutomaticTestBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,31 @@
import hudson.PluginManager.FailedPlugin;
import hudson.PluginWrapper;
import hudson.cli.CLICommand;
import java.io.File;
import java.util.Map;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ClassInfo;
import io.github.classgraph.ScanResult;
import jenkins.model.Jenkins;
import junit.framework.TestCase;
import junit.framework.TestSuite;
import org.apache.commons.lang3.StringUtils;
import org.junit.Assert;
import org.kohsuke.stapler.WebMethod;
import org.kohsuke.stapler.interceptor.InterceptorAnnotation;
import org.kohsuke.stapler.interceptor.RequirePOST;
import org.kohsuke.stapler.verb.HttpVerbInterceptor;

import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.logging.Logger;

import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.empty;

/**
* Called by the code generated by maven-hpi-plugin to build tests for plugins.
Expand Down Expand Up @@ -65,6 +84,7 @@ public static TestSuite build(Map<String,?> params) throws Exception {
String packaging = StringUtils.defaultIfBlank((String)params.get("packaging"), "hpi");
if ("hpi".equals(packaging)) {
inJenkins.addTest(new OtherTests("testPluginActive", params));
inJenkins.addTest(new OtherTests("testStaplerDispatches", params));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather other things did not go into InjectedTest (and that it would die). The reason being is that skipping part of the tests (rather than all of them) is pretty much impossible as it uses old junit Suites rather than newer test classes, and is hard to debug.
This would be much better going forward in a generate-tests mojo in maven-hpi-plugin which then also makes it much easier to debug in an IDE. but otherwise 👍

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

skipping part of the tests (rather than all of them) is pretty much impossible

Well, you should not be skipping any of them. :-)

I am not sure how creating JUnit 4-style tests would make it easier to ignore some test cases; target/generated-test-sources/whatever/InjectedTest.java would still not be editable.

Note that there is a reason all the tests go into one suite: so that only one Jenkins startup is needed, to minimize overhead.

At any rate, InjectedTest is a well-established part of Jenkins plugin development. Proposals to replace its structure should be kept separate. So long as this PR does not introduce a test which is going to fail widely and spuriously (which I have not yet reviewed), it should be fine IMO.

}
master.addTest(inJenkins);
master.addTest(new PropertiesTestSuite(outputDirectory));
Expand Down Expand Up @@ -106,5 +126,88 @@ public void testPluginActive() {
}
}

public void testStaplerDispatches() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
List<String> methodsFound = new ArrayList<>();

Method isStaplerRoutableMethod = findIsRoutableMethod();
if (isStaplerRoutableMethod == null) {
return;
}

PluginWrapper thisPlugin = determineCurrentPlugin();
if (thisPlugin == null) {
return; // we're not in a plugin
}
ClassGraph classGraph = new ClassGraph();
classGraph.addClassLoader(thisPlugin.classLoader);
classGraph.disableJarScanning();
classGraph.enableClassInfo();
ScanResult result = classGraph.scan();

for (ClassInfo classInfo : result.getAllClasses()) {
Class clazz = classInfo.loadClass();
for (Method m : clazz.getDeclaredMethods()) {
if (isStaplerDispatchable(m) && (boolean)isStaplerRoutableMethod.invoke(null, m)) {
if (!hasStaplerVerbAnnotation(m)) {
methodsFound.add(clazz.getName() + "#" + m.getName());
}
}
}
}
Assert.assertThat("There should be no web methods that lack HTTP verb annotations like @RequirePOST, @GET, @POST, etc. -- see https://jenkins.io/redirect/developer/csrf-protection", methodsFound, is(empty()));
}

private Method findIsRoutableMethod() throws NoSuchMethodException {
try {
Method method = Class.forName("jenkins.security.stapler.TypedFilter").getDeclaredMethod("isRoutableMethod", Method.class);
method.setAccessible(true);
return method;
} catch (ClassNotFoundException e) {
LOGGER.warning("This test requires Jenkins 2.154, Jenkins LTS 2.138.4, or newer to run, use e.g. -Djenkins.version=2.138.4");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use of assume from JUnit might be appropriate here for better integration with error reporting.

// TODO add a fallback implementing similar code directly here
return null;
}
}

private PluginWrapper determineCurrentPlugin() {
String plugin = (String) params.get("artifactId");
if (plugin != null) {
return Jenkins.getActiveInstance().pluginManager.getPlugin(plugin);
}
return null;
}

private boolean hasStaplerVerbAnnotation(Method m) {
if (m.getAnnotation(RequirePOST.class) != null) {
return true;
}
for (Annotation annotation : m.getAnnotations()) {
InterceptorAnnotation interceptorAnnotation = annotation.annotationType().getAnnotation(InterceptorAnnotation.class);
if (interceptorAnnotation != null && interceptorAnnotation.value() == HttpVerbInterceptor.class) {
// see org.kohsuke.stapler.verb.HttpVerbInterceptor
return true;
}
}
return false;
}

private boolean isStaplerDispatchable(Method m) {
if (m.isBridge()) {
return false;
}
if (m.isSynthetic()) {
return false;
}
if (!Modifier.isPublic(m.getModifiers())) {
return false;
}
String name = m.getName();
if (!name.startsWith("do") && m.getAnnotation(WebMethod.class) == null) {
return false;
}
return true;
}

private static Logger LOGGER = Logger.getLogger(OtherTests.class.getName());
}
}
1 change: 1 addition & 0 deletions src/test/java/org/jvnet/hudson/test/JenkinsRuleTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ public void assertEqualDataBoundBeansWithSettersFailForField() throws Exception
@Test
public void testTokenHelperMethods() throws Exception {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
// Jenkins 2.129+: ApiTokenPropertyConfiguration.get().setTokenGenerationOnCreationEnabled(true);

JenkinsRule.WebClient wc = j.createWebClient();

Expand Down