changeset 1080:dd39c8d202fb

Add more tests for WebStorageEndPoint. Reviewed-by: neugens Review-thread: http://icedtea.classpath.org/pipermail/thermostat/2013-April/006418.html PR952
author Severin Gehwolf <sgehwolf@redhat.com>
date Tue, 23 Apr 2013 11:44:28 +0200
parents dff087c3c875
children 17aed647ccff
files web/server/src/main/java/com/redhat/thermostat/web/server/WebStorageEndPoint.java web/server/src/main/java/com/redhat/thermostat/web/server/auth/WebStoragePathHandler.java web/server/src/test/java/com/redhat/thermostat/web/server/WebStorageEndpointTest.java
diffstat 3 files changed, 376 insertions(+), 43 deletions(-) [+]
line wrap: on
line diff
--- a/web/server/src/main/java/com/redhat/thermostat/web/server/WebStorageEndPoint.java	Fri Apr 19 14:09:10 2013 +0200
+++ b/web/server/src/main/java/com/redhat/thermostat/web/server/WebStorageEndPoint.java	Tue Apr 23 11:44:28 2013 +0200
@@ -88,6 +88,7 @@
 import com.redhat.thermostat.web.common.WebRemove;
 import com.redhat.thermostat.web.common.WebUpdate;
 import com.redhat.thermostat.web.server.auth.Roles;
+import com.redhat.thermostat.web.server.auth.WebStoragePathHandler;
 
 @SuppressWarnings("serial")
 public class WebStorageEndPoint extends HttpServlet {
@@ -215,6 +216,7 @@
         }
     }
 
+    @WebStoragePathHandler( path = "ping" )
     private void ping(HttpServletRequest req, HttpServletResponse resp) {
         if (! isAuthorized(req, resp, Roles.LOGIN)) {
             return;
@@ -223,6 +225,7 @@
         resp.setStatus(HttpServletResponse.SC_OK);
     }
 
+    @WebStoragePathHandler( path = "purge" )
     private void purge(HttpServletRequest req, HttpServletResponse resp) {
         if (! isAuthorized(req, resp, Roles.PURGE)) {
             return;
@@ -233,6 +236,7 @@
         resp.setStatus(HttpServletResponse.SC_OK);
     }
 
+    @WebStoragePathHandler( path = "load-file" )
     private void loadFile(HttpServletRequest req, HttpServletResponse resp) throws IOException {
         if (! isAuthorized(req, resp, Roles.LOAD_FILE)) {
             return;
@@ -252,6 +256,7 @@
         }
     }
 
+    @WebStoragePathHandler( path = "save-file" )
     private void saveFile(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
         if (! isAuthorized(req, resp, Roles.SAVE_FILE)) {
             return;
@@ -280,6 +285,7 @@
         
     }
 
+    @WebStoragePathHandler( path = "get-count" )
     private void getCount(HttpServletRequest req, HttpServletResponse resp) {
         if (! isAuthorized(req, resp, Roles.GET_COUNT)) {
             return;
@@ -299,6 +305,7 @@
         }
     }
 
+    @WebStoragePathHandler( path = "register-category" )
     private synchronized void registerCategory(HttpServletRequest req, HttpServletResponse resp) throws IOException {
         if (! isAuthorized(req, resp, Roles.REGISTER_CATEGORY)) {
             return;
@@ -326,6 +333,7 @@
         writer.flush();
     }
 
+    @WebStoragePathHandler( path = "put-pojo" )
     private void putPojo(HttpServletRequest req, HttpServletResponse resp) {
         String insertParam = req.getParameter("insert");
         WebInsert insert = gson.fromJson(insertParam, WebInsert.class);
@@ -351,6 +359,7 @@
         resp.setStatus(HttpServletResponse.SC_OK);
     }
 
+    @WebStoragePathHandler( path = "remove-pojo" )
     @SuppressWarnings({ "rawtypes", "unchecked" })
     private void removePojo(HttpServletRequest req, HttpServletResponse resp) {
         if (! isAuthorized(req, resp, Roles.DELETE)) {
@@ -370,6 +379,7 @@
         resp.setStatus(HttpServletResponse.SC_OK);
     }
 
+    @WebStoragePathHandler( path = "update-pojo" )
     @SuppressWarnings({ "rawtypes", "unchecked" })
     private void updatePojo(HttpServletRequest req, HttpServletResponse resp) {
         if (! isAuthorized(req, resp, Roles.UPDATE)) {
@@ -410,6 +420,7 @@
         }
     }
 
+    @WebStoragePathHandler( path = "find-all" )
     @SuppressWarnings({ "rawtypes", "unchecked" })
     private void findAll(HttpServletRequest req, HttpServletResponse resp) throws IOException {
         if (! isAuthorized(req, resp, Roles.READ)) {
@@ -455,6 +466,7 @@
         resp.flushBuffer();
     }
 
+    @WebStoragePathHandler( path = "generate-token" )
     private void generateToken(HttpServletRequest req, HttpServletResponse resp) throws IOException {
         if (! isAuthorized(req, resp, Roles.CMD_CHANNEL_GENERATE) ) {
             return;
@@ -468,6 +480,7 @@
         resp.getOutputStream().write(token);
     }
 
+    @WebStoragePathHandler( path = "verify-token" )
     private void verifyToken(HttpServletRequest req, HttpServletResponse resp) {
         if (! isAuthorized(req, resp, Roles.CMD_CHANNEL_VERIFY) ) {
             return;
@@ -488,6 +501,7 @@
         if (req.isUserInRole(role)) {
             return true;
         } else {
+            logger.log(Level.FINEST, "Not permitting access to " + req.getPathInfo() + ". User '" + req.getRemoteUser() + "' not in role " + role);
             resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
             return false;
         }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/server/src/main/java/com/redhat/thermostat/web/server/auth/WebStoragePathHandler.java	Tue Apr 23 11:44:28 2013 +0200
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.web.server.auth;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marker annotation for web storage handler methods. This is used in order
+ * to ensure proper authorization coverage.
+ *
+ */
+@Target( ElementType.METHOD )
+@Retention( RetentionPolicy.RUNTIME )
+public @interface WebStoragePathHandler {
+    
+    String path();
+    
+}
--- a/web/server/src/test/java/com/redhat/thermostat/web/server/WebStorageEndpointTest.java	Fri Apr 19 14:09:10 2013 +0200
+++ b/web/server/src/test/java/com/redhat/thermostat/web/server/WebStorageEndpointTest.java	Tue Apr 23 11:44:28 2013 +0200
@@ -37,6 +37,7 @@
 package com.redhat.thermostat.web.server;
 
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
 import static org.junit.Assert.fail;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.eq;
@@ -52,12 +53,17 @@
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.Reader;
+import java.lang.reflect.Method;
 import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.ProtocolException;
 import java.net.URL;
 import java.net.URLEncoder;
 import java.util.HashMap;
 import java.util.Map;
 
+import javax.servlet.http.HttpServletResponse;
+
 import org.apache.commons.codec.binary.Base64;
 import org.eclipse.jetty.security.DefaultUserIdentity;
 import org.eclipse.jetty.security.LoginService;
@@ -74,6 +80,7 @@
 import org.mockito.ArgumentCaptor;
 
 import com.google.gson.Gson;
+import com.redhat.thermostat.storage.core.Add;
 import com.redhat.thermostat.storage.core.Categories;
 import com.redhat.thermostat.storage.core.Category;
 import com.redhat.thermostat.storage.core.Cursor;
@@ -96,6 +103,7 @@
 import com.redhat.thermostat.web.common.WebRemove;
 import com.redhat.thermostat.web.common.WebUpdate;
 import com.redhat.thermostat.web.server.auth.Roles;
+import com.redhat.thermostat.web.server.auth.WebStoragePathHandler;
 
 public class WebStorageEndpointTest {
 
@@ -175,10 +183,69 @@
 
     @After
     public void tearDown() throws Exception {
-        server.stop();
-        server.join();
+        // some tests don't use server
+        if (server != null) {
+            server.stop();
+            server.join();
+        }
     }
 
+    /**
+     * Makes sure that all paths we dispatch to, dispatch to
+     * {@link WebStoragePathHandler} annotated methods.
+     * 
+     * @throws Exception
+     */
+    @Test
+    public void ensureAuthorizationCovered() throws Exception {
+        // manually maintained list of path handlers which should include
+        // authorization checks
+        final String[] authPaths = new String[] {
+                "find-all", "put-pojo", "register-category", "remove-pojo",
+                "update-pojo", "get-count", "save-file", "load-file",
+                "purge", "ping", "generate-token", "verify-token"
+        };
+        Map<String, Boolean> checkedAutPaths = new HashMap<>();
+        for (String path: authPaths) {
+            checkedAutPaths.put(path, false);
+        }
+        int methodsReqAuthorization = 0;
+        for (Method method: WebStorageEndPoint.class.getDeclaredMethods()) {
+            if (method.isAnnotationPresent(WebStoragePathHandler.class)) {
+                methodsReqAuthorization++;
+                WebStoragePathHandler annot = method.getAnnotation(WebStoragePathHandler.class);
+                try {
+                    // this may NPE if there is something funny going on in
+                    // WebStorageEndPoint (e.g. one method annotated but this
+                    // reference list has not been updated).
+                    if (!checkedAutPaths.get(annot.path())) {
+                        // mark path as covered
+                        checkedAutPaths.put(annot.path(), true);
+                    } else {
+                        throw new AssertionError(
+                                "method "
+                                        + method
+                                        + " annotated as web storage path handler (path '"
+                                        + annot.path()
+                                        + "'), but not in reference list we know about!");
+                    }
+                } catch (NullPointerException e) {
+                    throw new AssertionError("Don't know about path '"
+                            + annot.path() + "'");
+                }
+            }
+        }
+        // at this point we should have all dispatched paths covered
+        for (String path: authPaths) {
+            assertTrue(
+                    "Is " + path
+                          + " marked with @WebStoragePathHandler and have proper authorization checks been included?",
+                    checkedAutPaths.get(path));
+        }
+        assertEquals(authPaths.length, methodsReqAuthorization);
+    }
+
+    @SuppressWarnings({ "unchecked", "rawtypes" })
     @Test
     public void authorizedFindAllPojos() throws Exception {
         String[] roleNames = new String[] {
@@ -203,7 +270,6 @@
         TestClass expected2 = new TestClass();
         expected2.setKey1("fluff2");
         expected2.setKey2(43);
-        @SuppressWarnings("unchecked")
         Cursor<TestClass> cursor = mock(Cursor.class);
         when(cursor.hasNext()).thenReturn(true).thenReturn(true).thenReturn(false);
         when(cursor.next()).thenReturn(expected1).thenReturn(expected2);
@@ -216,7 +282,7 @@
         URL url = new URL(endpoint + "/find-all");
         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
         conn.setRequestMethod("POST");
-        sendAuthorization(conn, testuser, password);
+        sendAuthentication(conn, testuser, password);
         conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
         conn.setDoInput(true);
         conn.setDoOutput(true);
@@ -248,6 +314,46 @@
         verify(mockQuery).execute();
         verifyNoMoreInteractions(mockQuery);
     }
+    
+    @Test
+    public void unauthorizedFindAllPojos() throws Exception {
+        String failMsg = "thermostat-read role missing, expected Forbidden!";
+        doUnauthorizedTest("find-all", failMsg);
+    }
+    
+    private void doUnauthorizedTest(String pathForEndPoint, String failMessage) throws Exception {
+        String[] insufficientRoleNames = new String[] {
+                Roles.REGISTER_CATEGORY,
+        };
+        doUnauthorizedTest(pathForEndPoint, failMessage, insufficientRoleNames, true);
+    }
+
+    private void doUnauthorizedTest(String pathForEndPoint, String failMessage,
+            String[] insufficientRoles, boolean doRegisterCategory) throws Exception,
+            MalformedURLException, IOException, ProtocolException {
+        String testuser = "testuser";
+        String password = "testpassword";
+        final LoginService loginService = new TestLoginService(testuser, password, insufficientRoles); 
+        port = FreePortFinder.findFreePort(new TryPort() {
+            
+            @Override
+            public void tryPort(int port) throws Exception {
+                startServer(port, loginService);
+            }
+        });
+        if (doRegisterCategory) {
+            registerCategory(testuser, password);
+        }
+        
+        String endpoint = getEndpoint();
+        URL url = new URL(endpoint + "/" + pathForEndPoint);
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setRequestMethod("POST");
+        sendAuthentication(conn, testuser, password);
+        conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+        
+        assertEquals(failMessage, HttpServletResponse.SC_FORBIDDEN, conn.getResponseCode());
+    }
 
     @Test
     public void authorizedReplacePutPojo() throws Exception {
@@ -279,7 +385,7 @@
         URL url = new URL(endpoint + "/put-pojo");
         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
         conn.setRequestMethod("POST");
-        sendAuthorization(conn, testuser, password);
+        sendAuthentication(conn, testuser, password);
 
         conn.setDoOutput(true);
         conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
@@ -297,14 +403,147 @@
         verify(mockStorage).createReplace(category);
         verify(replace).setPojo(expected1);
         verify(replace).apply();
+    }    
+    
+    @Test
+    public void unauthorizedReplacePutPojo() throws Exception {
+        String[] insufficientRoleNames = new String[] {
+                Roles.REGISTER_CATEGORY,
+        };
+        String testuser = "testuser";
+        String password = "testpassword";
+        final LoginService loginService = new TestLoginService(testuser, password, insufficientRoleNames); 
+        port = FreePortFinder.findFreePort(new TryPort() {
+            
+            @Override
+            public void tryPort(int port) throws Exception {
+                startServer(port, loginService);
+            }
+        });
+        registerCategory(testuser, password);
+        
+        String endpoint = getEndpoint();
+        URL url = new URL(endpoint + "/put-pojo");
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setRequestMethod("POST");
+        sendAuthentication(conn, testuser, password);
+        conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+        conn.setDoOutput(true);
+        conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+        // replace
+        WebInsert insert = new WebInsert(categoryId, true);
+        Gson gson = new Gson();
+        OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
+        out.write("insert=");
+        gson.toJson(insert, out);
+        out.flush();
+        out.write("&pojo=");
+        TestClass expected1 = new TestClass();
+        gson.toJson(expected1, out);
+        out.write("\n");
+        out.flush();
+        
+        assertEquals("thermostat-replace role missing", HttpServletResponse.SC_FORBIDDEN, conn.getResponseCode());
     }
 
-    private void sendAuthorization(HttpURLConnection conn, String username, String passwd) {
+    @Test
+    public void authorizedInsertPutPojo() throws Exception {
+        String[] roleNames = new String[] {
+                Roles.APPEND,
+                Roles.REGISTER_CATEGORY
+        };
+        String testuser = "testuser";
+        String password = "testpassword";
+        final LoginService loginService = new TestLoginService(testuser, password, roleNames); 
+        port = FreePortFinder.findFreePort(new TryPort() {
+            
+            @Override
+            public void tryPort(int port) throws Exception {
+                startServer(port, loginService);
+            }
+        });
+        registerCategory(testuser, password);
+        
+        Add insert = mock(Add.class);
+        when(mockStorage.createAdd(any(Category.class))).thenReturn(insert);
+
+        TestClass expected1 = new TestClass();
+        expected1.setKey1("fluff1");
+        expected1.setKey2(42);
+
+        String endpoint = getEndpoint();
+
+        URL url = new URL(endpoint + "/put-pojo");
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setRequestMethod("POST");
+        sendAuthentication(conn, testuser, password);
+
+        conn.setDoOutput(true);
+        conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+        WebInsert ins = new WebInsert(categoryId, false);
+        Gson gson = new Gson();
+        OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
+        out.write("insert=");
+        gson.toJson(ins, out);
+        out.flush();
+        out.write("&pojo=");
+        gson.toJson(expected1, out);
+        out.write("\n");
+        out.flush();
+        assertEquals(200, conn.getResponseCode());
+        verify(mockStorage).createAdd(category);
+        verify(insert).setPojo(expected1);
+        verify(insert).apply();
+    }
+    
+    @Test
+    public void unauthorizedInsertPutPojo() throws Exception {
+        String[] insufficientRoleNames = new String[] {
+                Roles.REGISTER_CATEGORY,
+        };
+        String testuser = "testuser";
+        String password = "testpassword";
+        final LoginService loginService = new TestLoginService(testuser, password, insufficientRoleNames); 
+        port = FreePortFinder.findFreePort(new TryPort() {
+            
+            @Override
+            public void tryPort(int port) throws Exception {
+                startServer(port, loginService);
+            }
+        });
+        registerCategory(testuser, password);
+        
+        String endpoint = getEndpoint();
+        URL url = new URL(endpoint + "/put-pojo");
+        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+        conn.setRequestMethod("POST");
+        sendAuthentication(conn, testuser, password);
+        conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+        conn.setDoOutput(true);
+        conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
+        // replace
+        WebInsert insert = new WebInsert(categoryId, false);
+        Gson gson = new Gson();
+        OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
+        out.write("insert=");
+        gson.toJson(insert, out);
+        out.flush();
+        out.write("&pojo=");
+        TestClass expected1 = new TestClass();
+        gson.toJson(expected1, out);
+        out.write("\n");
+        out.flush();
+        
+        assertEquals("thermostat-add role missing", HttpServletResponse.SC_FORBIDDEN, conn.getResponseCode());
+    }
+    
+    private void sendAuthentication(HttpURLConnection conn, String username, String passwd) {
         String userpassword = username + ":" + passwd;
         String encodedAuthorization = Base64.encodeBase64String(userpassword.getBytes());
         conn.setRequestProperty("Authorization", "Basic "+ encodedAuthorization);
     }
 
+    @SuppressWarnings("unchecked")
     @Test
     public void authorizedRemovePojo() throws Exception {
         String[] roleNames = new String[] {
@@ -335,7 +574,7 @@
         URL url = new URL(endpoint + "/remove-pojo");
         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
         conn.setRequestMethod("POST");
-        sendAuthorization(conn, testuser, password);
+        sendAuthentication(conn, testuser, password);
         conn.setDoOutput(true);
         conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
         Map<Category<?>,Integer> categoryIds = new HashMap<>();
@@ -354,6 +593,12 @@
         verify(mockRemove).where(key1, "test");
         verify(mockStorage).removePojo(mockRemove);
     }
+    
+    @Test
+    public void unauthorizedRemovePojo() throws Exception {
+        String failMsg = "thermostat-remove role missing, expected Forbidden!";
+        doUnauthorizedTest("remove-pojo", failMsg);
+    }
 
     @Test
     public void authorizedUpdatePojo() throws Exception {
@@ -381,7 +626,7 @@
         URL url = new URL(endpoint + "/update-pojo");
         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
         conn.setRequestMethod("POST");
-        sendAuthorization(conn, testuser, password);
+        sendAuthentication(conn, testuser, password);
         conn.setDoOutput(true);
         conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
 
@@ -408,6 +653,12 @@
         verify(mockUpdate).apply();
         verifyNoMoreInteractions(mockUpdate);
     }
+    
+    @Test
+    public void unauthorizedUpdatePojo() throws Exception {
+        String failMsg = "thermostat-update role missing, expected Forbidden!";
+        doUnauthorizedTest("update-pojo", failMsg);
+    }
 
 
     @Test
@@ -434,7 +685,7 @@
         URL url = new URL(endpoint + "/get-count");
         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
         conn.setRequestMethod("POST");
-        sendAuthorization(conn, testuser, password);
+        sendAuthentication(conn, testuser, password);
         conn.setDoOutput(true);
         conn.setDoInput(true);
         conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
@@ -451,6 +702,12 @@
         verify(mockStorage).getCount(category);
         
     }
+    
+    @Test
+    public void unauthorizedGetCount() throws Exception {
+        String failMsg = "thermostat-get-count role missing, expected Forbidden!";
+        doUnauthorizedTest("get-count", failMsg);
+    }
 
     @Test
     public void authorizedSaveFile() throws Exception {
@@ -472,7 +729,7 @@
         URL url = new URL(endpoint + "/save-file");
         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
         conn.setRequestMethod("POST");
-        sendAuthorization(conn, testuser, password);
+        sendAuthentication(conn, testuser, password);
         conn.setDoOutput(true);
         conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=fluff");
         conn.setRequestProperty("Transfer-Encoding", "chunked");
@@ -485,7 +742,8 @@
         out.write("Hello World\r\n");
         out.write("--fluff--\r\n");
         out.flush();
-        int status = conn.getResponseCode();
+        // needed in order to trigger inCaptor interaction with mock
+        conn.getResponseCode();
         ArgumentCaptor<InputStream> inCaptor = ArgumentCaptor.forClass(InputStream.class);
         verify(mockStorage).saveFile(eq("fluff"), inCaptor.capture());
         InputStream in = inCaptor.getValue();
@@ -500,6 +758,13 @@
         }
         assertEquals("Hello World", new String(data));
     }
+    
+    @Test
+    public void unauthorizedSaveFile() throws Exception {
+        String failMsg = "thermostat-save-file role missing, expected Forbidden!";
+        String[] insufficientRoles = new String[0];
+        doUnauthorizedTest("save-file", failMsg, insufficientRoles, false);
+    }
 
     @Test
     public void authorizedLoadFile() throws Exception {
@@ -526,7 +791,7 @@
 
         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
         conn.setRequestMethod("POST");
-        sendAuthorization(conn, testuser, password);
+        sendAuthentication(conn, testuser, password);
         conn.setDoOutput(true);
         conn.setDoInput(true);
         OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
@@ -545,6 +810,13 @@
         assertEquals("Hello World", new String(data));
         verify(mockStorage).loadFile("fluff");
     }
+    
+    @Test
+    public void unauthorizedLoadFile() throws Exception {
+        String failMsg = "thermostat-load-file role missing, expected Forbidden!";
+        String[] insufficientRoles = new String[0];
+        doUnauthorizedTest("load-file", failMsg, insufficientRoles, false);
+    }
 
     @Test
     public void authorizedPurge() throws Exception {
@@ -566,12 +838,19 @@
         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
         conn.setDoOutput(true);
         conn.setRequestMethod("POST");
-        sendAuthorization(conn, testuser, password);
+        sendAuthentication(conn, testuser, password);
         conn.getOutputStream().write("agentId=fluff".getBytes());
         int status = conn.getResponseCode();
         assertEquals(200, status);
         verify(mockStorage).purge("fluff");
     }
+    
+    @Test
+    public void unauthorizedPurge() throws Exception {
+        String failMsg = "thermostat-purge role missing, expected Forbidden!";
+        String[] insufficientRoles = new String[0];
+        doUnauthorizedTest("purge", failMsg, insufficientRoles, false);
+    }
 
     private void registerCategory(String username, String password) {
         try {
@@ -583,7 +862,7 @@
             conn.setDoOutput(true);
             conn.setDoInput(true);
             conn.setRequestMethod("POST");
-            sendAuthorization(conn, username, password);
+            sendAuthentication(conn, username, password);
             OutputStream out = conn.getOutputStream();
             Gson gson = new Gson();
             OutputStreamWriter writer = new OutputStreamWriter(out);
@@ -626,31 +905,9 @@
 
     @Test
     public void unauthorizedGenerateToken() throws Exception {
-        String[] noRoles = new String[0];
-        String testuser = "testuser";
-        String password = "testpassword";
-        final LoginService loginService = new TestLoginService(testuser, password, noRoles); 
-        
-        port = FreePortFinder.findFreePort(new TryPort() {
-            
-            @Override
-            public void tryPort(int port) throws Exception {
-                startServer(port, loginService);
-            }
-        });
-        
-        String endpoint = getEndpoint();
-        URL url = new URL(endpoint + "/generate-token");
-
-        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
-        conn.setRequestMethod("POST");
-        sendAuthorization(conn, testuser, password);
-        conn.setDoOutput(true);
-        conn.setDoInput(true);
-        OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
-        out.write("client-token=fluff");
-        out.flush();
-        assertEquals(403, conn.getResponseCode());
+        String failMsg = "thermostat-cmdc-generate role missing, expected Forbidden!";
+        String[] insufficientRoles = new String[0];
+        doUnauthorizedTest("generate-token", failMsg, insufficientRoles, false);
     }
 
     @Test
@@ -677,7 +934,7 @@
         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
         conn.setRequestMethod("POST");
         conn.setRequestProperty("Content-type", "application/x-www-form-urlencoded");
-        sendAuthorization(conn, testuser, password);
+        sendAuthentication(conn, testuser, password);
         conn.setDoOutput(true);
         conn.setDoInput(true);
         OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
@@ -712,7 +969,7 @@
         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
         conn.setRequestMethod("POST");
         conn.setRequestProperty("Content-type", "application/x-www-form-urlencoded");
-        sendAuthorization(conn, testuser, password);
+        sendAuthentication(conn, testuser, password);
         conn.setDoOutput(true);
         conn.setDoInput(true);
         OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
@@ -745,7 +1002,7 @@
         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
         conn.setRequestMethod("POST");
         conn.setRequestProperty("Content-type", "application/x-www-form-urlencoded");
-        sendAuthorization(conn, testuser, password);
+        sendAuthentication(conn, testuser, password);
         conn.setDoOutput(true);
         conn.setDoInput(true);
         OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
@@ -753,6 +1010,13 @@
         out.flush();
         assertEquals(403, conn.getResponseCode());
     }
+    
+    @Test
+    public void unauthorizedVerifyToken() throws Exception {
+        String failMsg = "thermostat-cmdc-verify role missing, expected Forbidden!";
+        String[] insufficientRoles = new String[0];
+        doUnauthorizedTest("verify-token", failMsg, insufficientRoles, false);
+    }
 
     private byte[] verifyAuthorizedGenerateToken(String username, String password) throws IOException {
         String endpoint = getEndpoint();
@@ -760,7 +1024,7 @@
 
         HttpURLConnection conn = (HttpURLConnection) url.openConnection();
         conn.setRequestMethod("POST");
-        sendAuthorization(conn, username, password);
+        sendAuthentication(conn, username, password);
         conn.setDoOutput(true);
         conn.setDoInput(true);
         OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());