Add SP MockRunner interface class, code an example of its use, improve comments
[java-idp.git] / tests / edu / internet2 / middleware / shibboleth / runner / ShibbolethRunner.java
1 /*
2  * Copyright [2005] [University Corporation for Advanced Internet Development, Inc.]
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package edu.internet2.middleware.shibboleth.runner;
18
19 import javax.naming.directory.Attributes;
20 import javax.naming.directory.BasicAttributes;
21
22 import org.apache.log4j.ConsoleAppender;
23 import org.apache.log4j.Layout;
24 import org.apache.log4j.Level;
25 import org.apache.log4j.Logger;
26 import org.apache.log4j.PatternLayout;
27 import org.opensaml.SAMLBinding;
28 import org.opensaml.SAMLConfig;
29
30 import com.mockrunner.mock.web.MockFilterConfig;
31 import com.mockrunner.mock.web.MockHttpServletRequest;
32 import com.mockrunner.mock.web.MockHttpServletResponse;
33 import com.mockrunner.mock.web.MockServletContext;
34 import com.mockrunner.mock.web.WebMockObjectFactory;
35 import com.mockrunner.servlet.ServletTestModule;
36
37 import edu.internet2.middleware.shibboleth.common.ShibbolethConfigurationException;
38 import edu.internet2.middleware.shibboleth.idp.IdPResponder;
39 import edu.internet2.middleware.shibboleth.resource.AuthenticationFilter;
40 import edu.internet2.middleware.shibboleth.serviceprovider.AssertionConsumerServlet;
41 import edu.internet2.middleware.shibboleth.serviceprovider.FilterSupportImpl;
42 import edu.internet2.middleware.shibboleth.serviceprovider.ServiceProviderConfig;
43 import edu.internet2.middleware.shibboleth.serviceprovider.ServiceProviderContext;
44
45 /**
46  * Initialize on request the IdP, SP, and Filter on behalf of a JUnit test.
47  * 
48  * <p>An instance of this class is created by a JUnit test class, which
49  * uses it to initialize and create MockRunner objects for testing the
50  * Shibboleth components. This keeps the tests themselves simple.</p>
51  * 
52  * <p>Look at *.integration.IntegrationTest for an example of use.</p>
53  * 
54  * @author Howard Gilbert.
55  */
56 public class ShibbolethRunner {
57     
58     
59     public static int SERVER_PORT = 443;
60     public static int RESOURCE_SERVER_PORT = 9443;
61     public static String IDP_SERVER_NAME = "idp.example.org";
62     public static String SP_SERVER_NAME = "sp.example.org";
63     public static String SCHEME = "https";
64     public static String PROTOCOL = "HTTP/1.1";
65     public static String IDP_CONTEXT_PATH = "/shibboleth-idp";
66     public static String SP_CONTEXT_PATH = "/shibboleth-sp";
67     public static String RESOURCE_CONTEXT_PATH = "/secure";
68     public static String REMOTE_ADDR = "127.0.0.1";
69     public static String IDP_CONTEXT_URL = SCHEME+"://"+IDP_SERVER_NAME+IDP_CONTEXT_PATH+"/";
70     public static String SP_CONTEXT_URL = SCHEME+"://"+SP_SERVER_NAME+SP_CONTEXT_PATH+"/";
71     public static String RESOURCE_CONTEXT_URL = SCHEME+"://"+SP_SERVER_NAME+":"+RESOURCE_SERVER_PORT+RESOURCE_CONTEXT_PATH+"/";
72     
73     
74     private static SAMLConfig samlConfig; 
75
76     
77     
78     /**
79      * Initialization logic goes here.
80      * <p>Reqires that Log4J already be configured.</p>
81      */
82     public ShibbolethRunner() {
83         
84         // Configure SAML to use the MockRunner interface to callback
85         // from the SP to the IdP instead of trying to use real HTTP.
86         samlConfig = SAMLConfig.instance();
87         samlConfig.setDefaultBindingProvider(SAMLBinding.SOAP,"edu.internet2.middleware.shibboleth.runner.MockHTTPBindingProvider" );
88     }
89  
90     
91     
92     /*
93      * Logging
94      * 
95      * For automated test cases that normally just work, you 
96      * probably want to leave the logging level to ERROR. However,
97      * if you are running a custom test case to discover the source
98      * of a problem, or when building a new test case, then you 
99      * may want to set the logging level to DEBUG.
100      * 
101      * You can change the loglevel variable from the test case
102      * code before calling setupLogging(). 
103      */
104     public static Level loglevel = Level.INFO;
105     
106     private static Logger clientLogger = Logger.getLogger("edu.internet2.middleware.shibboleth");
107     private static Logger initLogger = Logger.getLogger("shibboleth.init");
108     private static Logger samlLogger = Logger.getLogger("org.opensaml");
109     private static boolean manageLogs = false;
110     
111     /**
112      * You will almost always call setupLogging first, but it
113      * it not automatic in case you have exotic logging 
114      * requirements.
115      * 
116      * <p>Restriction: avoid any static initialization that generates
117      * log messages because this method can only be called after 
118      * static initialation.</p>
119      */
120     public static void setupLogging() {
121         manageLogs = true;
122         Logger root = Logger.getRootLogger();
123         Layout initLayout = new PatternLayout("%d{HH:mm} %-5p %m%n");
124         ConsoleAppender consoleAppender= new ConsoleAppender(initLayout,ConsoleAppender.SYSTEM_OUT);
125         root.removeAllAppenders();
126         root.addAppender(consoleAppender);
127         root.setLevel(Level.ERROR);
128         clientLogger.removeAllAppenders();
129         clientLogger.setLevel(loglevel);
130         initLogger.removeAllAppenders();
131         initLogger.setLevel(loglevel);
132         samlLogger.removeAllAppenders();
133         samlLogger.setLevel(loglevel);
134     }
135     
136     /**
137      * Sometimes (as in IdP initialization) the logging levels
138      * get reset to some unintended level. This resets them
139      * to whatever we want for testing.
140      */
141     public static void resetLoggingLevels() {
142         if (!manageLogs) return;  // If setupLogging was never called.
143         clientLogger.removeAllAppenders();
144         clientLogger.setLevel(loglevel);
145         initLogger.removeAllAppenders();
146         initLogger.setLevel(loglevel);
147         samlLogger.removeAllAppenders();
148         samlLogger.setLevel(loglevel);
149         
150     }
151     
152     
153     
154     /*
155      * The SP is represented by an SPContext object and the objects
156      * SessionManager, SPConfig, etc. chained off it. The context
157      * is initialized and then the main configuration file is read
158      * in to create the Config object.
159      * 
160      * The testing environment doesn't bother with MockRunner objects.
161      * The Servlet interface to the SP is a thin layer that only 
162      * translates between HTTP/HTML (the Request object) and method
163      * calls. So once initialized, it is just as easy to call the
164      * SessionManager and FilterSupportImpl directly.
165      */
166     
167     private String spConfigFileName = "/basicSpHome/spconfig.xml";
168     /**
169      * If you are goint to change the SP Config File
170      * do it before calling initServiceProvider.
171      * 
172      * @param spConfigFileName
173      */
174     public void setSpConfigFileName(String spConfigFileName) {
175         this.spConfigFileName = spConfigFileName;
176     }
177     
178     private static boolean SPinitialized = false; // don't do it twice
179     public static SPTestContext consumer = null;
180     
181     /**
182      * Load an SP configuration file if you are not using SPTestContext.
183      * 
184      * <p>Calling this routine does not create a MockRunner context
185      * to call the AssertionConsumerServlet. However, you can still
186      * create sessions by directly calling SessionManager.</p>
187      * 
188      * @throws ShibbolethConfigurationException  if bad config file
189      */
190     public void initializeSP() 
191         throws ShibbolethConfigurationException{
192         if (SPinitialized) return;
193         SPinitialized=true;
194         
195         ServiceProviderContext context = ServiceProviderContext.getInstance();
196         context.initialize();
197         
198         ServiceProviderConfig config = new ServiceProviderConfig();
199         context.setServiceProviderConfig(config);
200         config.loadConfigObjects(spConfigFileName);
201         
202         // Plug an instance of FilterSupportImpl into the Filter
203         FilterSupportImpl service = new FilterSupportImpl();
204         AuthenticationFilter.setFilterSupport(service);
205         
206     }
207     
208     
209     /**
210      * A MockRunner context for the AssertionConsumerServlet.
211      */
212     public class SPTestContext {
213
214         // The Factory creates the Request, Response, Session, etc.
215         public WebMockObjectFactory factory = new WebMockObjectFactory();
216         
217         // The TestModule runs the Servlet and Filter methods in the simulated container
218         public ServletTestModule testModule = new ServletTestModule(factory);
219         
220         // Now simulated Servlet API objects
221         public MockServletContext servletContext= factory.getMockServletContext();
222         public MockFilterConfig filterConfig= factory.getMockFilterConfig();
223         public MockHttpServletResponse response = factory.getMockResponse();
224         public MockHttpServletRequest request = factory.getMockRequest();
225         
226         
227         // The Servlet object is created by Mockrunner
228         public AssertionConsumerServlet spServlet;
229         
230         
231         /**
232          * Construct the related objects
233          */
234         public SPTestContext() {
235             
236             // ServletContext
237             servletContext.setServletContextName("dummy SP Context");
238             servletContext.setInitParameter("ServiceProviderConfigFile", spConfigFileName);
239             
240             
241             // Create instance of Filter class, add to chain, call its init()
242             // NOTE: The SP reads its configuration file and initializes
243             // itself within this call.
244             spServlet = (AssertionConsumerServlet) testModule.createServlet(AssertionConsumerServlet.class);
245             SPinitialized=true;
246             
247             // Unchanging properties of the HttpServletRequest
248             request.setRemoteAddr(REMOTE_ADDR);
249             request.setContextPath(SP_CONTEXT_PATH);
250             request.setProtocol(PROTOCOL);
251             request.setScheme(SCHEME);
252             request.setServerName(SP_SERVER_NAME);
253             request.setServerPort(SERVER_PORT);
254             
255         }
256         
257         /**
258          * Set the fields of the request that depend on a suffix,
259          */
260         public void setRequestUrls(String suffix) {
261             request.setRequestURI(SP_CONTEXT_URL+suffix);
262             request.setRequestURL(SP_CONTEXT_URL+suffix);
263             request.setServletPath(SP_CONTEXT_PATH+"/"+suffix);
264         }
265         
266     }
267
268     
269     
270     
271     
272     
273     /*
274      * Setup the IdP interface object
275      * 
276      * The IdP keeps its "context" of cached data and configured 
277      * objects internal rather than exposing it as a public object.
278      * The IdpTestContext object does the initialization and creates
279      * a set of MockRunner object through which the SSO, AA, and 
280      * Artifact requests can be generated.
281      * 
282      * The real IdP objects configure themselves when the Servlet
283      * init() method is called. The Configuration file name coded
284      * here is passed to the Servlet as a simulated context parameter.
285      * 
286      * To direct the AA and Artifact queries back to the Idp object,
287      * a call to SAML sets up the MockHTTPBindingProvider to replace
288      * the normal HTTPBindingProvider. Thus instead of creating URL
289      * and sockets to talk to the IdP, a simulated Request object is
290      * configured and the IdP is called through MockRunner.
291      */
292     public String idpConfigFileName = "/basicIdpHome/idpconfig.xml";
293     public void setIdpConfigFileName(String idpConfigFileName) {
294         this.idpConfigFileName = idpConfigFileName;
295     } 
296     
297     public static IdpTestContext idp = null;
298     
299     /**
300      * Initializes the IdP if necessary, then returns a 
301      * pointer to the MockRunner interface object
302      * @return IdpTestContext with Mockrunner objects
303      */
304     public IdpTestContext getIdp() {
305         if (idp==null) {
306             idp = new IdpTestContext();
307         }
308         return idp;
309     }
310     
311     
312     /**
313      * Establish initialized IdP and a set of MockRunner objects to
314      * process SSO, AA, and Artifact requests.
315      * 
316      * <p>The IdP is initialized when the IdpResponder servlet init() is 
317      * called. This establishes the static context of tables that 
318      * allow the IdP to issue a Subject and then respond when that
319      * Subject is returned in an Attribute Query.</p>
320      * 
321      * <p>This class creates the Mockrunner control blocks needed to 
322      * call the IdP and, by creating the IdP Servlet object, also 
323      * initializes an instance of the IdP. It depends on a configuration
324      * file located as a resource in the classpath, typically in the 
325      * /testresources directory of the project.</p>
326      */
327     public class IdpTestContext {
328         
329         
330
331         // The Factory creates the Request, Response, Session, etc.
332         public WebMockObjectFactory factory = new WebMockObjectFactory();
333         
334         // The TestModule runs the Servlet and Filter methods in the simulated container
335         public ServletTestModule testModule = new ServletTestModule(factory);
336         
337         // Now simulated Servlet API objects
338         public MockServletContext servletContext= factory.getMockServletContext();
339         public MockFilterConfig filterConfig= factory.getMockFilterConfig();
340         public MockHttpServletResponse response = factory.getMockResponse();
341         public MockHttpServletRequest request = factory.getMockRequest();
342         
343         
344         // The IdP Servlet that processes SSO, AA, and Artifact requests
345         // The object is created by Mockrunner
346         public IdPResponder idpServlet;
347         
348         /**
349          * Construct with the default configuration file
350          */
351         public IdpTestContext() {
352             this(idpConfigFileName);
353         }
354         
355         /**
356          * Construct using a specified IdP configuration file.
357          */
358         public IdpTestContext(String configFileName) {
359             
360             // ServletContext
361             servletContext.setServletContextName("dummy IdP Context");
362             servletContext.setInitParameter("IdPConfigFile", configFileName);
363             
364             
365             // Create instance of Filter class, add to chain, call its init()
366             // NOTE: The IdP reads its configuration file and initializes
367             // itself within this call.
368             idpServlet = (IdPResponder) testModule.createServlet(IdPResponder.class);
369             resetLoggingLevels();
370             
371             // Unchanging properties of the HttpServletRequest
372             request.setRemoteAddr(REMOTE_ADDR);
373             request.setContextPath(IDP_CONTEXT_PATH);
374             request.setProtocol(PROTOCOL);
375             request.setScheme(SCHEME);
376             request.setServerName(IDP_SERVER_NAME);
377             request.setServerPort(SERVER_PORT);
378             
379         }
380         
381         /**
382          * Set the fields of the request that depend on a suffix,
383          * normally SSO, AA, or Artifact
384          */
385         public void setRequestUrls(String suffix) {
386             request.setRequestURI(IDP_CONTEXT_URL+suffix);
387             request.setRequestURL(IDP_CONTEXT_URL+suffix);
388             request.setServletPath(IDP_CONTEXT_PATH+"/"+suffix);
389         }
390         
391     }
392
393     
394     
395     
396     
397     /*
398      * Here we keep a static reference to a Collection of Attributes. 
399      * 
400      * The Test can clear the collection and add attributes. When
401      * the IdP needs attributes, it treats this collection as the 
402      * starting point and processes them through ARP. When then get
403      * to the SP they go through AAP. So you can test the Attribute
404      * processing logic in both components by creating Attributes 
405      * with names and values that are accepted or rejected.
406      */
407     
408     public static BasicAttributes attributes = new BasicAttributes();
409     
410     /**
411      * The Test should obtain a reference to the Attribute collection and add
412      * such attributes as it wants the IdP to return for a Principal.
413      * @return Attributes collection
414      */
415     public Attributes getAttributesCollection() {
416         return attributes;
417     }
418     
419     
420     
421     /*
422      * The Filter depends on a Servlet environment simulated by MockRunner.
423      * We give it its own set of MockRunner blocks because in real life
424      * it runs in a separate context from the SP or IdP.
425      * 
426      * The Filter depends on the SP and, once initialized, has a reference
427      * to FilterSupportImpl and through it the SP configuration and Sessions.
428      */
429     private AuthenticationFilterContext filter;
430     public AuthenticationFilterContext getFilter() throws ShibbolethConfigurationException {
431         if (filter==null)
432             filter=new AuthenticationFilterContext();
433         return filter;
434     }
435     
436     /**
437      * Create the MockRunning interface for running the ServletFilter.
438      * 
439      * <p>The SP must be initialized to provide parameters.</p>
440      *
441      */
442     public class AuthenticationFilterContext {
443         
444
445         // The Factory creates the Request, Response, Session, etc.
446         public WebMockObjectFactory factory = new WebMockObjectFactory();
447         
448         // The TestModule runs the Servlet and Filter methods in the simulated container
449         public ServletTestModule testModule = new ServletTestModule(factory);
450         
451         // Now simulated Servlet API objects
452         public MockServletContext servletContext= new MockServletContext();
453         public MockFilterConfig filterConfig= factory.getMockFilterConfig();
454         public MockHttpServletResponse response = factory.getMockResponse();
455         public MockHttpServletRequest request = factory.getMockRequest();
456         
457         /*
458          * The Missing Manual: There are three types of init-params in
459          * the web.xml. One applies to the Context as a whole. The other
460          * two are nested inside a <servlet> or <filter> and provide
461          * parameters specific to that particular object. If you do
462          * a factory.getMockServletContext() you get an object that corresponds
463          * to the web.xml configuration itself. However, rather than adding
464          * init-param collections to the MockServletConfig and MockFilterConfig,
465          * Mockrunner seems to chain a user-created MockServletContext object
466          * to them and use its init-params as the parameters fed back to the
467          * Filter or Servlet object. So when you see "new MockServletContext()"
468          * there is a pretty good reason to expect this will not be used as a
469          * real ServletContext but rather as a secondary control block to a 
470          * MockFilterConfig or MockServletConfig.
471          */
472         
473         // Filter objects
474         private AuthenticationFilter filter;
475         
476        public AuthenticationFilterContext() {
477             
478             // ServletContext (argument to Filters and Servlets)
479             servletContext.setServletContextName("dummy Servlet Context");
480             servletContext.setInitParameter("requireId", ".+/test.+");
481             
482             // The FilterConfig (argument to Filter init)
483             filterConfig.setupServletContext(servletContext);
484             filterConfig.setFilterName("Test Filter under JUnit");
485        }
486        
487        /**
488         * Call after any changes to Context init-param values to
489         * initialize the filter object and connect to the SP.
490         * 
491         * @throws ShibbolethConfigurationException from SP init.
492         */
493        public void setUp() throws ShibbolethConfigurationException {
494             
495             // Create instance of Filter class, add to chain, call its init()
496             filter = (AuthenticationFilter) testModule.createFilter(AuthenticationFilter.class);
497             
498             // Note: if the SP is already initialized, this noops.
499             initializeSP();
500             
501
502             request.setRemoteAddr(REMOTE_ADDR);
503             request.setContextPath(RESOURCE_CONTEXT_PATH);
504             request.setProtocol(PROTOCOL);
505             request.setScheme(SCHEME);
506             request.setServerName(SP_SERVER_NAME);
507             request.setServerPort(RESOURCE_SERVER_PORT);
508         }
509         
510         public void setRequestUrls(String suffix) {
511             request.setMethod("GET");
512             request.setRequestURI(RESOURCE_CONTEXT_URL+suffix);
513             request.setRequestURL(RESOURCE_CONTEXT_URL+suffix);
514             request.setServletPath(RESOURCE_CONTEXT_PATH+"/"+suffix);
515             
516         }
517     }
518     
519 }