[JENKINS-75378] Adding a CLI command listener (#10382)

This commit is contained in:
Kris Stern 2025-03-23 00:20:28 +08:00 committed by GitHub
commit 2fb523ffe3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 485 additions and 95 deletions

View File

@ -48,10 +48,10 @@ import java.lang.reflect.Type;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.UUID; import jenkins.cli.listeners.CLIContext;
import java.util.logging.Level; import jenkins.cli.listeners.CLIListener;
import java.util.logging.Logger;
import jenkins.model.Jenkins; import jenkins.model.Jenkins;
import jenkins.util.Listeners;
import jenkins.util.SystemProperties; import jenkins.util.SystemProperties;
import org.jvnet.hudson.annotation_indexer.Index; import org.jvnet.hudson.annotation_indexer.Index;
import org.jvnet.tiger_types.Types; import org.jvnet.tiger_types.Types;
@ -242,70 +242,73 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
this.locale = locale; this.locale = locale;
CmdLineParser p = getCmdLineParser(); CmdLineParser p = getCmdLineParser();
Authentication auth = getTransportAuthentication2();
CLIContext context = new CLIContext(getName(), args, auth);
// add options from the authenticator // add options from the authenticator
SecurityContext sc = null; SecurityContext sc = null;
Authentication old = null; Authentication old = null;
Authentication auth;
try { try {
// TODO as in CLIRegisterer this may be doing too much work // TODO as in CLIRegisterer this may be doing too much work
sc = SecurityContextHolder.getContext(); sc = SecurityContextHolder.getContext();
old = sc.getAuthentication(); old = sc.getAuthentication();
sc.setAuthentication(auth = getTransportAuthentication2()); sc.setAuthentication(auth);
if (!(this instanceof HelpCommand || this instanceof WhoAmICommand)) if (!(this instanceof HelpCommand || this instanceof WhoAmICommand))
Jenkins.get().checkPermission(Jenkins.READ); Jenkins.get().checkPermission(Jenkins.READ);
p.parseArgument(args.toArray(new String[0])); p.parseArgument(args.toArray(new String[0]));
LOGGER.log(Level.FINE, "Invoking CLI command {0}, with {1} arguments, as user {2}.",
new Object[] {getName(), args.size(), auth.getName()}); Listeners.notify(CLIListener.class, true, listener -> listener.onExecution(context));
int res = run(); int res = run();
LOGGER.log(Level.FINE, "Executed CLI command {0}, with {1} arguments, as user {2}, return code {3}", Listeners.notify(CLIListener.class, true, listener -> listener.onCompleted(context, res));
new Object[] {getName(), args.size(), auth.getName(), res});
return res; return res;
} catch (CmdLineException e) {
logFailedCommandAndPrintExceptionErrorMessage(args, e);
printUsage(stderr, p);
return 2;
} catch (IllegalStateException e) {
logFailedCommandAndPrintExceptionErrorMessage(args, e);
return 4;
} catch (IllegalArgumentException e) {
logFailedCommandAndPrintExceptionErrorMessage(args, e);
return 3;
} catch (AbortException e) {
logFailedCommandAndPrintExceptionErrorMessage(args, e);
return 5;
} catch (AccessDeniedException e) {
logFailedCommandAndPrintExceptionErrorMessage(args, e);
return 6;
} catch (BadCredentialsException e) {
// to the caller, we can't reveal whether the user didn't exist or the password didn't match.
// do that to the server log instead
String id = UUID.randomUUID().toString();
logAndPrintError(e, "Bad Credentials. Search the server log for " + id + " for more details.",
"CLI login attempt failed: " + id, Level.INFO);
return 7;
} catch (Throwable e) { } catch (Throwable e) {
String errorMsg = "Unexpected exception occurred while performing " + getName() + " command."; int exitCode = handleException(e, context, p);
logAndPrintError(e, errorMsg, errorMsg, Level.WARNING); Listeners.notify(CLIListener.class, true, listener -> listener.onThrowable(context, e));
Functions.printStackTrace(e, stderr); return exitCode;
return 1;
} finally { } finally {
if (sc != null) if (sc != null)
sc.setAuthentication(old); // restore sc.setAuthentication(old); // restore
} }
} }
private void logFailedCommandAndPrintExceptionErrorMessage(List<String> args, Throwable e) { /**
Authentication auth = getTransportAuthentication2(); * Determines command stderr output and return the exit code as described on {@link #main(List, Locale, InputStream, PrintStream, PrintStream)}
String logMessage = String.format("Failed call to CLI command %s, with %d arguments, as user %s.", * */
getName(), args.size(), auth != null ? auth.getName() : "<unknown>"); protected int handleException(Throwable e, CLIContext context, CmdLineParser p) {
int exitCode;
logAndPrintError(e, e.getMessage(), logMessage, Level.FINE); if (e instanceof CmdLineException) {
exitCode = 2;
printError(e.getMessage());
printUsage(stderr, p);
} else if (e instanceof IllegalArgumentException) {
exitCode = 3;
printError(e.getMessage());
} else if (e instanceof IllegalStateException) {
exitCode = 4;
printError(e.getMessage());
} else if (e instanceof AbortException) {
exitCode = 5;
printError(e.getMessage());
} else if (e instanceof AccessDeniedException) {
exitCode = 6;
printError(e.getMessage());
} else if (e instanceof BadCredentialsException) {
exitCode = 7;
printError(
"Bad Credentials. Search the server log for " + context.getCorrelationId() + " for more details.");
} else {
exitCode = 1;
printError("Unexpected exception occurred while performing " + getName() + " command.");
Functions.printStackTrace(e, stderr);
}
return exitCode;
} }
private void logAndPrintError(Throwable e, String errorMessage, String logMessage, Level logLevel) {
LOGGER.log(logLevel, logMessage, e); private void printError(String errorMessage) {
this.stderr.println(); this.stderr.println();
this.stderr.println("ERROR: " + errorMessage); this.stderr.println("ERROR: " + errorMessage);
} }
@ -541,8 +544,6 @@ public abstract class CLICommand implements ExtensionPoint, Cloneable {
return null; return null;
} }
private static final Logger LOGGER = Logger.getLogger(CLICommand.class.getName());
private static final ThreadLocal<CLICommand> CURRENT_COMMAND = new ThreadLocal<>(); private static final ThreadLocal<CLICommand> CURRENT_COMMAND = new ThreadLocal<>();
/*package*/ static CLICommand setCurrent(CLICommand cmd) { /*package*/ static CLICommand setCurrent(CLICommand cmd) {

View File

@ -27,11 +27,9 @@ package hudson.cli.declarative;
import static java.util.logging.Level.SEVERE; import static java.util.logging.Level.SEVERE;
import edu.umd.cs.findbugs.annotations.NonNull; import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.AbortException;
import hudson.Extension; import hudson.Extension;
import hudson.ExtensionComponent; import hudson.ExtensionComponent;
import hudson.ExtensionFinder; import hudson.ExtensionFinder;
import hudson.Functions;
import hudson.Util; import hudson.Util;
import hudson.cli.CLICommand; import hudson.cli.CLICommand;
import hudson.cli.CloneableCLICommand; import hudson.cli.CloneableCLICommand;
@ -49,19 +47,17 @@ import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.MissingResourceException; import java.util.MissingResourceException;
import java.util.Stack; import java.util.Stack;
import java.util.UUID;
import java.util.logging.Level;
import java.util.logging.Logger; import java.util.logging.Logger;
import jenkins.ExtensionComponentSet; import jenkins.ExtensionComponentSet;
import jenkins.ExtensionRefreshException; import jenkins.ExtensionRefreshException;
import jenkins.cli.listeners.CLIContext;
import jenkins.cli.listeners.CLIListener;
import jenkins.model.Jenkins; import jenkins.model.Jenkins;
import jenkins.util.Listeners;
import org.jvnet.hudson.annotation_indexer.Index; import org.jvnet.hudson.annotation_indexer.Index;
import org.jvnet.localizer.ResourceBundleHolder; import org.jvnet.localizer.ResourceBundleHolder;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser; import org.kohsuke.args4j.CmdLineParser;
import org.kohsuke.args4j.ParserProperties; import org.kohsuke.args4j.ParserProperties;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
@ -202,6 +198,9 @@ public class CLIRegisterer extends ExtensionFinder {
List<MethodBinder> binders = new ArrayList<>(); List<MethodBinder> binders = new ArrayList<>();
Authentication auth = getTransportAuthentication2();
CLIContext context = new CLIContext(getName(), args, auth);
CmdLineParser parser = bindMethod(binders); CmdLineParser parser = bindMethod(binders);
try { try {
// TODO this could probably use ACL.as; why is it calling SecurityContext.setAuthentication rather than SecurityContextHolder.setContext? // TODO this could probably use ACL.as; why is it calling SecurityContext.setAuthentication rather than SecurityContextHolder.setContext?
@ -211,19 +210,19 @@ public class CLIRegisterer extends ExtensionFinder {
// fill up all the binders // fill up all the binders
parser.parseArgument(args); parser.parseArgument(args);
Authentication auth = getTransportAuthentication2();
sc.setAuthentication(auth); // run the CLI with the right credential sc.setAuthentication(auth); // run the CLI with the right credential
jenkins.checkPermission(Jenkins.READ); jenkins.checkPermission(Jenkins.READ);
Listeners.notify(CLIListener.class, true, listener -> listener.onExecution(context));
// resolve them // resolve them
Object instance = null; Object instance = null;
for (MethodBinder binder : binders) for (MethodBinder binder : binders)
instance = binder.call(instance); instance = binder.call(instance);
if (instance instanceof Integer) Integer exitCode = (instance instanceof Integer) ? (Integer) instance : 0;
return (Integer) instance; Listeners.notify(CLIListener.class, true, listener -> listener.onCompleted(context, exitCode));
else return exitCode;
return 0;
} catch (InvocationTargetException e) { } catch (InvocationTargetException e) {
Throwable t = e.getTargetException(); Throwable t = e.getTargetException();
if (t instanceof Exception) if (t instanceof Exception)
@ -232,47 +231,13 @@ public class CLIRegisterer extends ExtensionFinder {
} finally { } finally {
sc.setAuthentication(old); // restore sc.setAuthentication(old); // restore
} }
} catch (CmdLineException e) {
printError(e.getMessage());
printUsage(stderr, parser);
return 2;
} catch (IllegalStateException e) {
printError(e.getMessage());
return 4;
} catch (IllegalArgumentException e) {
printError(e.getMessage());
return 3;
} catch (AbortException e) {
printError(e.getMessage());
return 5;
} catch (AccessDeniedException e) {
printError(e.getMessage());
return 6;
} catch (BadCredentialsException e) {
// to the caller, we can't reveal whether the user didn't exist or the password didn't match.
// do that to the server log instead
String id = UUID.randomUUID().toString();
logAndPrintError(e, "Bad Credentials. Search the server log for " + id + " for more details.",
"CLI login attempt failed: " + id, Level.INFO);
return 7;
} catch (Throwable e) { } catch (Throwable e) {
final String errorMsg = "Unexpected exception occurred while performing " + getName() + " command."; int exitCode = handleException(e, context, parser);
logAndPrintError(e, errorMsg, errorMsg, Level.WARNING); Listeners.notify(CLIListener.class, true, listener -> listener.onThrowable(context, e));
Functions.printStackTrace(e, stderr); return exitCode;
return 1;
} }
} }
private void printError(String errorMessage) {
this.stderr.println();
this.stderr.println("ERROR: " + errorMessage);
}
private void logAndPrintError(Throwable e, String errorMessage, String logMessage, Level logLevel) {
LOGGER.log(logLevel, logMessage, e);
printError(errorMessage);
}
@Override @Override
protected int run() throws Exception { protected int run() throws Exception {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();

View File

@ -0,0 +1,88 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.cli.listeners;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.Nullable;
import java.util.List;
import java.util.UUID;
import org.springframework.security.core.Authentication;
/**
* Holds information of a command execution. Same instance is used to all {@link CLIListener} invocations.
* Use {@code correlationId} in order to group related events to the same command.
*
* @since TODO
*/
public class CLIContext {
private final String correlationId = UUID.randomUUID().toString();
private final String command;
private final List<String> args;
private final Authentication auth;
/**
* @param command The command being executed.
* @param args Arguments passed to the command.
* @param auth Authenticated user performing the execution.
*/
public CLIContext(@NonNull String command, @CheckForNull List<String> args, @Nullable Authentication auth) {
this.command = command;
this.args = args != null ? args : List.of();
this.auth = auth;
}
/**
* @return Correlate this command event to other, related command events.
*/
@NonNull
public String getCorrelationId() {
return correlationId;
}
/**
* @return Command being executed.
*/
@NonNull
public String getCommand() {
return command;
}
/**
* @return Arguments passed to the command.
*/
@NonNull
public List<String> getArgs() {
return args;
}
/**
* @return Authenticated user performing the execution.
*/
@CheckForNull
public Authentication getAuth() {
return auth;
}
}

View File

@ -0,0 +1,60 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.cli.listeners;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.ExtensionPoint;
import hudson.cli.CLICommand;
/**
* Allows implementations to listen to {@link CLICommand#run()} execution events.
*
* @since TODO
*/
public interface CLIListener extends ExtensionPoint {
/**
* Invoked before command execution.
*
* @param context Information about the command being executed.
* */
default void onExecution(@NonNull CLIContext context) {}
/**
* Invoked after command execution.
*
* @param context Information about the command being executed.
* @param exitCode Exit code returned by the implementation of {@link CLICommand#run()}.
* */
default void onCompleted(@NonNull CLIContext context, int exitCode) {}
/**
* Invoked when an exception or error occurs during command execution.
*
* @param context Information about the command being executed.
* @param t Any error during the execution of the command.
* */
default void onThrowable(@NonNull CLIContext context, @NonNull Throwable t) {}
}

View File

@ -0,0 +1,90 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.cli.listeners;
import hudson.AbortException;
import hudson.Extension;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.args4j.CmdLineException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
/**
* Basic default implementation of {@link CLIListener} that just logs.
*/
@Extension
@Restricted(NoExternalUse.class)
public class DefaultCLIListener implements CLIListener {
private static final Logger LOGGER = Logger.getLogger(DefaultCLIListener.class.getName());
@Override
public void onExecution(CLIContext context) {
LOGGER.log(Level.FINE, "Invoking CLI command {0}, with {1} arguments, as user {2}.", new Object[] {
context.getCommand(), context.getArgs().size(), authName(context.getAuth()),
});
}
@Override
public void onCompleted(CLIContext context, int exitCode) {
LOGGER.log(
Level.FINE, "Executed CLI command {0}, with {1} arguments, as user {2}, return code {3}", new Object[] {
context.getCommand(), context.getArgs().size(), authName(context.getAuth()), exitCode,
});
}
@Override
public void onThrowable(CLIContext context, Throwable t) {
if (t instanceof BadCredentialsException) {
// to the caller (stderr), we can't reveal whether the user didn't exist or the password didn't match.
// do that to the server log instead
LOGGER.log(Level.INFO, "CLI login attempt failed: " + context.getCorrelationId(), t);
} else if (t instanceof CmdLineException
|| t instanceof IllegalArgumentException
|| t instanceof IllegalStateException
|| t instanceof AbortException
|| t instanceof AccessDeniedException) {
// covered cases on CLICommand#handleException
LOGGER.log(
Level.FINE,
String.format(
"Failed call to CLI command %s, with %d arguments, as user %s.",
context.getCommand(), context.getArgs().size(), authName(context.getAuth())),
t);
} else {
LOGGER.log(
Level.WARNING,
"Unexpected exception occurred while performing " + context.getCommand() + " command.",
t);
}
}
private static String authName(Authentication auth) {
return auth != null ? auth.getName() : "<unknown>";
}
}

View File

@ -0,0 +1,186 @@
/*
* The MIT License
*
* Copyright (c) 2025, CloudBees, Inc.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package jenkins.cli;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.notNullValue;
import hudson.cli.CLICommand;
import hudson.cli.CLICommandInvoker;
import hudson.cli.ListJobsCommand;
import java.io.IOException;
import java.util.List;
import java.util.logging.Level;
import jenkins.cli.listeners.DefaultCLIListener;
import jenkins.model.Jenkins;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.jvnet.hudson.test.JenkinsRule;
import org.jvnet.hudson.test.LoggerRule;
import org.jvnet.hudson.test.MockAuthorizationStrategy;
import org.jvnet.hudson.test.TestExtension;
public class DefaultCLIListenerTest {
@Rule
public JenkinsRule j = new JenkinsRule();
@Rule
public LoggerRule logging = new LoggerRule();
private static final String USER = "cli-user";
@Before
public void setUp() throws IOException {
j.jenkins.setSecurityRealm(j.createDummySecurityRealm());
j.jenkins.setAuthorizationStrategy(new MockAuthorizationStrategy()
.grant(Jenkins.ADMINISTER)
.everywhere()
.to(USER));
j.createFreeStyleProject("p");
logging.record(DefaultCLIListener.class, Level.FINE).capture(2);
}
@Test
public void commandOnCompletedIsLogged() throws Exception {
CLICommandInvoker command = new CLICommandInvoker(j, new ListJobsCommand());
command.asUser(USER).invoke();
List<String> messages = logging.getMessages();
assertThat(messages, hasSize(2));
assertThat(
messages.get(0),
containsString("Invoking CLI command list-jobs, with 0 arguments, as user %s.".formatted(USER)));
assertThat(
messages.get(1),
containsString(
"Executed CLI command list-jobs, with 0 arguments, as user %s, return code 0".formatted(USER)));
}
@Test
public void commandOnThrowableIsLogged() throws Exception {
CLICommandInvoker command = new CLICommandInvoker(j, new ListJobsCommand());
command.asUser(USER).invokeWithArgs("view-not-found");
List<String> messages = logging.getMessages();
assertThat(messages, hasSize(2));
assertThat(
messages.get(0),
containsString("Invoking CLI command list-jobs, with 1 arguments, as user %s.".formatted(USER)));
assertThat(
messages.get(1),
containsString("Failed call to CLI command list-jobs, with 1 arguments, as user %s.".formatted(USER)));
assertThat(
logging.getRecords().get(0).getThrown().getMessage(),
containsString("No view or item group with the given name 'view-not-found' found"));
}
@Test
public void commandOnThrowableUnexpectedIsLogged() throws Exception {
CLICommandInvoker command = new CLICommandInvoker(j, new ThrowsTestCommand());
command.asUser(USER).invoke();
List<String> messages = logging.getMessages();
assertThat(messages, hasSize(2));
assertThat(
messages.get(0),
containsString(
"Invoking CLI command throws-test-command, with 0 arguments, as user %s.".formatted(USER)));
assertThat(
messages.get(1),
containsString("Unexpected exception occurred while performing throws-test-command command."));
assertThat(logging.getRecords().get(0).getThrown().getMessage(), containsString("unexpected"));
}
@Test
public void methodOnCompletedIsLogged() throws Exception {
CLICommandInvoker command = new CLICommandInvoker(j, "disable-job");
command.asUser(USER).invokeWithArgs("p");
List<String> messages = logging.getMessages();
assertThat(messages, hasSize(2));
assertThat(
messages.get(0),
containsString("Invoking CLI command disable-job, with 1 arguments, as user %s.".formatted(USER)));
assertThat(
messages.get(1),
containsString("Executed CLI command disable-job, with 1 arguments, as user %s, return code 0"
.formatted(USER)));
}
@Test
public void methodOnThrowableIsLogged() throws Exception {
CLICommandInvoker command = new CLICommandInvoker(j, "disable-job");
command.asUser(USER).invokeWithArgs("job-not-found");
List<String> messages = logging.getMessages();
assertThat(messages, hasSize(2));
assertThat(
messages.get(0),
containsString("Invoking CLI command disable-job, with 1 arguments, as user %s.".formatted(USER)));
assertThat(
messages.get(1),
containsString(
"Failed call to CLI command disable-job, with 1 arguments, as user %s.".formatted(USER)));
assertThat(
logging.getRecords().get(0).getThrown().getMessage(),
containsString("No such job job-not-found exists."));
}
@Test
public void methodOnThrowableUnexpectedIsLogged() throws Exception {
CLICommandInvoker command = new CLICommandInvoker(j, "restart");
command.asUser(USER).invoke();
List<String> messages = logging.getMessages();
assertThat(messages, hasSize(2));
assertThat(
messages.get(0),
containsString("Invoking CLI command restart, with 0 arguments, as user %s.".formatted(USER)));
assertThat(messages.get(1), containsString("Unexpected exception occurred while performing restart command."));
assertThat(logging.getRecords().get(0).getThrown(), notNullValue());
}
@TestExtension
public static class ThrowsTestCommand extends CLICommand {
@Override
public String getName() {
return "throws-test-command";
}
@Override
public String getShortDescription() {
return "throws test command";
}
@Override
protected int run() {
throw new RuntimeException("unexpected");
}
}
}