Skip to content
Snippets Groups Projects
Commit 35852e2f authored by Alexander Lovett's avatar Alexander Lovett
Browse files

Merge branch 'develop' into 'master'

V2.0.14.RELEASE

See merge request alexander.lovett/looking-glass-service!13
parents 3c174fcd a5f5da99
Branches main share
No related tags found
No related merge requests found
Showing
with 1709 additions and 167 deletions
package org.geant.lgservice.ecmr;
import static org.junit.Assert.assertEquals;
import org.geant.lgservice.pojos.Command;
import org.geant.lgservice.pojos.Router;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class CallableCommandExecutorTest {
public static final String TEST_ROUTER = "test-router.geant.net";
public static final String TEST_COMMAND = "ping 25";
@Test
public void testGetCommandWithArgumentsXML() {
Router router = new Router();
router.setName(TEST_ROUTER);
Command command = new Command();
command.setValue(TEST_COMMAND);
command.setRequiresParams(true);
CallableCommandExecutor callable = new CallableCommandExecutor(router, command, "param1", true, null);
assertEquals("ping 25 param1 | display xml", callable.getCommandWithArguments());
}
@Test
public void testGetCommandWithoutArgumentsXML() {
Router router = new Router();
router.setName(TEST_ROUTER);
Command command = new Command();
command.setValue(TEST_COMMAND);
command.setRequiresParams(false);
CallableCommandExecutor callable = new CallableCommandExecutor(router, command, null, true, null);
assertEquals("ping 25 | display xml", callable.getCommandWithArguments());
}
@Test
public void testGetCommandWithoutArguments() {
Router router = new Router();
router.setName(TEST_ROUTER);
Command command = new Command();
command.setValue(TEST_COMMAND);
command.setRequiresParams(false);
CallableCommandExecutor callable = new CallableCommandExecutor(router, command, null, false, null);
assertEquals("ping 25", callable.getCommandWithArguments());
}
}
package org.geant.lgservice.infrastructure.ssh;
import ch.ethz.ssh2.Connection;
import ch.ethz.ssh2.Session;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.SneakyThrows;
import org.geant.lgservice.exceptions.TechnicalException;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.geant.lgservice.infrastructure.ssh.SSHHost.ConnectionStateListener.State.DISCONNECTED;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.when;
public class SSHHostTest {
private static final String HOSTNAME = "localhost";
private static final String RESPONSE_STRING = "A Response String";
private Connection connection = mock(Connection.class);
private Session session = mock(Session.class);
private SSHHost subject;
private PipedInputStream response;
private PipedOutputStream responseSource;
@Before
@SneakyThrows
public void setup() {
reset(connection);
when(connection.isAuthenticationComplete()).thenReturn(true);
when(connection.openSession()).thenReturn(session);
when(connection.getHostname()).thenReturn(HOSTNAME);
responseSource = new PipedOutputStream();
response = new PipedInputStream(responseSource);
when(session.getStdout()).thenReturn(response);
when(session.getStderr()).thenReturn(response);
responseSource.write(RESPONSE_STRING.getBytes());
responseSource.close();
subject = SSHHost.fromConnection(connection);
}
@Test
public void commandExecutedSuccessfully() {
when(session.getExitStatus()).thenReturn(0);
assertThat(subject.execute("some command")).isEqualTo(RESPONSE_STRING);
}
@Test
public void commandExitedWithNonZeroStatus() {
when(session.getExitStatus()).thenReturn(1);
assertThat(subject.execute("some command")).isEqualTo(RESPONSE_STRING);
}
@Test(expected = TechnicalException.class)
public void createWithUnAuthenticatedConnection() {
reset(connection);
when(connection.isAuthenticationComplete()).thenReturn(false);
SSHHost.fromConnection(connection);
fail("subject should have thrown an exception");
}
@Test(expected = TechnicalException.class)
public void sessionThrowsIOException() {
try {
Mockito.doThrow(new IOException()).when(session).execCommand(any(String.class));
} catch (IOException e) {
fail("Mock should not throw IOException", e);
}
subject.execute("some command");
fail("subject should have thrown an exception");
}
@Getter
@Setter
@NoArgsConstructor
private static class StateListenerOutput {
private String host;
private SSHHost.ConnectionStateListener.State state;
}
@Test
public void connectionStateListenersTriggered() {
final StateListenerOutput listenerOutput = new StateListenerOutput();
subject.withConnectionStateListener((host, state) -> {
listenerOutput.setHost(host.hostname());
listenerOutput.setState(state);
});
subject.connectionLost(new IOException());
assertThat(listenerOutput.getHost()).isEqualTo(HOSTNAME);
assertThat(listenerOutput.getState()).isEqualTo(DISCONNECTED);
}
}
package org.geant.lgservice.integration;
import ch.ethz.ssh2.Connection;
import ch.ethz.ssh2.Session;
import com.alexlovett.jgivenextension.ExtendedScenarioTest;
import com.github.tomakehurst.wiremock.junit.WireMockClassRule;
import com.tngtech.jgiven.integration.spring.SpringStageCreator;
import lombok.SneakyThrows;
import org.geant.lgservice.LookingGlassServiceApplication;
import org.geant.lgservice.infrastructure.ssh.ConnectionFactory;
import org.geant.lgservice.infrastructure.ssh.ConnectionManager;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.junit4.SpringRunner;
import java.lang.reflect.Field;
import java.util.Map;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.reset;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import static org.springframework.test.annotation.DirtiesContext.ClassMode.BEFORE_CLASS;
@RunWith(SpringRunner.class)
@SpringBootTest(
webEnvironment = RANDOM_PORT,
classes = {LookingGlassServiceApplication.class, Config.class}
)
@TestPropertySource(properties = {
"server.ssl.enabled=false"
})
@DirtiesContext(classMode = BEFORE_CLASS)
public abstract class BaseIT <GIVEN, WHEN, THEN> extends ExtendedScenarioTest<GIVEN, WHEN, THEN> implements BeanFactoryAware {
@LocalServerPort
protected int port;
@Autowired
private PortSink portSink;
@Autowired
private ConnectionFactory connectionFactory;
@Autowired
private ConnectionManager connectionManager;
@Autowired
private Connection connection;
@Autowired
private Session session;
interface PortSink {
void sink(int port);
}
@ClassRule
public static final WireMockClassRule INVENTORY_PROVIDER = new WireMockClassRule(wireMockConfig().dynamicPort());
@Before
@SneakyThrows
public void setup() {
portSink.sink(port);
INVENTORY_PROVIDER.resetAll();
reset(connectionFactory);
reset(connection);
reset(session);
Mockito.when(connectionFactory.createConnection(any(String.class))).thenReturn(connection);
Mockito.when(connection.openSession()).thenReturn(session);
Mockito.when(connection.isAuthenticationComplete()).thenReturn(true);
Field hostField = ConnectionManager.class.getDeclaredField("hosts");
hostField.setAccessible(true);
Map hosts = (Map)hostField.get(connectionManager);
hosts.clear();
}
@Override
public void setBeanFactory( BeanFactory beanFactory ) {
getScenario().setStageCreator( beanFactory.getBean( SpringStageCreator.class ) );
}
@BeforeClass
public static void setupClass() {
System.setProperty("inventoryprovider.baseUrl", String.format("http://localhost:%s/lg/", INVENTORY_PROVIDER.port()));
}
}
package org.geant.lgservice.integration;
import ch.ethz.ssh2.Connection;
import ch.ethz.ssh2.Session;
import com.tngtech.jgiven.integration.spring.EnableJGiven;
import org.geant.lgservice.infrastructure.ssh.ConnectionFactory;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.support.HttpRequestWrapper;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.util.UriComponentsBuilder;
import java.io.IOException;
import java.net.URI;
import static java.util.Optional.of;
import static org.geant.lgservice.integration.Context.withDefaults;
@Configuration
@EnableJGiven
@ComponentScan(basePackages = { "org.geant.lgservice.integration"})
@MockBean({
ConnectionFactory.class,
Connection.class,
Session.class
})
public class Config {
@Bean
public RestTemplate restTemplate(PortInjectingInterceptor interceptor) {
return new RestTemplateBuilder()
.rootUri("http://localhost")
.interceptors(interceptor)
.messageConverters(new MappingJackson2HttpMessageConverter())
.build();
}
@Bean
PortInjectingInterceptor portInjector() {
return new PortInjectingInterceptor();
}
static class PortInjectingInterceptor implements ClientHttpRequestInterceptor, BaseIT.PortSink {
private int port;
@Override
public ClientHttpResponse intercept(
HttpRequest httpRequest,
byte[] bytes,
ClientHttpRequestExecution clientHttpRequestExecution) throws IOException {
HttpRequest req = new HttpRequestWrapper(httpRequest) {
@Override
public URI getURI() {
return of(port)
.map(port -> UriComponentsBuilder.fromUri(super.getURI())
.host("localhost")
.port(port)
.build()
.toUri())
.get();
}
};
return clientHttpRequestExecution.execute(req, bytes);
}
@Override
public void sink(int port) {
this.port = port;
}
}
@Bean
public Context context(){
return withDefaults();
}
@Bean
public MockMvc mockMvc(WebApplicationContext context) {
return MockMvcBuilders.webAppContextSetup(context)
.apply(SecurityMockMvcConfigurers.springSecurity())
.build();
}
}
package org.geant.lgservice.integration;
import ch.ethz.ssh2.Connection;
import ch.ethz.ssh2.Session;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.stubbing.StubMapping;
import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.geant.lgservice.security.LGUser;
import org.mockito.Mockito;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static org.geant.lgservice.integration.Context.Router.INTERNAL;
import static org.geant.lgservice.integration.Context.Router.PUBLIC;
import static org.mockito.Mockito.mock;
@Getter
@Builder
public class Context {
@Getter
@RequiredArgsConstructor
enum Router {
PUBLIC("mx2.tal.ee.geant.net"),
INTERNAL("mx1.cbg.uk.geant.net");
private final String host;
}
private Router router;
private String command;
private LGUser user;
private StubMapping inventoryProviderMapping;
private int responseStatus;
private SubmitResponseBody submitResponseBody;
private Collection<CommandGroup> commandResponseBody;
public Context withPublicRouter() {
this.router = PUBLIC;
return this;
}
public Context withInternalRouter() {
this.router = INTERNAL;
return this;
}
public Context withCommand(String command) {
this.command = command;
return this;
}
public Context withUser(LGUser user) {
this.user = user;
return this;
}
public Context withInventoryProviderStub(StubMapping mapping) {
this.inventoryProviderMapping = mapping;
return this;
}
public Context withResponseStatus(int responseStatus) {
this.responseStatus = responseStatus;
return this;
}
public Context withSubmitResponseBody(SubmitResponseBody submitResponseBody) {
this.submitResponseBody = submitResponseBody;
return this;
}
public Context withCommandResponseBody(Collection<CommandGroup> commandResponseBody) {
this.commandResponseBody = commandResponseBody;
return this;
}
public String getRouterHost() {
return router.getHost();
}
@SneakyThrows
public static Context withDefaults() {
return Context.builder()
.inventoryProviderMapping(stubFor(get("/lg/routers/all")
.willReturn(WireMock.aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBodyFile("routers.json"))))
.build()
.withPublicRouter()
.withCommand("show config");
}
@JsonIgnoreProperties(ignoreUnknown = true)
public static class SubmitResponseBody {
private final Map<String, CommandResult> output;
@JsonCreator
public SubmitResponseBody(@JsonProperty("output") Map<String, CommandResult> output) {
this.output = output;
}
public CommandResult result() {
return output.values()
.stream()
.findFirst()
.orElseThrow(() -> new AssertionError("There should be command result in the response"));
}
@Getter
public static class CommandResult {
private String routerName;
@JsonProperty("commandResult")
private String response;
private Long executionTime;
}
}
@Getter
public static class CommandGroup {
private List<Command> commands;
private String name;
@Getter
public static class Command {
private String name;
private String value;
private String description;
private boolean requiresParams;
}
}
}
package org.geant.lgservice.integration.scenario;
import org.geant.lgservice.integration.BaseIT;
import org.junit.Test;
import static org.geant.lgservice.integration.scenario.Then.ThenResponse.ThenCommandResponse.UserType.EXTERNAL;
import static org.geant.lgservice.integration.scenario.Then.ThenResponse.ThenCommandResponse.UserType.INTERNAL;
import static org.geant.lgservice.integration.scenario.Then.ThenResponse.ThenCommandResponse.UserType.PUBLIC;
public class GetCommandsIT extends BaseIT<Given, When, Then> {
@Test
public void anonymous_user_requests_commands() {
given()
.a().user()
.that().is().anonymous();
when()
.a().request_for_available_commands_is_sent();
then()
.the().response()
.is().ok()
.and().has_commands()
.that().are_for_$_users(PUBLIC)
.and().are_not_for_$_users(EXTERNAL)
.and().are_not_for_$_users(INTERNAL);
}
@Test
public void external_user_requests_commands() {
given()
.a().user()
.that().is().logged_in()
.but().does_not_have_internal_access();
when()
.a().request_for_available_commands_is_sent();
then()
.the().response()
.is().ok()
.and().has_commands()
.that().are_for_$_users(PUBLIC)
.and().are_for_$_users(EXTERNAL)
.and().are_not_for_$_users(INTERNAL);
}
@Test
public void internal_user_requests_commands() {
given()
.a().user()
.that().is().logged_in();
when()
.a().request_for_available_commands_is_sent();
then()
.the().response()
.is().ok()
.and().has_commands()
.that().are_for_$_users(PUBLIC)
.and().are_for_$_users(EXTERNAL)
.and().are_for_$_users(INTERNAL);
}
}
package org.geant.lgservice.integration.scenario;
import ch.ethz.ssh2.Connection;
import ch.ethz.ssh2.Session;
import com.alexlovett.jgivenextension.ExtendedStage;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.http.ResponseDefinition;
import com.github.tomakehurst.wiremock.stubbing.StubMapping;
import com.tngtech.jgiven.annotation.AfterStage;
import com.tngtech.jgiven.annotation.BeforeScenario;
import com.tngtech.jgiven.annotation.ExpectedScenarioState;
import com.tngtech.jgiven.annotation.ProvidedScenarioState;
import com.tngtech.jgiven.integration.spring.JGivenStage;
import lombok.SneakyThrows;
import org.geant.lgservice.infrastructure.ssh.ConnectionFactory;
import org.geant.lgservice.integration.BaseIT;
import org.geant.lgservice.integration.Context;
import org.geant.lgservice.security.LGUser;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.Collections;
import static com.github.tomakehurst.wiremock.client.WireMock.get;
import static org.geant.lgservice.integration.Context.withDefaults;
import static org.geant.lgservice.security.LGUser.Group.EXTERNAL;
import static org.geant.lgservice.security.LGUser.Group.INTERNAL;
import static org.mockito.ArgumentMatchers.any;
@JGivenStage
public class Given extends ExtendedStage<Given> {
@ProvidedScenarioState
private Context context;
@Autowired
@ProvidedScenarioState
private ConnectionFactory connectionFactory;
@Autowired
@ProvidedScenarioState
private Session session;
@BeforeScenario
public void setup() {
context = withDefaults();
}
public GivenUser user() {
return nest(GivenUser.class);
}
public static class GivenUser extends NestedStage<GivenUser, Given> {
@ExpectedScenarioState
private Context context;
public GivenUser anonymous() {
return self();
}
public GivenUser logged_in() {
context.withUser(
LGUser.builder()
.federatedUser("1")
.givenName("barry")
.surname("scott")
.mail("barry.scott@geant.org")
.group(INTERNAL)
.group(EXTERNAL)
.build());
return self();
}
public GivenUser does_not_have_internal_access() {
LGUser user = context.getUser();
context.withUser(
LGUser.builder()
.federatedUser(user.getFederatedUser())
.givenName(user.getGivenName())
.surname(user.getSurname())
.mail(user.getMail())
.group(EXTERNAL)
.build());
return self();
}
}
public GivenRouter router() {
return nest(GivenRouter.class);
}
public static class GivenRouter extends NestedStage<GivenRouter, Given> {
@ExpectedScenarioState
private Context context;
@Autowired
@ExpectedScenarioState
private ConnectionFactory connectionFactory;
@ExpectedScenarioState
private Session session;
public GivenRouter publicly_accessible() {
context.withPublicRouter();
return self();
}
public GivenRouter internal() {
context.withInternalRouter();
return self();
}
@SneakyThrows
public GivenRouter ok() {
PipedOutputStream responseSource = new PipedOutputStream();
PipedInputStream response = new PipedInputStream(responseSource);
Mockito.when(session.getStdout()).thenReturn(response);
Mockito.when(session.getExitStatus())
.thenReturn(0);
Mockito.doAnswer(invocation -> {
responseSource.write(invocation.getArgument(0, String.class).getBytes());
responseSource.flush();
responseSource.close();
return null;
}).when(session).execCommand(any(String.class));
return self();
}
}
public GivenInventoryProvider inventory_provider() {
return nest(GivenInventoryProvider.class);
}
public static class GivenInventoryProvider extends NestedStage<GivenInventoryProvider, Given> {
@ExpectedScenarioState
private Context context;
public GivenInventoryProvider returns_(int status) {
context.withInventoryProviderStub(get("/lg/routers/all")
.willReturn(WireMock.aResponse()
.withStatus(status))
.build());
return self();
}
public GivenInventoryProvider bad_body() {
StubMapping mapping = context.getInventoryProviderMapping();
ResponseDefinition response = mapping.getResponse();
mapping.setResponse(WireMock.aResponse().withStatus(response.getStatus())
.withBody("{\"This is not valid response body\":true").build());
return self();
}
}
@AfterStage
public void setupMocks() {
BaseIT.INVENTORY_PROVIDER.resetAll();
BaseIT.INVENTORY_PROVIDER.addStubMapping(context.getInventoryProviderMapping());
}
}
package org.geant.lgservice.integration.scenario;
import org.geant.lgservice.integration.BaseIT;
import org.junit.Test;
public class SubmitCommandRequestIT extends BaseIT<Given, When, Then> {
@Test
public void anonymous_user_sends_command_to_public_router(){
given()
.a().user()
.that().is().anonymous().backup()
.and().a().router()
.that().is().publicly_accessible()
.and().is().ok();
when()
.a().request_to_the_router_is_sent();
then()
.the().command_is_executed()
.and().the().result_is_returned();
}
@Test
public void anonymous_user_sends_command_to_internal_router() {
given()
.a().user()
.that().is().anonymous().backup()
.and().a().router()
.that().is().internal();
when()
.a().request_to_the_router_is_sent();
then()
.the().response()
.is().an().error()
.with_a().status(401);
}
@Test
public void logged_in_user_sends_command_to_public_router() {
given()
.a().user()
.that().is().logged_in()
.backup()
.and().a().router()
.that().is().publicly_accessible()
.and().is().ok();
when()
.a().request_to_the_router_is_sent();
then()
.the().command_is_executed()
.and().the().result_is_returned();
}
@Test
public void logged_in_user_sends_command_to_internal_router() {
given()
.a().user()
.that().is().logged_in()
.backup()
.and().a().router()
.that().is().internal()
.and().is().ok();
when()
.a().request_to_the_router_is_sent();
then()
.the().command_is_executed()
.and().the().result_is_returned();
}
@Test
public void logged_in_user_without_internal_access_sends_command_to_internal_router() {
given()
.a().user()
.that().is().logged_in()
.but().does_not_have_internal_access()
.backup()
.and().a().router()
.that().is().internal();
when()
.a().request_to_the_router_is_sent();
then()
.the().response()
.is().an().error()
.with_a().status(403);
}
@Test
public void inventory_provider_returns_client_error() {
given()
.a().user()
.that().is().anonymous()
.backup()
.and().a().router()
.that().is().publicly_accessible()
.backup()
.and().inventory_provider()
.returns_(400);
when()
.a().request_to_the_router_is_sent();
then()
.the().response()
.is().an().error()
.with().status(500);
}
@Test
public void inventory_provider_returns_server_error() {
given()
.a().user()
.that().is().anonymous()
.backup()
.and().a().router()
.that().is().publicly_accessible()
.backup()
.and().inventory_provider()
.returns_(500);
when()
.a().request_to_the_router_is_sent();
then()
.the().response()
.is().an().error()
.with().status(500);
}
@Test
public void inventory_provider_returns_invalid_body() {
given()
.a().user()
.that().is().anonymous()
.backup()
.and().a().router()
.that().is().publicly_accessible()
.backup()
.and().inventory_provider()
.returns_(200)
.with().a().bad_body();
when()
.a().request_to_the_router_is_sent();
then()
.the().response()
.is().an().error()
.with().status(500);
}
}
package org.geant.lgservice.integration.scenario;
import ch.ethz.ssh2.Session;
import com.alexlovett.jgivenextension.ExtendedStage;
import com.tngtech.jgiven.annotation.ExpectedScenarioState;
import com.tngtech.jgiven.integration.spring.JGivenStage;
import lombok.SneakyThrows;
import org.geant.lgservice.integration.Context;
import org.mockito.ArgumentCaptor;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@JGivenStage
public class Then extends ExtendedStage<Then> {
@ExpectedScenarioState
private Context context;
@ExpectedScenarioState
private Session session;
@SneakyThrows
public Then command_is_executed() {
ArgumentCaptor<String> command = ArgumentCaptor.forClass(String.class);
verify(session, times(1))
.execCommand(command.capture());
assertThat(command.getValue()).isEqualTo(context.getCommand());
return self();
}
public Then result_is_returned() {
Context.SubmitResponseBody submitResponseBody = context.getSubmitResponseBody();
assertThat(submitResponseBody).isNotNull();
Context.SubmitResponseBody.CommandResult result = submitResponseBody.result();
assertThat(result).isNotNull();
assertThat(result.getRouterName()).isEqualTo(context.getRouterHost());
assertThat(result.getResponse()).isEqualTo(context.getCommand());
return self();
}
public ThenResponse response() {
return nest(ThenResponse.class);
}
public static class ThenResponse extends NestedStage<ThenResponse, Then> {
@ExpectedScenarioState
private Context context;
public ThenResponseError error() {
assertThat(context.getResponseStatus()).isNotEqualTo(200);
return nest(ThenResponseError.class);
}
public ThenResponse ok() {
assertThat(context.getResponseStatus()).isEqualTo(200);
return self();
}
public static class ThenResponseError extends NestedStage<ThenResponseError, ThenResponse> {
@ExpectedScenarioState
private Context context;
public ThenResponseError status(int status) {
assertThat(context.getResponseStatus()).isEqualTo(status);
return self();
}
public ThenResponseError with_a() {
return self();
}
}
public ThenCommandResponse has_commands() {
assertThat(context.getCommandResponseBody()).isNotNull();
return nest(ThenCommandResponse.class);
}
public static class ThenCommandResponse extends NestedStage<ThenCommandResponse, ThenResponse> {
@ExpectedScenarioState
private Context context;
public enum UserType {
PUBLIC,
EXTERNAL,
INTERNAL
}
public ThenCommandResponse are_for_$_users(UserType userType) {
assertThat(context.getCommandResponseBody().stream().map(Context.CommandGroup::getName).collect(Collectors.toList()))
.contains(String.format("%s-group", userType.name().toLowerCase()));
return self();
}
public ThenCommandResponse are_not_for_$_users(UserType userType) {
assertThat(context.getCommandResponseBody().stream().map(Context.CommandGroup::getName).collect(Collectors.toList()))
.doesNotContain(String.format("%s-group", userType.name().toLowerCase()));
return self();
}
}
}
}
package org.geant.lgservice.integration.scenario;
import com.alexlovett.jgivenextension.ExtendedStage;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tngtech.jgiven.annotation.ExpectedScenarioState;
import com.tngtech.jgiven.integration.spring.JGivenStage;
import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.geant.lgservice.integration.Context;
import org.geant.lgservice.integration.Context.SubmitResponseBody;
import org.geant.lgservice.security.LGUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.lang.reflect.Type;
import java.util.Collection;
import static java.util.Collections.singletonList;
import static org.springframework.http.HttpMethod.GET;
import static org.springframework.http.HttpMethod.POST;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
@JGivenStage
@Slf4j
public class When extends ExtendedStage<When> {
@ExpectedScenarioState
private Context context;
@Autowired
private RestTemplate restTemplate;
@Autowired
private MockMvc mvc;
private static final ObjectMapper MAPPER = new ObjectMapper();
@SneakyThrows
public When request_to_the_router_is_sent() {
MockHttpServletResponse response = send(POST, "rest/submit", MAPPER.writeValueAsString(RequestBody.builder()
.command(context.getCommand())
.routerHost(context.getRouterHost())
.build()));
context.withResponseStatus(response.getStatus());
if (response.getStatus() == 200) {
String responseBody = response.getContentAsString();
log.info("response body = {}", responseBody);
context.withSubmitResponseBody(MAPPER.readValue(responseBody, SubmitResponseBody.class));
}
return self();
}
@SneakyThrows
public When request_for_available_commands_is_sent() {
MockHttpServletResponse response = send(GET,"rest/commands/all", null);
if (response.getStatus() == 200) {
String responseBody = response.getContentAsString();
log.info("response body = {}", responseBody);
context.withCommandResponseBody(MAPPER.readValue(responseBody, new TypeReference<Collection<Context.CommandGroup>>() {}));
}
return self();
}
@SneakyThrows
private MockHttpServletResponse send(HttpMethod method, String path, String body) {
MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders
.request(method, UriComponentsBuilder.newInstance().scheme("http").path(path).build().toUri())
.contentType(APPLICATION_JSON);
if (StringUtils.isNotBlank(body)) {
requestBuilder.content(body);
}
LGUser user = context.getUser();
if (user != null) {
requestBuilder.with(user(user));
}
MockHttpServletResponse response = mvc.perform(requestBuilder).andReturn().getResponse();
context.withResponseStatus(response.getStatus());
return response;
}
@Getter
public static class RequestBody {
private final Collection<Router> selectedRouters;
private final Command selectedCommand;
@Builder
private RequestBody(String routerHost, String command) {
this.selectedRouters = singletonList(Router.withHost(routerHost));
this.selectedCommand = Command.withCommand(command);
}
@RequiredArgsConstructor
@Getter
public static class Router {
private final String name;
public static Router withHost(String hostName) {
return new Router(hostName);
}
}
@RequiredArgsConstructor
@Getter
public static class Command {
private final String value;
public static Command withCommand(String command) {
return new Command(command);
}
}
}
}
package org.geant.lgservice.security;
import com.google.common.collect.ImmutableMap;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.saml.SAMLCredential;
import static org.assertj.core.api.Assertions.assertThat;
import static org.geant.lgservice.security.LGUser.Group.EXTERNAL;
import static org.geant.lgservice.security.LGUser.Group.INTERNAL;
import static org.geant.lgservice.security.SamlKeyMappings.Key.EMAIL;
import static org.geant.lgservice.security.SamlKeyMappings.Key.GIVEN_NAME;
import static org.geant.lgservice.security.SamlKeyMappings.Key.GROUPS;
import static org.geant.lgservice.security.SamlKeyMappings.Key.ID;
import static org.geant.lgservice.security.SamlKeyMappings.Key.SURNAME;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.when;
public class LGUserServiceTest {
SAMLCredential credential = mock(SAMLCredential.class);
LGUserService subject = new LGUserService(
ImmutableMap.<SamlKeyMappings.Key, String>builder()
.put(ID, "uid")
.put(GIVEN_NAME, "givenName")
.put(SURNAME, "sn")
.put(EMAIL, "mail")
.put(GROUPS, "isMemberOf")
.build(),
ImmutableMap.<String, LGUser.Group>builder()
.put("internal", INTERNAL)
.put("external", EXTERNAL)
.build());
@Before
public void setup() {
reset(credential);
when(credential.getAttributeAsString("uid")).thenReturn("1");
when(credential.getAttributeAsString("givenName")).thenReturn("barry");
when(credential.getAttributeAsString("sn")).thenReturn("scott");
when(credential.getAttributeAsString("mail")).thenReturn("barry.scott@geant.org");
}
@Test
public void loadUserWithMultipleGroups() {
when(credential.getAttributeAsStringArray("isMemberOf"))
.thenReturn(new String[]{"internal", "external", "ignore"});
LGUser result = (LGUser) subject.loadUserBySAML(credential);
assertThat(result.getFederatedUser()).isEqualTo("1");
assertThat(result.getGivenName()).isEqualTo("barry");
assertThat(result.getSurname()).isEqualTo("scott");
assertThat(result.getMail()).isEqualTo("barry.scott@geant.org");
assertThat(result.getAuthorities()).containsExactlyInAnyOrder(INTERNAL, EXTERNAL);
}
@Test
public void loadUser() {
when(credential.getAttributeAsStringArray("isMemberOf"))
.thenReturn(new String[]{"external", "ignore"});
LGUser result = (LGUser) subject.loadUserBySAML(credential);
assertThat(result.getFederatedUser()).isEqualTo("1");
assertThat(result.getGivenName()).isEqualTo("barry");
assertThat(result.getSurname()).isEqualTo("scott");
assertThat(result.getMail()).isEqualTo("barry.scott@geant.org");
assertThat(result.getAuthorities()).containsExactlyInAnyOrder(EXTERNAL);
}
}
package org.geant.lgservice.services;
import org.assertj.core.api.Assertions;
import org.geant.lgservice.ecmr.CommandExecutor;
import org.geant.lgservice.pojos.Command;
import org.geant.lgservice.pojos.CommandOutput;
import org.geant.lgservice.pojos.Coordinates;
import org.geant.lgservice.pojos.Router;
import org.hibernate.result.Output;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mockito;
import java.util.Collections;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.Mockito.*;
public class LookingGlassServiceImplTest {
private CommandExecutor commandExecutor = mock(CommandExecutor.class);
private LookingGlassServiceImpl subject = new LookingGlassServiceImpl(null, commandExecutor);
@Before
public void setup() {
reset(commandExecutor);
}
@Test
public void testQuery() {
Router router = Router.builder()
.name("router.geant.org")
.build();
Command command = new Command();
String arguments = "some args";
CommandOutput output = new CommandOutput();
output.setRouterName(router.getName());
when(commandExecutor.execute(any(Router.class), any(Command.class), any(String.class), anyBoolean())).thenReturn(output);
Map<String, CommandOutput> result = subject.submitQuery(Collections.singletonList(router), command, arguments, false);
assertThat(result).hasSize(1);
assertThat(result).containsKey(router.getName());
assertThat(result.get(router.getName())).isEqualTo(output);
}
}
package org.geant.lgservice.services;
import static org.junit.Assert.assertEquals;
import java.util.List;
import org.geant.lgservice.pojos.Group;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class LookingGlassServiceTest {
@Autowired
LookingGlassService fixture;
@Test
public void test() {
List<Group> commandGroups = fixture.getAllCommands(null);
assertEquals("number of commands not correct", 1, commandGroups.size());
assertEquals("group name not correct", "test-group", commandGroups.get(0).getName());
assertEquals("command name not correct", "test-name", commandGroups.get(0).getCommands().get(0).getName());
}
}
[
{
"equipment name": "mx1.cbg.uk.geant.net",
"pop": {
"abbreviation": "lab",
"city": "Cambridge",
"country": "UK",
"country code": "UK",
"latitude": 0.0,
"longitude": 0.0,
"name": "DANTE Lab"
},
"type": "INTERNAL"
},
{
"equipment name": "mx2.tal.ee.geant.net",
"pop": {
"abbreviation": "tal",
"city": "Tallinn",
"country": "Estonia",
"country code": "EE",
"latitude": 59.434825,
"longitude": 24.71385,
"name": "Tallinn"
},
"type": "CORE"
},
{
"equipment name": "mx1.lon.uk.geant.net",
"pop": {
"abbreviation": "lon",
"city": "London",
"country": "England",
"country code": "UK",
"latitude": 51.498652777778,
"longitude": -0.015805555555556,
"name": "London"
},
"type": "CORE"
},
{
"equipment name": "mx1.pra.cz.geant.net",
"pop": {
"abbreviation": "pra",
"city": "Prague",
"country": "Czech Republic",
"country code": "CZ",
"latitude": 50.101847222222,
"longitude": 14.391738888889,
"name": "Prague"
},
"type": "CORE"
},
{
"equipment name": "mx1.sof.bg.geant.net",
"pop": {
"abbreviation": "sof",
"city": "Sofia",
"country": "Bulgaria",
"country code": "BG",
"latitude": 42.675758333333,
"longitude": 23.370986111111,
"name": "Sofia"
},
"type": "CORE"
},
{
"equipment name": "mx2.rig.lv.geant.net",
"pop": {
"abbreviation": "rig",
"city": "Riga",
"country": "Latvia",
"country code": "LV",
"latitude": 56.948344444444,
"longitude": 24.118,
"name": "Riga"
},
"type": "CORE"
},
{
"equipment name": "mx1.lon2.uk.geant.net",
"pop": {
"abbreviation": "lon2",
"city": "Slough",
"country": "UK",
"country code": "UK",
"latitude": 51.521986111111,
"longitude": 0.62331666666667,
"name": "London 2 (Slough)"
},
"type": "CORE"
},
{
"equipment name": "mx1.ham.de.geant.net",
"pop": {
"abbreviation": "ham",
"city": "Hamburg",
"country": "Germany",
"country code": "DE",
"latitude": 53.550902777778,
"longitude": 10.046486111111,
"name": "Hamburg"
},
"type": "CORE"
},
{
"equipment name": "mx1.poz.pl.geant.net",
"pop": {
"abbreviation": "poz",
"city": "Poznan",
"country": "Poland",
"country code": "PL",
"latitude": 52.411775,
"longitude": 16.917561111111,
"name": "Poznan"
},
"type": "CORE"
},
{
"equipment name": "mx1.ath2.gr.geant.net",
"pop": {
"abbreviation": "ath2",
"city": "Attiki",
"country": "Greece",
"country code": "GR",
"latitude": 37.98,
"longitude": 23.73,
"name": "Athens 2"
},
"type": "CORE"
},
{
"equipment name": "mx1.kau.lt.geant.net",
"pop": {
"abbreviation": "kau",
"city": "Kaunas",
"country": "Lithuania",
"country code": "LT",
"latitude": 54.940672222222,
"longitude": 24.018013888889,
"name": "Kaunas"
},
"type": "CORE"
},
{
"equipment name": "mx1.bud.hu.geant.net",
"pop": {
"abbreviation": "bud",
"city": "Budapest",
"country": "Hungary",
"country code": "HU",
"latitude": 47.517902777778,
"longitude": 19.055436111111,
"name": "Budapest"
},
"type": "CORE"
},
{
"equipment name": "mx2.bru.be.geant.net",
"pop": {
"abbreviation": "bru",
"city": "Brussels",
"country": "Belgium",
"country code": "BE",
"latitude": 50.857002777778,
"longitude": 4.4115694444444,
"name": "Brussels"
},
"type": "CORE"
},
{
"equipment name": "mx2.bra.sk.geant.net",
"pop": {
"abbreviation": "bra",
"city": "Bratislava",
"country": "Slovakia",
"country code": "SK",
"latitude": 48.119027777778,
"longitude": 17.0957,
"name": "Bratislava"
},
"type": "CORE"
},
{
"equipment name": "mx2.kau.lt.geant.net",
"pop": {
"abbreviation": "kau",
"city": "Kaunas",
"country": "Lithuania",
"country code": "LT",
"latitude": 54.940672222222,
"longitude": 24.018013888889,
"name": "Kaunas"
},
"type": "CORE"
},
{
"equipment name": "mx2.cbg.uk",
"pop": {
"abbreviation": "oc",
"city": "CAMBRIDGE",
"country": "UK",
"country code": "UK",
"latitude": 0.0,
"longitude": 0.0,
"name": "Cambridge OC"
},
"type": "INTERNAL"
},
{
"equipment name": "mx1.buc.ro.geant.net",
"pop": {
"abbreviation": "buc",
"city": "Bucharest",
"country": "Romania",
"country code": "RO",
"latitude": 44.444741666667,
"longitude": 26.096422222222,
"name": "Bucharest"
},
"type": "CORE"
},
{
"equipment name": "mx1.vie.at.geant.net",
"pop": {
"abbreviation": "vie",
"city": "Vienna",
"country": "Austria",
"country code": "AT",
"latitude": 48.268925,
"longitude": 16.410194444444,
"name": "Vienna"
},
"type": "CORE"
},
{
"equipment name": "mx1.mil2.it.geant.net",
"pop": {
"abbreviation": "mil2",
"city": "Milan",
"country": "Italy",
"country code": "IT",
"latitude": 45.475522222222,
"longitude": 9.1033194444444,
"name": "Milan 2 Caldera"
},
"type": "CORE"
},
{
"equipment name": "mx1.ams.nl.geant.net",
"pop": {
"abbreviation": "ams",
"city": "Amsterdam",
"country": "Netherlands",
"country code": "NL",
"latitude": 52.356363888889,
"longitude": 4.9529555555556,
"name": "Amsterdam"
},
"type": "CORE"
},
{
"equipment name": "sw2.am.office.geant.net",
"pop": {
"abbreviation": "am",
"city": "Amsterdam",
"country": "Netherlands",
"country code": "NL",
"latitude": 52.313305555556,
"longitude": 4.9491111111111,
"name": "Amsterdam GEANT Office"
},
"type": "INTERNAL"
},
{
"equipment name": "mx1.fra.de.geant.net",
"pop": {
"abbreviation": "fra",
"city": "Frankfurt",
"country": "Germany",
"country code": "DE",
"latitude": 50.120294444444,
"longitude": 8.7358444444444,
"name": "Frankfurt"
},
"type": "CORE"
},
{
"equipment name": "mx1.lis.pt.geant.net",
"pop": {
"abbreviation": "lis2",
"city": "Lisbon",
"country": "Portugal",
"country code": "PT",
"latitude": 38.759097222222,
"longitude": -9.1421944444444,
"name": "Lisbon 2"
},
"type": "CORE"
},
{
"equipment name": "mx2.lju.si.geant.net",
"pop": {
"abbreviation": "lju",
"city": "Ljubljana",
"country": "Slovenia",
"country code": "SI",
"latitude": 46.042366666667,
"longitude": 14.488719444444,
"name": "Ljubljana"
},
"type": "CORE"
},
{
"equipment name": "mx1.dub.ie.geant.net",
"pop": {
"abbreviation": "dub",
"city": "Dublin",
"country": "Ireland",
"country code": "IE",
"latitude": 53.291980555556,
"longitude": -6.4147333333333,
"name": "Dublin (City West)"
},
"type": "CORE"
},
{
"equipment name": "mx2.lis.pt.geant.net",
"pop": {
"abbreviation": "lis",
"city": "Lisbon",
"country": "Portugal",
"country code": "PT",
"latitude": 38.759097222222,
"longitude": -9.1421944444444,
"name": "Lisbon"
},
"type": "CORE"
},
{
"equipment name": "mx1.mad.es.geant.net",
"pop": {
"abbreviation": "mad",
"city": "Alcobendas - Madrid",
"country": "Spain",
"country code": "ES",
"latitude": 40.536477777778,
"longitude": -3.6488027777778,
"name": "Madrid"
},
"type": "CORE"
},
{
"equipment name": "mx1.tal.ee.geant.net",
"pop": {
"abbreviation": "tal",
"city": "Tallinn",
"country": "Estonia",
"country code": "EE",
"latitude": 59.434825,
"longitude": 24.71385,
"name": "Tallinn"
},
"type": "CORE"
},
{
"equipment name": "mx2.ath.gr.geant.net",
"pop": {
"abbreviation": "ath",
"city": "Athens",
"country": "Greece",
"country code": "GR",
"latitude": 37.973086111111,
"longitude": 23.745555555556,
"name": "Athens"
},
"type": "CORE"
},
{
"equipment name": "mx4 - Camb Lab",
"pop": {
"abbreviation": "lab",
"city": "Cambridge",
"country": "UK",
"country code": "UK",
"latitude": 0.0,
"longitude": 0.0,
"name": "DANTE Lab"
},
"type": "INTERNAL"
},
{
"equipment name": "mx1.par.fr.geant.net",
"pop": {
"abbreviation": "par",
"city": "Paris",
"country": "France",
"country code": "FR",
"latitude": 48.904630555556,
"longitude": 2.3713555555556,
"name": "Paris"
},
"type": "CORE"
},
{
"equipment name": "mx1.dub2.ie.geant.net",
"pop": {
"abbreviation": "dub2",
"city": "Dublin 12",
"country": "Ireland",
"country code": "IE",
"latitude": 53.33425,
"longitude": -6.3655444444444,
"name": "Dublin 2 - Parkwest"
},
"type": "CORE"
},
{
"equipment name": "sw3.am.office.geant.net",
"pop": {
"abbreviation": "am",
"city": "Amsterdam",
"country": "Netherlands",
"country code": "NL",
"latitude": 52.313305555556,
"longitude": 4.9491111111111,
"name": "Amsterdam GEANT Office"
},
"type": "INTERNAL"
},
{
"equipment name": "mx2.zag.hr.geant.net",
"pop": {
"abbreviation": "zag",
"city": "Zagreb",
"country": "Crotia",
"country code": "HR",
"latitude": 45.792030555556,
"longitude": 15.969494444444,
"name": "Zagreb"
},
"type": "CORE"
},
{
"equipment name": "mx1.mar.fr.geant.net",
"pop": {
"abbreviation": "mar",
"city": "Marseille",
"country": "France",
"country code": "FR",
"latitude": 43.311027777778,
"longitude": 5.3738888888889,
"name": "Marseille"
},
"type": "CORE"
},
{
"equipment name": "mx1.gen.ch.geant.net",
"pop": {
"abbreviation": "gen",
"city": "Geneva",
"country": "Switzerland",
"country code": "CH",
"latitude": 46.232194444444,
"longitude": 6.0457638888889,
"name": "Geneva"
},
"type": "CORE"
}
]
\ No newline at end of file
...@@ -10,24 +10,6 @@ configuration: ...@@ -10,24 +10,6 @@ configuration:
security: security:
saml2: saml2:
metadataURL: "https://login.terena.org/wayf/saml2/idp/metadata" metadataURL: "https://login.terena.org/wayf/saml2/idp/metadata"
server:
port: 8009
ssl:
enabled: true
spring:
datasource:
driverClassName: com.mysql.jdbc.Driver
password: opsro
url: "jdbc:mysql://test-opsdb01.geant.net:3306/opsdb"
username: opsro
jpa:
generate-ddl: true
hibernate.ddl-auto: update
properties:
hibernate:
dialect: org.hibernate.dialect.MySQL5Dialect
show-sql: false
show-sql: false
saml: saml:
metadata-url: https://login.terena.org/wayf/saml2/idp/metadata metadata-url: https://login.terena.org/wayf/saml2/idp/metadata
entity-name: https://test-lg.geant.net/saml entity-name: https://test-lg.geant.net/saml
...@@ -35,3 +17,12 @@ saml: ...@@ -35,3 +17,12 @@ saml:
key-store: classpath:saml/keystore.jks key-store: classpath:saml/keystore.jks
key-alias: spring key-alias: spring
key-store-password: secret key-store-password: secret
key-mappings:
id: "id"
given-name: "givenName"
surname: "surname"
email: "mail"
groups: "isMemberOf"
group-mappings:
INTERNAL: "GEANT Staff:All"
EXTERNAL: "GEANT_CO:GEANT Services:NREN Svc Mgmt:GEANT NRENs:members_GEANT NRENs"
<?xml version="1.0" encoding="UTF-8"?>
<!-- List of Commands, here name element contains the command name that is
visible on UI and value contains actual command. -->
<commands>
<group name="public-group">
<command>
<name>public-command</name>
<value>public-command</value>
<description>public-command</description>
<requiresParams>true</requiresParams>
</command>
</group>
<group name="external-group">
<command>
<name>external-command</name>
<value>external-command</value>
<description>external-command</description>
<requiresParams>true</requiresParams>
</command>
</group>
</commands>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<!-- List of Commands, here name element contains the command name that is
visible on UI and value contains actual command. -->
<commands>
<group name="public-group">
<command>
<name>public-command</name>
<value>public-command</value>
<description>public-command</description>
<requiresParams>true</requiresParams>
</command>
</group>
<group name="external-group">
<command>
<name>external-command</name>
<value>external-command</value>
<description>external-command</description>
<requiresParams>true</requiresParams>
</command>
</group>
<group name="internal-group">
<command>
<name>internal-command</name>
<value>internal-command</value>
<description>internal-command</description>
<requiresParams>true</requiresParams>
</command>
</group>
</commands>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- List of Commands, here name element contains the command name that is <!-- List of Commands, here name element contains the command name that is
visible on UI and value contains actual command. --> visible on UI and value contains actual command. -->
<commands> <commands>
<group name="test-group"> <group name="public-group">
<command> <command>
<name>test-name</name> <name>public-command</name>
<value>test-value</value> <value>public-command</value>
<description>test-description</description> <description>public-command</description>
<requiresParams>true</requiresParams> <requiresParams>true</requiresParams>
</command> </command>
</group> </group>
</commands> </commands>
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment