view web/client/src/test/java/com/redhat/thermostat/web/client/internal/WebStorageTest.java @ 1412:a0592d702416

Update copyright year in release branch. reviewed-by: neugens review-thread: http://icedtea.classpath.org/pipermail/thermostat/2014-June/009965.html PR1821
author Jon VanAlten <jon.vanalten@redhat.com>
date Tue, 03 Jun 2014 11:55:56 -0600
parents 044868dbf9ad
children
line wrap: on
line source

/*
 * Copyright 2012-2014 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.client.internal;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.CountDownLatch;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.codec.binary.Base64;
import org.apache.http.client.HttpClient;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.impl.client.DefaultHttpClient;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.Mockito;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.redhat.thermostat.shared.config.SSLConfiguration;
import com.redhat.thermostat.storage.core.AuthToken;
import com.redhat.thermostat.storage.core.BackingStorage;
import com.redhat.thermostat.storage.core.Categories;
import com.redhat.thermostat.storage.core.Category;
import com.redhat.thermostat.storage.core.Connection.ConnectionListener;
import com.redhat.thermostat.storage.core.Connection.ConnectionStatus;
import com.redhat.thermostat.storage.core.Cursor;
import com.redhat.thermostat.storage.core.DescriptorParsingException;
import com.redhat.thermostat.storage.core.IllegalDescriptorException;
import com.redhat.thermostat.storage.core.Key;
import com.redhat.thermostat.storage.core.PreparedParameter;
import com.redhat.thermostat.storage.core.PreparedParameters;
import com.redhat.thermostat.storage.core.PreparedStatement;
import com.redhat.thermostat.storage.core.StatementDescriptor;
import com.redhat.thermostat.storage.core.StatementExecutionException;
import com.redhat.thermostat.storage.core.StorageCredentials;
import com.redhat.thermostat.storage.core.StorageException;
import com.redhat.thermostat.storage.model.Pojo;
import com.redhat.thermostat.test.FreePortFinder;
import com.redhat.thermostat.test.FreePortFinder.TryPort;
import com.redhat.thermostat.web.common.PreparedParameterSerializer;
import com.redhat.thermostat.web.common.PreparedStatementResponseCode;
import com.redhat.thermostat.web.common.ThermostatGSONConverter;
import com.redhat.thermostat.web.common.WebPreparedStatement;
import com.redhat.thermostat.web.common.WebPreparedStatementResponse;
import com.redhat.thermostat.web.common.WebPreparedStatementSerializer;
import com.redhat.thermostat.web.common.WebQueryResponse;
import com.redhat.thermostat.web.common.WebQueryResponseSerializer;

public class WebStorageTest {

    private Server server;

    private int port;

    // Set these in prepareServer() to determine server behaviour
    private int responseStatus = HttpServletResponse.SC_OK;
    private String responseBody;

    // These get set by test server handler (anonymous class in startServer())
    // Check them after WebStorage method call that should interact with server.
    private String requestBody;
    private Map<String,String> headers;
    private String method;
    private String requestURI;

    private static Category<TestObj> category;
    private static Key<String> key1;

    private WebStorage storage;


    @BeforeClass
    public static void setupCategory() {
        key1 = new Key<>("property1");
        category = new Category<>("test", TestObj.class, key1);
    }

    @AfterClass
    public static void cleanupCategory() {
        Categories.remove(category);
        category = null;
        key1 = null;
    }

    @Before
    public void setUp() throws Exception {

        port = FreePortFinder.findFreePort(new TryPort() {
            @Override
            public void tryPort(int port) throws Exception {
                startServer(port);
            }
        });

        SSLConfiguration sslConf = mock(SSLConfiguration.class);
        storage = new WebStorage("http://localhost:" + port + "/", 
                new TrivialStorageCredentials(null, null), sslConf);
        headers = new HashMap<>();
        registerCategory();
    }

    private void startServer(int port) throws Exception {
        server = new Server(port);
        server.setHandler(new AbstractHandler() {
            
            @Override
            public void handle(String target, Request baseRequest,
                    HttpServletRequest request, HttpServletResponse response)
                    throws IOException, ServletException {
                Enumeration<String> headerNames = request.getHeaderNames();
                while (headerNames.hasMoreElements()) {
                    String headerName = headerNames.nextElement();
                    headers.put(headerName, request.getHeader(headerName));
                }

                method = request.getMethod();
                requestURI = request.getRequestURI();

                // Read request body.
                StringBuilder body = new StringBuilder();
                Reader reader = request.getReader();
                while (true) {
                    int read = reader.read();
                    if (read == -1) {
                        break;
                    }
                    body.append((char) read);
                }
                requestBody = body.toString();
                // Send response body.
                response.setStatus(responseStatus);
                if (responseBody != null) {
                    response.getWriter().write(responseBody);
                }
                baseRequest.setHandled(true);
            }
        });
        server.start();
    }

    // Specified status and response body.
    private void prepareServer(int responseStatus, String responseBody) {
        this.responseStatus = responseStatus;
        this.responseBody = responseBody;

        requestBody = null;
        requestURI = null;
        method = null;
        headers.clear();
    }

    // Specified status and null response body.
    private void prepareServer(int responseStatus) {
        prepareServer(responseStatus, null);
    }

    // OK status and specified response body.
    private void prepareServer(String responseBody) {
        prepareServer(HttpServletResponse.SC_OK, responseBody);
    }

    // OK status and null response body.
    private void prepareServer() {
        prepareServer(HttpServletResponse.SC_OK);
    }

    @After
    public void tearDown() throws Exception {

        headers = null;
        requestURI = null;
        method = null;
        storage = null;

        server.stop();
        server.join();
    }

    private void registerCategory() {

        // Return 42 for categoryId.
        Gson gson = new Gson();
        responseBody = gson.toJson(42);

        storage.registerCategory(category);
    }
    
    // WebStorage is a proxy storage, no backing storage
    @Test
    public void isNoBackingStorage() {
        assertFalse(storage instanceof BackingStorage);
    }
    
    @Test
    public void preparingFaultyDescriptorThrowsException() throws UnsupportedEncodingException, IOException {
        Gson gson = new GsonBuilder()
                .registerTypeAdapter(WebPreparedStatement.class, new WebPreparedStatementSerializer())
                .create();

        // missing quotes for LHS key
        String strDesc = "QUERY test WHERE a = ?s";
        StatementDescriptor<TestObj> desc = new StatementDescriptor<>(category, strDesc);
        
        WebPreparedStatementResponse fakeResponse = new WebPreparedStatementResponse();
        fakeResponse.setStatementId(WebPreparedStatementResponse.DESCRIPTOR_PARSE_FAILED);
        prepareServer(gson.toJson(fakeResponse));
        try {
            storage.prepareStatement(desc);
            fail("Should have refused to prepare the statement");
        } catch (IllegalDescriptorException e) {
            // should have thrown superclass DescriptorParsingException
            fail(e.getMessage());
        } catch (DescriptorParsingException e) {
            // pass
        }
    }
    
    @Test
    public void preparingUnknownDescriptorThrowsException() throws UnsupportedEncodingException, IOException {
        Gson gson = new GsonBuilder()
                .registerTypeAdapter(WebPreparedStatement.class, new WebPreparedStatementSerializer())
                .create();

        String strDesc = "QUERY test WHERE 'property1' = ?s";
        StatementDescriptor<TestObj> desc = new StatementDescriptor<>(category, strDesc);
        
        WebPreparedStatementResponse fakeResponse = new WebPreparedStatementResponse();
        fakeResponse.setStatementId(WebPreparedStatementResponse.ILLEGAL_STATEMENT);
        prepareServer(gson.toJson(fakeResponse));
        try {
            storage.prepareStatement(desc);
            fail("Should have refused to prepare the statement");
        } catch (IllegalDescriptorException e) {
            // pass
            assertEquals(strDesc, e.getFailedDescriptor());
        } catch (DescriptorParsingException e) {
            // should have thrown IllegalDescriptorException
            fail(e.getMessage());
        }
    }
    
    @Test
    public void forbiddenExecuteQueryThrowsConsumingExcptn() throws UnsupportedEncodingException, IOException {
        Gson gson = new GsonBuilder().registerTypeHierarchyAdapter(PreparedParameter.class, new PreparedParameterSerializer())
                .registerTypeAdapter(WebPreparedStatement.class, new WebPreparedStatementSerializer())
                .registerTypeAdapter(WebQueryResponse.class, new WebQueryResponseSerializer<>())
                .registerTypeAdapter(Pojo.class, new ThermostatGSONConverter())
                .create();

        String strDesc = "QUERY test WHERE 'property1' = ?s";
        StatementDescriptor<TestObj> desc = new StatementDescriptor<>(category, strDesc);
        PreparedStatement<TestObj> stmt = null;
        
        int fakePrepStmtId = 5;
        WebPreparedStatementResponse fakeResponse = new WebPreparedStatementResponse();
        fakeResponse.setNumFreeVariables(1);
        fakeResponse.setStatementId(fakePrepStmtId);
        prepareServer(gson.toJson(fakeResponse));
        try {
            stmt = storage.prepareStatement(desc);
        } catch (DescriptorParsingException e) {
            // descriptor should parse fine and is trusted
            fail(e.getMessage());
        }
        assertTrue(stmt instanceof WebPreparedStatement);
        WebPreparedStatement<TestObj> webStmt = (WebPreparedStatement<TestObj>)stmt;
        assertEquals(fakePrepStmtId, webStmt.getStatementId());
        PreparedParameters params = webStmt.getParams();
        assertEquals(1, params.getParams().length);
        assertNull(params.getParams()[0]);
        
        // now set a parameter
        stmt.setString(0, "fluff");
        assertEquals("fluff", params.getParams()[0].getValue());
        assertEquals(String.class, params.getParams()[0].getType());
        
        prepareServer(HttpServletResponse.SC_FORBIDDEN);
        try {
            stmt.executeQuery();
            fail("Forbidden should have thrown an exception!");
        } catch (StatementExecutionException e) {
            Throwable t = e.getCause();
            assertTrue(t instanceof StorageException);
            t = t.getCause();
            assertTrue("Wanted EntityConsumingIOException as root cause", t instanceof EntityConsumingIOException);
        }
    }
    
    @Test
    public void canPrepareAndExecuteQuery() throws UnsupportedEncodingException, IOException {
        TestObj obj1 = new TestObj();
        obj1.setProperty1("fluffor1");
        TestObj obj2 = new TestObj();
        obj2.setProperty1("fluffor2");
        Gson gson = new GsonBuilder().registerTypeHierarchyAdapter(PreparedParameter.class, new PreparedParameterSerializer())
                .registerTypeAdapter(WebPreparedStatement.class, new WebPreparedStatementSerializer())
                .registerTypeAdapter(WebQueryResponse.class, new WebQueryResponseSerializer<>())
                .registerTypeAdapter(Pojo.class, new ThermostatGSONConverter())
                .create();

        String strDesc = "QUERY test WHERE 'property1' = ?s";
        StatementDescriptor<TestObj> desc = new StatementDescriptor<>(category, strDesc);
        PreparedStatement<TestObj> stmt = null;
        
        int fakePrepStmtId = 5;
        WebPreparedStatementResponse fakeResponse = new WebPreparedStatementResponse();
        fakeResponse.setNumFreeVariables(1);
        fakeResponse.setStatementId(fakePrepStmtId);
        prepareServer(gson.toJson(fakeResponse));
        try {
            stmt = storage.prepareStatement(desc);
        } catch (DescriptorParsingException e) {
            // descriptor should parse fine and is trusted
            fail(e.getMessage());
        }
        assertTrue(stmt instanceof WebPreparedStatement);
        WebPreparedStatement<TestObj> webStmt = (WebPreparedStatement<TestObj>)stmt;
        assertEquals(fakePrepStmtId, webStmt.getStatementId());
        PreparedParameters params = webStmt.getParams();
        assertEquals(1, params.getParams().length);
        assertNull(params.getParams()[0]);
        
        // now set a parameter
        stmt.setString(0, "fluff");
        assertEquals("fluff", params.getParams()[0].getValue());
        assertEquals(String.class, params.getParams()[0].getType());
        
        WebQueryResponse<TestObj> fakeQueryResponse = new WebQueryResponse<>();
        fakeQueryResponse.setResponseCode(PreparedStatementResponseCode.QUERY_SUCCESS);
        fakeQueryResponse.setResultList(new TestObj[] { obj1, obj2 });
        prepareServer(gson.toJson(fakeQueryResponse));
        Cursor<TestObj> results = null;
        try {
            results = stmt.executeQuery();
        } catch (StatementExecutionException e) {
            // should execute fine
            e.printStackTrace();
            fail(e.getMessage());
        }
        assertNotNull(results);
        assertTrue(results instanceof WebCursor);
        assertTrue(results.hasNext());
        assertEquals("fluffor1", results.next().getProperty1());
        assertTrue(results.hasNext());
        assertEquals("fluffor2", results.next().getProperty1());
        assertFalse(results.hasNext());
        try {
            results.next();
            fail();
        } catch (NoSuchElementException ex) {
            // Pass.
        }
    }
    
    @Test
    public void canPrepareAndExecuteWrite() {
        TestObj obj1 = new TestObj();
        obj1.setProperty1("fluffor1");
        TestObj obj2 = new TestObj();
        obj2.setProperty1("fluffor2");
        Gson gson = new GsonBuilder().registerTypeAdapter(PreparedParameter.class, new PreparedParameterSerializer())
                .registerTypeAdapter(WebPreparedStatement.class, new WebPreparedStatementSerializer())
                .registerTypeHierarchyAdapter(Pojo.class, new ThermostatGSONConverter())
                .create();

        String strDesc = "ADD test SET 'property1' = ?s";
        StatementDescriptor<TestObj> desc = new StatementDescriptor<>(category, strDesc);
        PreparedStatement<TestObj> stmt = null;
        
        int fakePrepStmtId = 3;
        WebPreparedStatementResponse fakeResponse = new WebPreparedStatementResponse();
        fakeResponse.setNumFreeVariables(1);
        fakeResponse.setStatementId(fakePrepStmtId);
        prepareServer(gson.toJson(fakeResponse));
        try {
            stmt = storage.prepareStatement(desc);
        } catch (DescriptorParsingException e) {
            // descriptor should parse fine and is trusted
            fail(e.getMessage());
        }
        assertTrue(stmt instanceof WebPreparedStatement);
        WebPreparedStatement<TestObj> webStmt = (WebPreparedStatement<TestObj>)stmt;
        assertEquals(fakePrepStmtId, webStmt.getStatementId());
        PreparedParameters params = webStmt.getParams();
        assertEquals(1, params.getParams().length);
        assertNull(params.getParams()[0]);
        
        // now set a parameter
        stmt.setString(0, "fluff");
        assertEquals("fluff", params.getParams()[0].getValue());
        assertEquals(String.class, params.getParams()[0].getType());
        
        prepareServer(gson.toJson(PreparedStatementResponseCode.WRITE_GENERIC_FAILURE));
        
        int response = Integer.MAX_VALUE;
        try {
            response = stmt.execute();
        } catch (StatementExecutionException e) {
            // should execute fine
            e.printStackTrace();
            fail(e.getMessage());
        }
        
        assertEquals(PreparedStatementResponseCode.WRITE_GENERIC_FAILURE, response);
    }
    
    @Test
    public void forbiddenExecuteWriteReturnsGenericWriteFailure() {
        Gson gson = new GsonBuilder().registerTypeAdapter(PreparedParameter.class, new PreparedParameterSerializer())
                .registerTypeAdapter(WebPreparedStatement.class, new WebPreparedStatementSerializer())
                .registerTypeHierarchyAdapter(Pojo.class, new ThermostatGSONConverter())
                .create();

        String strDesc = "ADD test SET 'property1' = ?s";
        StatementDescriptor<TestObj> desc = new StatementDescriptor<>(category, strDesc);
        PreparedStatement<TestObj> stmt = null;
        
        int fakePrepStmtId = 3;
        WebPreparedStatementResponse fakeResponse = new WebPreparedStatementResponse();
        fakeResponse.setNumFreeVariables(1);
        fakeResponse.setStatementId(fakePrepStmtId);
        prepareServer(gson.toJson(fakeResponse));
        try {
            stmt = storage.prepareStatement(desc);
        } catch (DescriptorParsingException e) {
            // descriptor should parse fine and is trusted
            fail(e.getMessage());
        }
        assertTrue(stmt instanceof WebPreparedStatement);
        WebPreparedStatement<TestObj> webStmt = (WebPreparedStatement<TestObj>)stmt;
        assertEquals(fakePrepStmtId, webStmt.getStatementId());
        PreparedParameters params = webStmt.getParams();
        assertEquals(1, params.getParams().length);
        assertNull(params.getParams()[0]);
        
        // now set a parameter
        stmt.setString(0, "fluff");
        assertEquals("fluff", params.getParams()[0].getValue());
        assertEquals(String.class, params.getParams()[0].getType());
        
        prepareServer(HttpServletResponse.SC_FORBIDDEN);
        try {
            stmt.execute();
            fail("Forbidden should have thrown an exception!");
        } catch (StatementExecutionException e) {
            Throwable t = e.getCause();
            assertTrue(t instanceof StorageException);
            t = t.getCause();
            assertTrue("Wanted EntityConsumingIOException as root cause", t instanceof EntityConsumingIOException);
        }
    }

    @Test
    public void testSaveFile() {
        String data = "Hello World";
        ByteArrayInputStream in = new ByteArrayInputStream(data.getBytes());

        prepareServer();
        storage.saveFile("fluff", in);

        assertEquals("chunked", headers.get("Transfer-Encoding"));
        String contentType = headers.get("Content-Type");
        assertTrue(contentType.startsWith("multipart/form-data; boundary="));
        String boundary = contentType.split("boundary=")[1];
        String[] lines = requestBody.split("\n");
        assertEquals("--" + boundary, lines[0].trim());
        assertEquals("Content-Disposition: form-data; name=\"file\"; filename=\"fluff\"", lines[1].trim());
        assertEquals("Content-Type: application/octet-stream", lines[2].trim());
        assertEquals("Content-Transfer-Encoding: binary", lines[3].trim());
        assertEquals("", lines[4].trim());
        assertEquals("Hello World", lines[5].trim());
        assertEquals("--" + boundary + "--", lines[6].trim());
        
    }

    @Test
    public void testLoadFile() throws IOException {
        prepareServer(HttpServletResponse.SC_NO_CONTENT);
        InputStream in = storage.loadFile("no_file_here");
        assertNull(in);

        prepareServer("Hello World");
        in = storage.loadFile("fluff");
        assertEquals("file=fluff", requestBody.trim());
        byte[] data = new byte[11];
        int totalRead = 0;
        while (totalRead < 11) {
            int read = in.read(data, totalRead, 11 - totalRead);
            if (read < 0) {
                fail();
            }
            totalRead += read;
        }
        assertEquals("Hello World", new String(data));

    }

    @Test
    public void testPurge() throws UnsupportedEncodingException, IOException {

        prepareServer();
        storage.purge("fluff");

        assertEquals("POST", method);
        assertTrue(requestURI.endsWith("/purge"));
        StringReader reader = new StringReader(requestBody);
        BufferedReader bufRead = new BufferedReader(reader);
        String line = URLDecoder.decode(bufRead.readLine(), "UTF-8");
        String[] parts = line.split("=");
        assertEquals("agentId", parts[0]);
        assertEquals("fluff", parts[1]);
    }

    @Test
    public void testGenerateToken() throws UnsupportedEncodingException {

        final String actionName = "some action";

        prepareServer("flufftoken");
        AuthToken authToken = storage.generateToken(actionName);

        assertTrue(requestURI.endsWith("/generate-token"));
        assertEquals("POST", method);

        String[] requestParts = requestBody.split("=");
        assertEquals("client-token", requestParts[0]);
        String clientTokenParam = URLDecoder.decode(requestParts[1], "UTF-8");
        byte[] clientToken = Base64.decodeBase64(clientTokenParam);
        assertEquals(256, clientToken.length);

        byte[] tokenBytes = authToken.getToken();
        assertEquals("flufftoken", new String(tokenBytes));

        assertTrue(Arrays.equals(clientToken, authToken.getClientToken()));

        // Send another request and verify that we send a different client-token every time.
        prepareServer("flufftoken");
        storage.generateToken(actionName);

        requestParts = requestBody.split("=");
        assertEquals("client-token", requestParts[0]);
        clientTokenParam = URLDecoder.decode(requestParts[1], "UTF-8");
        byte[] clientToken2 = Base64.decodeBase64(clientTokenParam);
        assertFalse(Arrays.equals(clientToken, clientToken2));

    }

    @Test
    public void canSSLEnableClient() {
        SSLConfiguration sslConf = mock(SSLConfiguration.class);
        WebStorage storage = new WebStorage("https://onlyHttpsPrefixUsed.example.com",
                new TrivialStorageCredentials(null, null), sslConf);
        HttpClient client = storage.httpClient;
        SchemeRegistry schemeReg = client.getConnectionManager().getSchemeRegistry();
        Scheme scheme = schemeReg.getScheme("https");
        assertNotNull(scheme);
        assertEquals(443, scheme.getDefaultPort());
    }

    @Test
    public void testVerifyToken() throws UnsupportedEncodingException, NoSuchAlgorithmException {
        byte[] token = "stuff".getBytes();
        String clientToken = "fluff";
        String someAction = "someAction";
        byte[] tokenDigest = getShaBytes(clientToken, someAction);
        
        AuthToken authToken = new AuthToken(token, tokenDigest);

        prepareServer();
        boolean ok = storage.verifyToken(authToken, someAction);

        assertTrue(ok);
        assertTrue(requestURI.endsWith("/verify-token"));
        assertEquals("POST", method);
        String[] requestParts = requestBody.split("&");
        assertEquals(3, requestParts.length);
        String[] clientTokenParts = requestParts[0].split("=");
        assertEquals(2, clientTokenParts.length);
        assertEquals("client-token", clientTokenParts[0]);
        String urlDecoded = URLDecoder.decode(clientTokenParts[1], "UTF-8");
        assertTrue(Arrays.equals(tokenDigest, Base64.decodeBase64(urlDecoded)));
        String[] authTokenParts = requestParts[1].split("=");
        assertEquals(2, authTokenParts.length);
        assertEquals("token", authTokenParts[0]);
        String[] actionParts = requestParts[2].split("=");
        assertEquals(2, actionParts.length);
        assertEquals("action-name", actionParts[0]);
        urlDecoded = URLDecoder.decode(authTokenParts[1], "UTF-8");
        String base64decoded = new String(Base64.decodeBase64(urlDecoded));
        assertEquals("stuff", base64decoded);

        // Try another one in which verification fails.
        prepareServer(HttpServletResponse.SC_UNAUTHORIZED);
        ok = storage.verifyToken(authToken, someAction);
        assertFalse(ok);
        
    }

    private byte[] getShaBytes(String clientToken, String someAction)
            throws NoSuchAlgorithmException, UnsupportedEncodingException {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        digest.update(clientToken.getBytes());
        digest.update(someAction.getBytes("UTF-8"));
        byte[] tokenDigest = digest.digest();
        return tokenDigest;
    }
    
    @Test
    public void verifyConnectFiresEventOnConnectionFailure() {
        DefaultHttpClient client = mock(DefaultHttpClient.class);
        ClientConnectionManager connManager = mock(ClientConnectionManager.class);
        // this should make connect fail
        Mockito.doThrow(RuntimeException.class).when(client).getCredentialsProvider();
        SSLConfiguration sslConf = mock(SSLConfiguration.class);
        storage = new WebStorage("http://localhost:" + port + "/", new TrivialStorageCredentials(null, null),
                client, connManager, sslConf);
        
        CountDownLatch latch = new CountDownLatch(1);
        MyListener listener = new MyListener(latch);
        storage.getConnection().addListener(listener);
        storage.getConnection().connect();
        // wait for connection to fail
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        assertFalse(listener.connectEvent);
        assertTrue(listener.failedToConnectEvent);
    }
    
    @Test
    public void verifyConnectFiresEventOnSuccessfulConnect() {
        CountDownLatch latch = new CountDownLatch(1);
        MyListener listener = new MyListener(latch);
        storage.getConnection().addListener(listener);
        storage.getConnection().connect();
        // wait for connection to happen
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        assertTrue(listener.connectEvent);
        assertFalse(listener.failedToConnectEvent);
    }
    
    @Test
    public void verifyDisconnectFiresDisconnectEvent() {
        CountDownLatch latch = new CountDownLatch(1);
        MyListener listener = new MyListener(latch);
        storage.getConnection().addListener(listener);
        storage.getConnection().disconnect();
        try {
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        assertFalse(listener.connectEvent);
        assertFalse(listener.failedToConnectEvent);
        assertTrue(listener.disconnectEvent);
    }
    
    static class MyListener implements ConnectionListener {

        private CountDownLatch latch;
        boolean failedToConnectEvent = false;
        boolean connectEvent = false;
        boolean disconnectEvent = false;
        
        MyListener(CountDownLatch latch) {
            this.latch = latch;
        }
        
        @Override
        public void changed(ConnectionStatus newStatus) {
            if (newStatus == ConnectionStatus.CONNECTED) {
                connectEvent = true;
                latch.countDown();
            }
            if (newStatus == ConnectionStatus.FAILED_TO_CONNECT) {
                failedToConnectEvent = true;
                latch.countDown();
            }
            if (newStatus == ConnectionStatus.DISCONNECTED) {
                disconnectEvent = true;
                latch.countDown();
            }
        }
    }

    private class TrivialStorageCredentials implements StorageCredentials {
        private String user;
        private char[] pw;
        private TrivialStorageCredentials(String user, char[] password) {
            this.user = user;
            this.pw = password;
        }
        @Override
        public String getUsername() {
            return user;
        }
        @Override
        public char[] getPassword() {
            return pw;
        }
    }
}