/* * Copyright 2012 Jason Miller * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jj.document; import static org.junit.Assert.*; import static org.mockito.BDDMockito.*; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.sameInstance; import java.nio.file.Paths; import java.util.HashSet; import jj.application.AppLocation; import jj.document.servable.DocumentRequestProcessor; import jj.engine.EngineAPI; import jj.http.server.websocket.CurrentWebSocketConnection; import jj.http.server.websocket.MockAbstractWebSocketConnectionHostDependencies; import jj.http.server.websocket.MockCurrentWebSocketConnection; import jj.http.server.websocket.WebSocketConnection; import jj.http.server.websocket.WebSocketConnectionHost; import jj.resource.NoSuchResourceException; import jj.resource.ResourceFinder; import jj.resource.ResourceNotViableException; import jj.script.PendingKey; import jj.script.ScriptSystemEvent; import jj.script.MockRhinoContextProvider; import jj.script.module.ScriptResource; import jj.util.Closer; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; import org.mockito.runners.MockitoJUnitRunner; import org.mozilla.javascript.Scriptable; import org.mozilla.javascript.ScriptableObject; @RunWith(MockitoJUnitRunner.class) public class DocumentScriptEnvironmentTest { @Mock HtmlResource html; @Mock ScriptResource script; @Mock ResourceFinder resourceFinder; @Mock EngineAPI api; @Mock ScriptableObject local; @Mock ScriptCompiler scriptCompiler; @Mock DocumentWebSocketMessageProcessors processors; @Mock DocumentRequestProcessor documentRequestProcessor; MockRhinoContextProvider contextMaker; MockAbstractWebSocketConnectionHostDependencies dependencies; @Mock WebSocketConnection connection; CurrentDocumentRequestProcessor currentDocument; CurrentWebSocketConnection currentConnection; @Captor ArgumentCaptor<ScriptSystemEvent> eventCaptor; @Before public void before() throws Exception { given(script.path()).willReturn(Paths.get("/")); currentDocument = new CurrentDocumentRequestProcessor(); currentConnection = new MockCurrentWebSocketConnection(); } private void makeResourceDependencies(String name) { dependencies = new MockAbstractWebSocketConnectionHostDependencies(DocumentScriptEnvironment.class, name, resourceFinder); contextMaker = dependencies.mockRhinoContextProvider(); given(contextMaker.context.newObject(any(Scriptable.class))).willReturn(local); given(contextMaker.context.newChainedScope(any(Scriptable.class))).willReturn(local); } private void givenAnHtmlResource(String baseName) throws Exception { given(resourceFinder.loadResource(HtmlResource.class, AppLocation.Public, baseName + ".html")).willReturn(html); } private void givenAClientScript(String baseName) throws Exception { given(resourceFinder.loadResource(ScriptResource.class, AppLocation.Public, ScriptResourceType.Client.suffix(baseName))).willReturn(script); } private void givenASharedScript(String baseName) throws Exception { given(resourceFinder.loadResource(ScriptResource.class, AppLocation.Public, ScriptResourceType.Shared.suffix(baseName))).willReturn(script); } private void givenAServerScript(String baseName) throws Exception { given(resourceFinder.loadResource(ScriptResource.class, AppLocation.Private, ScriptResourceType.Client.suffix(baseName))).willReturn(script); } private DocumentScriptEnvironment givenADocumentScriptEnvironment(String baseName) { makeResourceDependencies(baseName); return new DocumentScriptEnvironment( dependencies, api, scriptCompiler, processors, currentDocument, currentConnection ); } @Test public void testManagesContext() throws Exception { PendingKey key = new PendingKey(); String name = "index"; givenAnHtmlResource(name); given(html.document()).willReturn(Jsoup.parse("<html><head><title>test</title></head></html>")); DocumentScriptEnvironment dse = givenADocumentScriptEnvironment(name); willReturn(dse).given(connection).webSocketConnectionHost(); // we expect documents to be cloned on first access, and that instance should be maintained Document document = dse.document(); given(documentRequestProcessor.document()).willReturn(document); try (Closer ignored = currentDocument.enterScope(documentRequestProcessor)) { dse.captureContextForKey(key); } assertThat(currentDocument.current(), is(nullValue())); try (Closer ignored = dse.restoreContextForKey(key)) { assertThat(currentDocument.current(), is(sameInstance(documentRequestProcessor))); } assertThat(currentDocument.current(), is(nullValue())); try (Closer ignored = currentConnection.enterScope(connection)) { dse.captureContextForKey(key); } assertThat(currentConnection.current(), is(nullValue())); try (Closer ignored = dse.restoreContextForKey(key)) { assertThat(currentConnection.current(), is(sameInstance(connection))); } assertThat(currentConnection.current(), is(nullValue())); } @Test public void testThrowsNotFound() throws Exception { String baseName = "index"; try { givenADocumentScriptEnvironment(baseName); fail(); } catch (NoSuchResourceException nsre) { assertThat(nsre.getMessage(), is(DocumentScriptEnvironment.class.getName() + "@" + baseName + "-" + baseName + ".html")); } } @Test public void testCompilationFailureHandledCorrectly() throws Exception { String name = "broken"; givenAnHtmlResource(name); givenASharedScript(name); givenAServerScript(name); RuntimeException re = new RuntimeException(); willThrow(re).given(scriptCompiler).compile(local, null, script, null); try { givenADocumentScriptEnvironment(name); fail("compilation failure should have happened!"); } catch (ResourceNotViableException rnve) { assertThat(rnve.getMessage(), is(name)); assertThat(rnve.getCause(), is(sameInstance((Throwable)re))); } } @Test public void testAllTogether() throws Exception { String name = "index"; givenAnHtmlResource(name); givenAClientScript(name); given(script.source()).willReturn(""); givenASharedScript(name); givenAServerScript(name); DocumentScriptEnvironment result = givenADocumentScriptEnvironment(name); verify(html).addDependent(result); verify(script, times(3)).addDependent(result); assertThat(result.sha1().length(), is(40)); assertThat(result.serverPath(), is("/" + result.sha1() + "/" + name)); assertThat(result.socketUri(), is("/" + result.sha1() + "/" + name + ".socket")); } @Test public void testHTMLWithNoServerScript() throws Exception { String name = "index"; givenAnHtmlResource(name); givenAClientScript(name); givenASharedScript(name); DocumentScriptEnvironment result = givenADocumentScriptEnvironment(name); assertThat(result.sha1().length(), is(40)); assertThat(result.serverPath(), is("/" + result.sha1() + "/" + name)); assertThat(result.socketUri(), is(nullValue())); assertThat(result.scope(), is(nullValue())); assertThat(result.script(), is(nullValue())); } @Mock WebSocketConnection connection1; @Mock WebSocketConnection connection2; @Mock WebSocketConnection connection3; @Mock WebSocketConnection connection4; @Mock WebSocketConnection connection5; private DocumentScriptEnvironment givenAConnectedDocumentScriptEnvironment() throws Exception { String name = "index"; givenAnHtmlResource(name); givenAClientScript(name); givenAServerScript(name); WebSocketConnectionHost result = givenADocumentScriptEnvironment(name); result.connected(connection1); result.connected(connection2); result.connected(connection3); result.connected(connection4); result.connected(connection5); given(connection1.webSocketConnectionHost()).willReturn(result); given(connection2.webSocketConnectionHost()).willReturn(result); given(connection3.webSocketConnectionHost()).willReturn(result); given(connection4.webSocketConnectionHost()).willReturn(result); given(connection5.webSocketConnectionHost()).willReturn(result); return (DocumentScriptEnvironment)result; } @Test public void testConnectionBroadcasting() throws Exception { DocumentScriptEnvironment result = givenAConnectedDocumentScriptEnvironment(); assertThat(result.broadcasting(), is(false)); result.startBroadcasting(); assertThat(result.broadcasting(), is(true)); assertThat(result.currentConnection(), is(nullValue())); HashSet<WebSocketConnection> iterated = new HashSet<>(); while (result.nextConnection()) { iterated.add(result.currentConnection()); } result.endBroadcasting(); assertThat(result.broadcasting(), is(false)); assertThat(iterated, containsInAnyOrder(connection1, connection2, connection3, connection4, connection5)); } private HashSet<WebSocketConnection> makeSet() { HashSet<WebSocketConnection> result = new HashSet<>(); result.add(connection1); result.add(connection2); result.add(connection3); result.add(connection4); result.add(connection5); return result; } @Test public void testConnectionBroadcastingWithContinuationAndNesting() throws Exception { // TODO!! this should be externalized behavior since it will be common to all // web socket connection hosts! DocumentScriptEnvironment result = givenAConnectedDocumentScriptEnvironment(); HashSet<WebSocketConnection> iterated1 = makeSet(); HashSet<WebSocketConnection> iterated2 = makeSet(); result.startBroadcasting(); assertThat(result.nextConnection(), is(true)); assertThat(iterated1.remove(result.currentConnection()), is(true)); assertThat(result.nextConnection(), is(true)); assertThat(iterated1.remove(result.currentConnection()), is(true)); PendingKey key = new PendingKey(); result.captureContextForKey(key); result.exitedScope(); assertThat(result.currentConnection(), is(nullValue())); result.restoreContextForKey(key); assertThat(result.nextConnection(), is(true)); assertThat(iterated1.remove(result.currentConnection()), is(true)); { // now we nest broadcasting duties - the broadcast function for some reason is also // broadcasting something // wrapped in its own block for clarity result.startBroadcasting(); assertThat(result.nextConnection(), is(true)); assertThat(iterated2.remove(result.currentConnection()), is(true)); assertThat(result.nextConnection(), is(true)); assertThat(iterated2.remove(result.currentConnection()), is(true)); // should we continue again here? assertThat(result.nextConnection(), is(true)); assertThat(iterated2.remove(result.currentConnection()), is(true)); assertThat(result.nextConnection(), is(true)); assertThat(iterated2.remove(result.currentConnection()), is(true)); assertThat(result.nextConnection(), is(true)); assertThat(iterated2.remove(result.currentConnection()), is(true)); assertThat(result.nextConnection(), is(false)); result.endBroadcasting(); // and we end that without sufferance and so forbears love } assertThat(result.nextConnection(), is(true)); assertThat(iterated1.remove(result.currentConnection()), is(true)); assertThat(result.nextConnection(), is(true)); assertThat(iterated1.remove(result.currentConnection()), is(true)); assertThat(result.nextConnection(), is(false)); result.endBroadcasting(); assertTrue(iterated1.isEmpty()); assertTrue(iterated2.isEmpty()); } }