AdHocCommandIntegrationTest.java

/**
 *
 * Copyright 2023-2024 Florian Schmaus
 *
 * 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 org.jivesoftware.smackx.commands;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

import org.jivesoftware.smack.SmackException.NoResponseException;
import org.jivesoftware.smack.SmackException.NotConnectedException;
import org.jivesoftware.smack.XMPPException.XMPPErrorException;
import org.jivesoftware.smack.packet.StanzaError;
import org.jivesoftware.smackx.commands.packet.AdHocCommandData;
import org.jivesoftware.smackx.commands.packet.AdHocCommandDataBuilder;
import org.jivesoftware.smackx.commands.packet.AdHocCommandDataBuilder.NextStage;
import org.jivesoftware.smackx.commands.packet.AdHocCommandDataBuilder.PreviousStage;
import org.jivesoftware.smackx.xdata.FormField;
import org.jivesoftware.smackx.xdata.form.FillableForm;
import org.jivesoftware.smackx.xdata.form.SubmitForm;
import org.jivesoftware.smackx.xdata.packet.DataForm;

import org.igniterealtime.smack.inttest.AbstractSmackIntegrationTest;
import org.igniterealtime.smack.inttest.SmackIntegrationTestEnvironment;
import org.igniterealtime.smack.inttest.annotations.SmackIntegrationTest;
import org.igniterealtime.smack.inttest.annotations.SpecificationReference;

@SpecificationReference(document = "XEP-0050")
public class AdHocCommandIntegrationTest extends AbstractSmackIntegrationTest {

    public AdHocCommandIntegrationTest(SmackIntegrationTestEnvironment environment) {
        super(environment);
    }

    @SmackIntegrationTest
    public void singleStageAdHocCommandTest() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
        AdHocCommandManager manOne = AdHocCommandManager.getInstance(conOne);
        AdHocCommandManager manTwo = AdHocCommandManager.getInstance(conTwo);

        String commandNode = "test-list";
        String commandName = "Return a list for testing purposes";
        AdHocCommandHandlerFactory factory = (String node, String name, String sessionId) -> {
            return new AdHocCommandHandler.SingleStage(node, name, sessionId) {
                @Override
                public AdHocCommandData executeSingleStage(AdHocCommandDataBuilder response) {
                    FormField field = FormField.textPrivateBuilder("my-field").build();
                    DataForm form = DataForm.builder(DataForm.Type.result).addField(field).build();

                    response.setForm(form);

                    return response.build();
                }
            };
        };
        manOne.registerCommand(commandNode, commandName, factory);
        try {
            AdHocCommand command = manTwo.getRemoteCommand(conOne.getUser(), commandNode);

            AdHocCommandResult result = command.execute();
            AdHocCommandData response = result.getResponse();
            DataForm form = response.getForm();
            FormField field = form.getField("my-field");
            assertNotNull(field, "Expected a field named 'my-field' to exist in the form that " +
                conTwo.getUser() + " obtained from " + conOne.getUser() + "'s command node '" + commandNode +
                "' (but it did not).");
        } finally {
            manOne.unregisterCommand(commandNode);
        }
    }

    private static class MyMultiStageAdHocCommandServer extends AdHocCommandHandler {

        private Integer a;
        private Integer b;

        private static DataForm createDataForm(String variableName) {
            FormField field = FormField.textSingleBuilder(variableName).setRequired().build();
            return DataForm.builder(DataForm.Type.form)
               .setTitle("Variable " + variableName)
               .setInstructions("Please provide an integer variable " + variableName)
               .addField(field)
               .build();
        }

        private static DataForm createDataFormOp() {
            FormField field = FormField.listSingleBuilder("op")
                            .setLabel("Arithmetic Operation")
                            .setRequired()
                            .addOption("+")
                            .addOption("-")
                            .build();
            return DataForm.builder(DataForm.Type.form)
               .setTitle("Operation")
               .setInstructions("Please select the arithmetic operation to be performed with a and b")
               .addField(field)
               .build();
        }
        private static final DataForm dataFormAskingForA = createDataForm("a");
        private static final DataForm dataFormAskingForB = createDataForm("b");
        private static final DataForm dataFormAskingForOp = createDataFormOp();

        MyMultiStageAdHocCommandServer(String node, String name, String sessionId) {
            super(node, name, sessionId);
        }

        @Override
        protected AdHocCommandData execute(AdHocCommandDataBuilder response) throws XMPPErrorException {
            return response.setForm(dataFormAskingForA).setStatusExecuting(PreviousStage.none,
                            NextStage.nonFinal).build();
        }

        private static Integer extractIntegerField(SubmitForm form, String fieldName) throws XMPPErrorException {
            FormField field = form.getField(fieldName);
            if (field == null)
                throw newBadRequestException("Submitted form does not contain a field of name " + fieldName);

            String fieldValue = field.getFirstValue();
            if (fieldValue == null)
                throw newBadRequestException("Submitted form contains field of name " + fieldName + " without value");

            try {
                return Integer.parseInt(fieldValue);
            } catch (NumberFormatException e) {
                throw newBadRequestException("Submitted form contains field of name " + fieldName + " with value " + fieldValue + " that is not an integer");
            }
        }

        @Override
        protected AdHocCommandData next(AdHocCommandDataBuilder response, SubmitForm submittedForm)
                        throws XMPPErrorException {
            DataForm form;
            switch (getCurrentStage()) {
            case 2:
                a = extractIntegerField(submittedForm, "a");
                form = dataFormAskingForB;
                response.setStatusExecuting(PreviousStage.exists, NextStage.nonFinal);
                break;
            case 3:
                b = extractIntegerField(submittedForm, "b");
                form = dataFormAskingForOp;
                response.setStatusExecuting(PreviousStage.exists, NextStage.isFinal);
                break;
            case 4:
                // Ad-Hoc Commands particularity: Can get to 'complete' via 'next'.
                return complete(response, submittedForm);
            default:
                throw new IllegalStateException();
            }

            return response.setForm(form).build();
        }

        @Override
        protected AdHocCommandData complete(AdHocCommandDataBuilder response, SubmitForm submittedForm)
                        throws XMPPErrorException {
            if (getCurrentStage() != 4) {
                throw new IllegalStateException();
            }

            if (a == null || b == null) {
                throw new IllegalStateException();
            }

            String op = submittedForm.getField("op").getFirstValue();

            int result;
            switch (op) {
            case "+":
                result = a + b;
                break;
            case "-":
                result = a - b;
                break;
            default:
                throw newBadRequestException("Submitted operation " + op + " is neither + nor -");
            }

            response.setStatusCompleted();

            FormField field = FormField.textSingleBuilder("result").setValue(result).build();
            DataForm form = DataForm.builder(DataForm.Type.result).setTitle("Result").addField(field).build();

            return response.setForm(form).build();
        }

        @Override
        protected AdHocCommandData prev(AdHocCommandDataBuilder response) throws XMPPErrorException {
            switch (getCurrentStage()) {
            case 1:
                return execute(response);
            case 2:
                return response.setForm(dataFormAskingForA)
                                .setStatusExecuting(PreviousStage.exists, NextStage.nonFinal)
                                .build();
            case 3:
                return response.setForm(dataFormAskingForB)
                                .setStatusExecuting(PreviousStage.exists, NextStage.isFinal)
                                .build();
            default:
                throw new IllegalStateException();
            }
        }

        @Override
        public void cancel() {
        }

    }

    @SmackIntegrationTest
    public void multiStageAdHocCommandTest() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
        AdHocCommandManager manOne = AdHocCommandManager.getInstance(conOne);
        AdHocCommandManager manTwo = AdHocCommandManager.getInstance(conTwo);

        String commandNode = "my-multi-stage-command";
        String commandName = "An example multi-sage ad-hoc command";
        AdHocCommandHandlerFactory factory = (String node, String name, String sessionId) -> {
            return new MyMultiStageAdHocCommandServer(node, name, sessionId);
        };
        manOne.registerCommand(commandNode, commandName, factory);

        try {
            AdHocCommand command = manTwo.getRemoteCommand(conOne.getUser(), commandNode);

            AdHocCommandResult.StatusExecuting result = command.execute().asExecutingOrThrow();

            FillableForm form = result.getFillableForm();
            form.setAnswer("a", 42);

            SubmitForm submitForm = form.getSubmitForm();


            result = command.next(submitForm).asExecutingOrThrow();

            form = result.getFillableForm();
            form.setAnswer("b", 23);

            submitForm = form.getSubmitForm();


            result = command.next(submitForm).asExecutingOrThrow();

            form = result.getFillableForm();
            form.setAnswer("op", "+");

            submitForm = form.getSubmitForm();

            AdHocCommandResult.StatusCompleted completed = command.complete(submitForm).asCompletedOrThrow();

            String operationResult = completed.getResponse().getForm().getField("result").getFirstValue();
            assertEquals("65", operationResult,
                "Unexpected value in the field 'result' from the command result that " + conTwo.getUser() +
                " received from " + conOne.getUser() + " after completing a multi-staged ad-hoc command on node '" +
                commandNode + "'.");
        } finally {
            manTwo.unregisterCommand(commandNode);
        }
    }

    @SmackIntegrationTest
    public void multiStageWithPrevAdHocCommandTest() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
        AdHocCommandManager manOne = AdHocCommandManager.getInstance(conOne);
        AdHocCommandManager manTwo = AdHocCommandManager.getInstance(conTwo);

        String commandNode = "my-multi-stage-with-prev-command";
        String commandName = "An example multi-sage ad-hoc command";
        AdHocCommandHandlerFactory factory = (String node, String name, String sessionId) -> {
            return new MyMultiStageAdHocCommandServer(node, name, sessionId);
        };
        manOne.registerCommand(commandNode, commandName, factory);

        try {
            AdHocCommand command = manTwo.getRemoteCommand(conOne.getUser(), commandNode);

            AdHocCommandResult.StatusExecuting result = command.execute().asExecutingOrThrow();

            FillableForm form = result.getFillableForm();
            form.setAnswer("a", 42);

            SubmitForm submitForm = form.getSubmitForm();

            command.next(submitForm).asExecutingOrThrow();


            // Ups, I wanted a different value for 'a', lets execute 'prev' to get back to the previous stage.
            result = command.prev().asExecutingOrThrow();

            form = result.getFillableForm();
            form.setAnswer("a", 77);

            submitForm = form.getSubmitForm();


            result = command.next(submitForm).asExecutingOrThrow();

            form = result.getFillableForm();
            form.setAnswer("b", 23);

            submitForm = form.getSubmitForm();


            result = command.next(submitForm).asExecutingOrThrow();

            form = result.getFillableForm();
            form.setAnswer("op", "+");

            submitForm = form.getSubmitForm();

            AdHocCommandResult.StatusCompleted completed = command.complete(submitForm).asCompletedOrThrow();

            String operationResult = completed.getResponse().getForm().getField("result").getFirstValue();
            assertEquals("100", operationResult,
                "Unexpected value in the field 'result' from the command result that " + conTwo.getUser() +
                " received from " + conOne.getUser() + " after completing a multi-staged ad-hoc command on node '" +
                commandNode + "'.");
        } finally {
            manTwo.unregisterCommand(commandNode);
        }
    }

    @SmackIntegrationTest
    public void multiStageInvalidArgAdHocCommandTest() throws NoResponseException, XMPPErrorException, NotConnectedException, InterruptedException {
        AdHocCommandManager manOne = AdHocCommandManager.getInstance(conOne);
        AdHocCommandManager manTwo = AdHocCommandManager.getInstance(conTwo);

        String commandNode = "my-multi-stage-invalid-arg-command";
        String commandName = "An example multi-sage ad-hoc command";
        AdHocCommandHandlerFactory factory = (String node, String name, String sessionId) -> {
            return new MyMultiStageAdHocCommandServer(node, name, sessionId);
        };
        manOne.registerCommand(commandNode, commandName, factory);

        try {
            AdHocCommand command = manTwo.getRemoteCommand(conOne.getUser(), commandNode);

            AdHocCommandResult.StatusExecuting result = command.execute().asExecutingOrThrow();

            FillableForm form = result.getFillableForm();
            form.setAnswer("a", "forty-two");

            SubmitForm submitForm = form.getSubmitForm();

            XMPPErrorException exception = assertThrows(XMPPErrorException.class, () -> command.next(submitForm));
            assertEquals(StanzaError.Condition.bad_request, exception.getStanzaError().getCondition(),
        "Unexpected error condition received after " + conTwo.getUser() + " supplied an invalid argument " +
                "to the command node '" + commandNode + "' of " + conOne.getUser());
        } finally {
            manTwo.unregisterCommand(commandNode);
        }
    }
}