blob: 38d3a019ee39de1acd6f4035bd591ea31eca0778 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.qpid.protonj2.test.driver;
import static org.hamcrest.CoreMatchers.isA;
import static org.hamcrest.CoreMatchers.notNullValue;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.UUID;
import org.apache.qpid.protonj2.test.driver.actions.AMQPHeaderInjectAction;
import org.apache.qpid.protonj2.test.driver.actions.AttachInjectAction;
import org.apache.qpid.protonj2.test.driver.actions.BeginInjectAction;
import org.apache.qpid.protonj2.test.driver.actions.CloseInjectAction;
import org.apache.qpid.protonj2.test.driver.actions.DeclareInjectAction;
import org.apache.qpid.protonj2.test.driver.actions.DetachInjectAction;
import org.apache.qpid.protonj2.test.driver.actions.DetachLastCoordinatorInjectAction;
import org.apache.qpid.protonj2.test.driver.actions.DischargeInjectAction;
import org.apache.qpid.protonj2.test.driver.actions.DispositionInjectAction;
import org.apache.qpid.protonj2.test.driver.actions.EmptyFrameInjectAction;
import org.apache.qpid.protonj2.test.driver.actions.EndInjectAction;
import org.apache.qpid.protonj2.test.driver.actions.ExecuteUserCodeAction;
import org.apache.qpid.protonj2.test.driver.actions.FlowInjectAction;
import org.apache.qpid.protonj2.test.driver.actions.OpenInjectAction;
import org.apache.qpid.protonj2.test.driver.actions.RawBytesInjectAction;
import org.apache.qpid.protonj2.test.driver.actions.SaslChallengeInjectAction;
import org.apache.qpid.protonj2.test.driver.actions.SaslInitInjectAction;
import org.apache.qpid.protonj2.test.driver.actions.SaslMechanismsInjectAction;
import org.apache.qpid.protonj2.test.driver.actions.SaslOutcomeInjectAction;
import org.apache.qpid.protonj2.test.driver.actions.SaslResponseInjectAction;
import org.apache.qpid.protonj2.test.driver.actions.TransferInjectAction;
import org.apache.qpid.protonj2.test.driver.codec.messaging.Source;
import org.apache.qpid.protonj2.test.driver.codec.messaging.Target;
import org.apache.qpid.protonj2.test.driver.codec.primitives.DescribedType;
import org.apache.qpid.protonj2.test.driver.codec.primitives.UnsignedInteger;
import org.apache.qpid.protonj2.test.driver.codec.security.SaslCode;
import org.apache.qpid.protonj2.test.driver.codec.transactions.Coordinator;
import org.apache.qpid.protonj2.test.driver.codec.transport.AMQPHeader;
import org.apache.qpid.protonj2.test.driver.codec.transport.Role;
import org.apache.qpid.protonj2.test.driver.expectations.AMQPHeaderExpectation;
import org.apache.qpid.protonj2.test.driver.expectations.AttachExpectation;
import org.apache.qpid.protonj2.test.driver.expectations.BeginExpectation;
import org.apache.qpid.protonj2.test.driver.expectations.CloseExpectation;
import org.apache.qpid.protonj2.test.driver.expectations.DeclareExpectation;
import org.apache.qpid.protonj2.test.driver.expectations.DetachExpectation;
import org.apache.qpid.protonj2.test.driver.expectations.DischargeExpectation;
import org.apache.qpid.protonj2.test.driver.expectations.DispositionExpectation;
import org.apache.qpid.protonj2.test.driver.expectations.EmptyFrameExpectation;
import org.apache.qpid.protonj2.test.driver.expectations.EndExpectation;
import org.apache.qpid.protonj2.test.driver.expectations.FlowExpectation;
import org.apache.qpid.protonj2.test.driver.expectations.OpenExpectation;
import org.apache.qpid.protonj2.test.driver.expectations.SaslChallengeExpectation;
import org.apache.qpid.protonj2.test.driver.expectations.SaslInitExpectation;
import org.apache.qpid.protonj2.test.driver.expectations.SaslMechanismsExpectation;
import org.apache.qpid.protonj2.test.driver.expectations.SaslOutcomeExpectation;
import org.apache.qpid.protonj2.test.driver.expectations.SaslResponseExpectation;
import org.apache.qpid.protonj2.test.driver.expectations.TransferExpectation;
import org.hamcrest.Matchers;
/**
* Class used to create test scripts using the {@link AMQPTestDriver}
*/
public abstract class ScriptWriter {
/**
* Implemented in the subclass this method returns the active AMQP test driver
* instance being used for the tests.
*
* @return the {@link AMQPTestDriver} to use for building a test script.
*/
public abstract AMQPTestDriver getDriver();
//----- AMQP Performative expectations
public AMQPHeaderExpectation expectAMQPHeader() {
AMQPHeaderExpectation expecting = new AMQPHeaderExpectation(AMQPHeader.getAMQPHeader(), getDriver());
getDriver().addScriptedElement(expecting);
return expecting;
}
public OpenExpectation expectOpen() {
OpenExpectation expecting = new OpenExpectation(getDriver());
getDriver().addScriptedElement(expecting);
return expecting;
}
public CloseExpectation expectClose() {
CloseExpectation expecting = new CloseExpectation(getDriver());
getDriver().addScriptedElement(expecting);
return expecting;
}
public BeginExpectation expectBegin() {
BeginExpectation expecting = new BeginExpectation(getDriver());
getDriver().addScriptedElement(expecting);
return expecting;
}
public EndExpectation expectEnd() {
EndExpectation expecting = new EndExpectation(getDriver());
getDriver().addScriptedElement(expecting);
return expecting;
}
public AttachExpectation expectAttach() {
AttachExpectation expecting = new AttachExpectation(getDriver());
getDriver().addScriptedElement(expecting);
return expecting;
}
public DetachExpectation expectDetach() {
DetachExpectation expecting = new DetachExpectation(getDriver());
getDriver().addScriptedElement(expecting);
return expecting;
}
public FlowExpectation expectFlow() {
FlowExpectation expecting = new FlowExpectation(getDriver());
getDriver().addScriptedElement(expecting);
return expecting;
}
public TransferExpectation expectTransfer() {
TransferExpectation expecting = new TransferExpectation(getDriver());
getDriver().addScriptedElement(expecting);
return expecting;
}
public DispositionExpectation expectDisposition() {
DispositionExpectation expecting = new DispositionExpectation(getDriver());
getDriver().addScriptedElement(expecting);
return expecting;
}
public EmptyFrameExpectation expectEmptyFrame() {
EmptyFrameExpectation expecting = new EmptyFrameExpectation(getDriver());
getDriver().addScriptedElement(expecting);
return expecting;
}
//----- Transaction expectations
public AttachExpectation expectCoordinatorAttach() {
AttachExpectation expecting = new AttachExpectation(getDriver());
expecting.withRole(Role.SENDER);
expecting.withCoordinator(isA(Coordinator.class));
expecting.withSource(notNullValue());
getDriver().addScriptedElement(expecting);
return expecting;
}
public DeclareExpectation expectDeclare() {
DeclareExpectation expecting = new DeclareExpectation(getDriver());
expecting.withHandle(notNullValue());
expecting.withDeliveryId(notNullValue());
expecting.withDeliveryTag(notNullValue());
expecting.withMessageFormat(Matchers.oneOf(null, 0, UnsignedInteger.ZERO));
getDriver().addScriptedElement(expecting);
return expecting;
}
public DischargeExpectation expectDischarge() {
DischargeExpectation expecting = new DischargeExpectation(getDriver());
expecting.withHandle(notNullValue());
expecting.withDeliveryId(notNullValue());
expecting.withDeliveryTag(notNullValue());
expecting.withMessageFormat(Matchers.oneOf(null, 0, UnsignedInteger.ZERO));
getDriver().addScriptedElement(expecting);
return expecting;
}
//----- SASL performative expectations
public AMQPHeaderExpectation expectSASLHeader() {
AMQPHeaderExpectation expecting = new AMQPHeaderExpectation(AMQPHeader.getSASLHeader(), getDriver());
getDriver().addScriptedElement(expecting);
return expecting;
}
public SaslMechanismsExpectation expectSaslMechanisms() {
SaslMechanismsExpectation expecting = new SaslMechanismsExpectation(getDriver());
getDriver().addScriptedElement(expecting);
return expecting;
}
public SaslInitExpectation expectSaslInit() {
SaslInitExpectation expecting = new SaslInitExpectation(getDriver());
getDriver().addScriptedElement(expecting);
return expecting;
}
public SaslChallengeExpectation expectSaslChallenge() {
SaslChallengeExpectation expecting = new SaslChallengeExpectation(getDriver());
getDriver().addScriptedElement(expecting);
return expecting;
}
public SaslResponseExpectation expectSaslResponse() {
SaslResponseExpectation expecting = new SaslResponseExpectation(getDriver());
getDriver().addScriptedElement(expecting);
return expecting;
}
public SaslOutcomeExpectation expectSaslOutcome() {
SaslOutcomeExpectation expecting = new SaslOutcomeExpectation(getDriver());
getDriver().addScriptedElement(expecting);
return expecting;
}
//----- Remote operations that happen while running the test script
public AMQPHeaderInjectAction remoteHeader(byte[] header) {
return new AMQPHeaderInjectAction(getDriver(), new AMQPHeader(header));
}
public AMQPHeaderInjectAction remoteHeader(AMQPHeader header) {
return new AMQPHeaderInjectAction(getDriver(), header);
}
public OpenInjectAction remoteOpen() {
return new OpenInjectAction(getDriver());
}
public CloseInjectAction remoteClose() {
return new CloseInjectAction(getDriver());
}
public BeginInjectAction remoteBegin() {
return new BeginInjectAction(getDriver());
}
public EndInjectAction remoteEnd() {
return new EndInjectAction(getDriver());
}
public AttachInjectAction remoteAttach() {
return new AttachInjectAction(getDriver());
}
public DetachInjectAction remoteDetach() {
return new DetachInjectAction(getDriver());
}
public DetachInjectAction remoteDetachLastCoordinatorLink() {
return new DetachLastCoordinatorInjectAction(getDriver());
}
public FlowInjectAction remoteFlow() {
return new FlowInjectAction(getDriver());
}
public TransferInjectAction remoteTransfer() {
return new TransferInjectAction(getDriver());
}
public DispositionInjectAction remoteDisposition() {
return new DispositionInjectAction(getDriver());
}
public DeclareInjectAction remoteDeclare() {
return new DeclareInjectAction(getDriver());
}
public DischargeInjectAction remoteDischarge() {
return new DischargeInjectAction(getDriver());
}
public EmptyFrameInjectAction remoteEmptyFrame() {
return new EmptyFrameInjectAction(getDriver());
}
public RawBytesInjectAction remoteBytes() {
return new RawBytesInjectAction(getDriver());
}
//----- Remote SASL operations that can be scripted during tests
public SaslInitInjectAction remoteSaslInit() {
return new SaslInitInjectAction(getDriver());
}
public SaslMechanismsInjectAction remoteSaslMechanisms() {
return new SaslMechanismsInjectAction(getDriver());
}
public SaslChallengeInjectAction remoteSaslChallenge() {
return new SaslChallengeInjectAction(getDriver());
}
public SaslResponseInjectAction remoteSaslResponse() {
return new SaslResponseInjectAction(getDriver());
}
public SaslOutcomeInjectAction remoteSaslOutcome() {
return new SaslOutcomeInjectAction(getDriver());
}
//----- SASL related test expectations
/**
* Creates all the scripted elements needed for a successful SASL Anonymous
* connection.
* <p>
* For this exchange the SASL header is expected which is responded to with the
* corresponding SASL header and an immediate SASL mechanisms frame that only
* advertises anonymous as the mechanism. It is expected that the remote will
* send a SASL init with the anonymous mechanism selected and the outcome is
* predefined as success. Once done the expectation is added for the AMQP
* header to arrive and a header response will be sent.
*/
public void expectSASLAnonymousConnect() {
expectSASLHeader().respondWithSASLHeader();
remoteSaslMechanisms().withMechanisms("ANONYMOUS").queue();
expectSaslInit().withMechanism("ANONYMOUS");
remoteSaslOutcome().withCode(SaslCode.OK).queue();
expectAMQPHeader().respondWithAMQPHeader();
}
/**
* Creates all the scripted elements needed for a successful SASL Plain
* connection.
* <p>
* For this exchange the SASL header is expected which is responded to with the
* corresponding SASL header and an immediate SASL mechanisms frame that only
* advertises plain as the mechanism. It is expected that the remote will
* send a SASL init with the plain mechanism selected and the outcome is
* predefined as success. Once done the expectation is added for the AMQP
* header to arrive and a header response will be sent.
*
* @param username
* The user name that is expected in the SASL Plain initial response.
* @param password
* The password that is expected in the SASL Plain initial response.
*/
public void expectSASLPlainConnect(String username, String password) {
expectSASLHeader().respondWithSASLHeader();
remoteSaslMechanisms().withMechanisms("PLAIN").queue();
expectSaslInit().withMechanism("PLAIN").withInitialResponse(saslPlainInitialResponse(username, password));
remoteSaslOutcome().withCode(SaslCode.OK).queue();
expectAMQPHeader().respondWithAMQPHeader();
}
/**
* Creates all the scripted elements needed for a successful SASL XOAUTH2
* connection.
* <p>
* For this exchange the SASL header is expected which is responded to with the
* corresponding SASL header and an immediate SASL mechanisms frame that only
* advertises XOAUTH2 as the mechanism. It is expected that the remote will
* send a SASL init with the XOAUTH2 mechanism selected and the outcome is
* predefined as success. Once done the expectation is added for the AMQP
* header to arrive and a header response will be sent.
*
* @param username
* The user name that is expected in the SASL initial response.
* @param password
* The password that is expected in the SASL initial response.
*/
public void expectSaslXOauth2Connect(String username, String password) {
expectSASLHeader().respondWithSASLHeader();
remoteSaslMechanisms().withMechanisms("XOAUTH2").queue();
expectSaslInit().withMechanism("XOAUTH2").withInitialResponse(saslXOauth2InitialResponse(username, password));
remoteSaslOutcome().withCode(SaslCode.OK).queue();
expectAMQPHeader().respondWithAMQPHeader();
}
/**
* Creates all the scripted elements needed for a failed SASL Plain
* connection.
* <p>
* For this exchange the SASL header is expected which is responded to with the
* corresponding SASL header and an immediate SASL mechanisms frame that only
* advertises plain as the mechanism. It is expected that the remote will
* send a SASL init with the plain mechanism selected and the outcome is
* predefined failing the exchange.
*
* @param saslCode
* The SASL code that indicates which failure the remote will be sent.
*/
public void expectFailingSASLPlainConnect(byte saslCode) {
expectFailingSASLPlainConnect(saslCode, "PLAIN");
}
/**
* Creates all the scripted elements needed for a failed SASL Plain
* connection.
* <p>
* For this exchange the SASL header is expected which is responded to with the
* corresponding SASL header and an immediate SASL mechanisms frame that only
* advertises plain as the mechanism. It is expected that the remote will
* send a SASL init with the plain mechanism selected and the outcome is
* predefined failing the exchange.
*
* @param saslCode
* The SASL code that indicates which failure the remote will be sent.
* @param offeredMechanisms
* The set of mechanisms that the server should offer in the SASL Mechanisms frame
*/
public void expectFailingSASLPlainConnect(byte saslCode, String... offeredMechanisms) {
if (!Arrays.asList(offeredMechanisms).contains("PLAIN")) {
throw new AssertionError("Expected offered mechanisms that contains the PLAIN mechanism");
}
expectSASLHeader().respondWithSASLHeader();
remoteSaslMechanisms().withMechanisms(offeredMechanisms).queue();
expectSaslInit().withMechanism("PLAIN");
if (saslCode <= 0 || saslCode > SaslCode.SYS_TEMP.ordinal()) {
throw new IllegalArgumentException("SASL Code should indicate a failure");
}
remoteSaslOutcome().withCode(SaslCode.valueOf(saslCode)).queue();
}
/**
* Creates all the scripted elements needed for a successful SASL EXTERNAL
* connection.
* <p>
* For this exchange the SASL header is expected which is responded to with the
* corresponding SASL header and an immediate SASL mechanisms frame that only
* advertises EXTERNAL as the mechanism. It is expected that the remote will
* send a SASL init with the EXTERNAL mechanism selected and the outcome is
* predefined as success. Once done the expectation is added for the AMQP
* header to arrive and a header response will be sent.
*/
public void expectSaslExternalConnect() {
expectSASLHeader().respondWithSASLHeader();
remoteSaslMechanisms().withMechanisms("EXTERNAL").queue();
expectSaslInit().withMechanism("EXTERNAL").withInitialResponse(new byte[0]);
remoteSaslOutcome().withCode(SaslCode.OK).queue();
expectAMQPHeader().respondWithAMQPHeader();
}
/**
* Creates all the scripted elements needed for a SASL exchange with the offered
* mechanisms but the client should fail if configured such that it cannot match
* any of those to its own available mechanisms.
*
* @param offeredMechanisms
* The set of SASL Mechanisms to advertise as available on the peer.
*/
public void expectSaslMechanismNegotiationFailure(String... offeredMechanisms) {
expectSASLHeader().respondWithSASLHeader();
remoteSaslMechanisms().withMechanisms(offeredMechanisms).queue();
}
/**
* Creates all the scripted elements needed for a SASL exchange with the offered
* mechanisms with the expectation that the client will respond with the provided
* mechanism and then the server will fail the exchange with the auth failed code.
*
* @param offeredMechanisms
* The set of SASL Mechanisms to advertise as available on the peer.
* @param chosenMechanism
* The SASL Mechanism that the client should select and respond with.
*/
public void expectSaslConnectThatAlwaysFailsAuthentication(String[] offeredMechanisms, String chosenMechanism) {
expectSASLHeader().respondWithSASLHeader();
remoteSaslMechanisms().withMechanisms(offeredMechanisms).queue();
expectSaslInit().withMechanism(chosenMechanism);
remoteSaslOutcome().withCode(SaslCode.AUTH).queue();
}
//----- Utility methods for tests writing raw scripted SASL tests
public byte[] saslPlainInitialResponse(String username, String password) {
byte[] usernameBytes = username.getBytes(StandardCharsets.UTF_8);
byte[] passwordBytes = password.getBytes(StandardCharsets.UTF_8);
byte[] initialResponse = new byte[usernameBytes.length+passwordBytes.length+2];
System.arraycopy(usernameBytes, 0, initialResponse, 1, usernameBytes.length);
System.arraycopy(passwordBytes, 0, initialResponse, 2 + usernameBytes.length, passwordBytes.length);
return initialResponse;
}
public byte[] saslXOauth2InitialResponse(String username, String password) {
byte[] usernameBytes = username.getBytes(StandardCharsets.UTF_8);
byte[] passwordBytes = password.getBytes(StandardCharsets.UTF_8);
byte[] initialResponse = new byte[usernameBytes.length+passwordBytes.length+20];
System.arraycopy("user=".getBytes(StandardCharsets.US_ASCII), 0, initialResponse, 0, 5);
System.arraycopy(usernameBytes, 0, initialResponse, 5, usernameBytes.length);
initialResponse[5 + usernameBytes.length] = 1;
System.arraycopy("auth=Bearer ".getBytes(StandardCharsets.US_ASCII), 0, initialResponse, 6+usernameBytes.length, 12);
System.arraycopy(passwordBytes, 0, initialResponse, 18 + usernameBytes.length, passwordBytes.length);
initialResponse[initialResponse.length - 2] = 1;
initialResponse[initialResponse.length - 1] = 1;
return initialResponse;
}
//----- Smart Scripted Response Actions
/**
* Creates a Begin response for the last session Begin that was received and fills in the Begin
* fields based on values from the remote. The caller can further customize the Begin that is
* emitted by using the various with methods to assign values to the fields in the Begin.
*
* @return a new {@link BeginInjectAction} that can be queued or sent immediately.
*
* @throws IllegalStateException if no Begin has yet been received from the remote.
*/
public BeginInjectAction respondToLastBegin() {
BeginInjectAction response = new BeginInjectAction(getDriver());
SessionTracker session = getDriver().sessions().getLastRemotelyOpenedSession();
if (session == null) {
throw new IllegalStateException("Cannot create response to Begin before one has been received.");
}
// Populate the response using data in the locally opened session, script can override this after return.
response.withRemoteChannel(session.getRemoteChannel());
return response;
}
/**
* Creates a Attach response for the last link Attach that was received and fills in the Attach
* fields based on values from the remote. The caller can further customize the Attach that is
* emitted by using the various with methods to assign values to the fields in the Attach.
*
* @return a new {@link AttachInjectAction} that can be queued or sent immediately.
*
* @throws IllegalStateException if no Attach has yet been received from the remote.
*/
public AttachInjectAction respondToLastAttach() {
AttachInjectAction response = new AttachInjectAction(getDriver());
final SessionTracker session = getDriver().sessions().getLastRemotelyOpenedSession();
final LinkTracker link = session.getLastRemotelyOpenedLink();
if (link == null) {
throw new IllegalStateException("Cannot create response to Attach before one has been received.");
}
if (link.isLocallyAttached()) {
throw new IllegalStateException("Cannot create response to Attach since a local Attach was already sent.");
}
// Populate the response using data in the locally opened link, script can override this after return.
response.onChannel(link.getSession().getLocalChannel());
response.withName(link.getName());
response.withRole(link.getRole());
response.withSndSettleMode(link.getRemoteSenderSettleMode());
response.withRcvSettleMode(link.getRemoteReceiverSettleMode());
if (link.getRemoteSource() != null) {
response.withSource(new Source(link.getRemoteSource()));
if (Boolean.TRUE.equals(link.getRemoteSource().getDynamic())) {
response.withSource().withAddress(UUID.randomUUID().toString());
}
}
if (link.getRemoteTarget() != null) {
response.withTarget(new Target(link.getRemoteTarget()));
if (Boolean.TRUE.equals(link.getRemoteTarget().getDynamic())) {
response.withTarget().withAddress(UUID.randomUUID().toString());
}
}
if (link.getRemoteCoordinator() != null) {
response.withTarget(new Coordinator(link.getRemoteCoordinator()));
}
if (response.getPerformative().getInitialDeliveryCount() == null) {
if (link.isSender()) {
response.withInitialDeliveryCount(0);
}
}
return response;
}
//----- Out of band script actions for user code
public ExecuteUserCodeAction execute(Runnable action) {
return new ExecuteUserCodeAction(getDriver(), action);
}
//----- Immediate operations performed outside the test script
public void fire(AMQPHeader header) {
getDriver().sendHeader(header);
}
public void fireAMQP(DescribedType performative) {
getDriver().sendAMQPFrame(0, performative, null);
}
public void fireSASL(DescribedType performative) {
getDriver().sendSaslFrame(0, performative);
}
}