I know I can unit test console commands by passing arguments and options like so:
$command->run(new ArrayInput($data), new NullOutput);
But what if I want to add a confirmation dialog to my command by using the confirm() method in Laravel?
Have you read the example on Symfony's site?
http://symfony.com/doc/current/components/console/helpers/dialoghelper.html#testing-a-command-which-expects-input
If you have but are still having trouble making it work, let us know.
I forgot to mention I was using PHPSpec for my unit tests.
Finally I figured out how to test confirmations with it.
These are the use statements:
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Helper\HelperSet;
And this is a sample test:
public function it_fires_the_command(QuestionHelper $question, HelperSet $helpers)
{
// $data is an array containing arguments and options for the console command
$input = new ArrayInput($data);
$output = new NullOutput;
// $query must be an instance of ConfirmationQuestion
$query = Argument::type('Symfony\Component\Console\Question\ConfirmationQuestion');
// with "willReturn" we can decide whether (TRUE) or not (FALSE) the user confirms
$question->ask($input, $output, $query)->willReturn(true);
// we expect the HelperSet to be invoked returning the mocked QuestionHelper
$helpers->get('question')->willReturn($question);
// finally we set the mocked HelperSet in our console command...
$this->setHelperSet($helpers);
// ...and run it
$this->run($input, $output);
}
Related
I was writing a very basic test for a Laravel Artisan Console Command, like this:
$this->artisan("my-command", ["--some-option" => "some-value"])
->expectsOutput("the expected output");
The test didn't pass. I got really troubled because "the expected output" was exactly what the command was outputting when executed mannualy.
But that's ok, I just have to inspect what the command output actually is when executed through automated tests, right? But wait, how do I do that?
I tried the following:
$output = new BufferedConsoleOutput();
Artisan::call("my-command", ["--some-option", "some-value"], $output);
// dd($output->fetch()
$this->assertTrue($output->fetch() === "the expected output");
But $output->fetch() seems to be always empty.
In brief: How do I print the actual output of a Laravel command in the context of a test?
This isn't something I would want to set up and leave in your tests, but if you prevent Laravel from mocking the console output, you can capture it for inspection.
Assuming you have this test that fails:
public function testStuffDoesntBreak(): void
{
$this->artisan("my-command", ["--some-option" => "some-value"])
->expectsOutput("the expected output");
}
You can rewrite it to this:
use Illuminate\Support\Facades\Artisan;
...
public function testStuffDoesntBreak(): void
{
$this->withoutMockingConsoleOutput()
->artisan("my-command", ["--some-option" => "some-value"]);
// capture the text output from the command
$result = Artisan::output();
// use standard text assertions
$this->assertEquals("the expected output", $result);
}
When you've disabled console mocking, the artisan() method stops being fluent and instead returns the exit code of the command. But it does allow the Artisan facade to access its output. Whether you want to rewrite tests or just change them on the fly in case of an error is personal preference. I've done the latter, as I'd rather not miss out on features like expectsTable().
(Symfony3)
I'm toying with the idea of setting up some simple cron tasks to generate security reports for our project managers so that they can schedule upgrade time for developers (vs. me forgetting to run them manually).
As a very basic check, I'll simply run...
php bin/console security:check
...to see what composer has to say about vulnerabilities. Ultimately I'd like to roll this output into an email or post it to a slack channel or basecamp job when the cron is run.
Problem
When I run the command from via terminal it works great. Running the command inside a controller always returns the response Lock file does not exist. I'm assuming this in reference to the composer.lock file at the root of the project. I can confirm that this file does in fact exist.
Following is the controller I'm currently using, which is adapted from this:
http://symfony.com/doc/current/console/command_in_controller.html
<?php
namespace Treetop1500\SecurityReportBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Bundle\FrameworkBundle\Console\Application;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
class DefaultController extends Controller
{
public function indexAction($key)
{
if ($key != $this->getParameter('easy_cron_key')) {
throw new UnauthorizedHttpException("You are not authorized to access this page.");
}
$kernel = $this->get('kernel');
$application = new Application($kernel);
$application->setAutoExit(false);
$input = new ArrayInput(array(
'command' => 'security:check'
));
// You can use NullOutput() if you don't need the output
$output = new BufferedOutput();
$application->run($input, $output);
// return the output, don't use if you used NullOutput()
$content = $output->fetch();
// return new Response(""), if you used NullOutput()
return new Response($content);
}
}
$content always has the value "Lock file does not exist."
I realize there are probably better tools and ways to do this, however I would really like to understand why this is the generated response from in this controller action. Thank you for taking a look!
Pass absolute path to composer.lock file just like that:
php bin/console security:check /path/to/another/composer.lock
So in your example, that's would be:
$input = new ArrayInput([
'command' => 'security:check',
'lockfile' => '/path/to/another/composer.lock'
]);
Read more: SecurityCheckerCommand from SensioLabs. Optional argument is lockfile, which is checked by SecurityChecker. On line 46, they are looking for composer.lock file (default argument) and throw an exception, when they not found.
P.S. Earlier, I type the wrong parameters to array. I checked in Symfony documentation (How to Call Other Commands) and fixed the answer.
The solution to this is to pass the lockfile argument to the ArrayInput object like this:
$lockfile = $this->get('kernel')->getRootDir()."/../composer.lock";
$input = new ArrayInput(array('command'=>'security:check','lockfile'=>$lockfile));
Problem / What I've tried:
Getting the currently used controller and action in Laravel 5 is easy (but not as easy as it should be), however I'm stuck with getting the currently used artisan console command.
To fetch the controller name I do this:
$route = Route::getRoutes()->match(Request::capture());
$listAction = explode('\\', $route->getActionName());
$rawAction = end($listAction);
// controller name and action in a simple array
$controllerAndAction = explode('#', $rawAction);
But when calling from a console action, it always returns the default index controller's name ("IndexController" or so in Laravel). Does anybody know how to make this ?
By the way I've also worked throught Request::capture() but this still gives no info about the command.
The simplest way is to just to look at the arguments specified on the command line:
if (array_get(request()->server(), 'argv.1') === 'cache:clear') {
// do things
}
Yes, you can use $_SERVER directly, but I like to use the helper functions or the Facades, as those will give you the current data.
I go from the assumption that - during unit tests - the superglobals might not always reflect the currently tested request.
By the way: Obviously can also do array_get(request()->server('argv'), '1') or something alike. (request()->server('argv.1') doesnt work at this point). Or use \Request::server(). Depends on what you like most.
As per the Symfony\Component\Console\Command\Command class, the method to return the name of the command (eg. my:command) is:
$this->getName();
You should use it from within an Artisan command extending Illuminate\Console\Command (default on Artisan commands).
Remember that it will return only the command name and not the available parameters (eg. for the command signature my:command {--with-params=} it will only return my:command).
Reflection might be of help? Try this:
$var = new \ReflectionClass($this);
dd($var);
Given the following snippet:
$schedule->call(function () {
// echo "HELLO 123"; // nope
// return "HELLO 123"; // also nope
})
->everyMinute()
->sendOutputTo(storage_path('logs/cron/hello.cron.log'))
->emailOutputTo('me#example.org');
The scheduled task is running, the emails are being generated, but with no content. I can capture output via the $schedule->exec and $schedule->command.
Ideal end state here is, run a few internal, non-command processes within the call method and output the results into a file/email.
I just googled the documentation of the class you are using with the words laravel scheduled output and the documentation (Just above the anchor in a red box) states:
Note: The emailOutputTo and sendOutputTo methods are exclusive to the
command method and are not supported for call.
Hope that helps.
The accepted answer got me to the solution, but for future reference:
$schedule->call(function () {
// Do stuff
// Report to file
\Storage::append('logs/cron/hello.cron.log', "HELLO 123");
})->everyMinute();
I find it quiet unsatisfying that Laravel does not support running multiple Artisan commands consecutively, including capturing their outputs and stopping in case of exceptions.
\Storage::append() from #Chris' answer does only write into the storage/app/ directory, but I wanted the logs in storage/logs/. So I replaced it with \File::append().
However, my log file storage/logs/schedule.log is a symbolic link in production environment and file_put_contents(), on which both methods rely on, is not able to write to symbolic links.
This is the solution I came up with:
use Illuminate\Support\Facades\Artisan;
use Symfony\Component\Console\Output\OutputInterface;
// ...
$schedule->callWithOutput(function (OutputInterface $output) {
Artisan::call(FooCommand::class, outputBuffer: $output);
Artisan::call(BarCommand::class, outputBuffer: $output);
}, storage_path('logs/schedule.log'))>everyMinute();
Schedule::callWithOutput() is a macro that takes our callback and captures all output in a BufferedOutput(). It is eventually redirected to the log file (second argument) with shell_exec(), so it works with regular files and symlinks:
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Support\Facades\App;
use Illuminate\Support\ServiceProvider;
use Symfony\Component\Console\Output\BufferedOutput;
class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
Schedule::macro('callWithOutput', function (callable $callback, string $logFile, array $parameters = []) {
/** #var Schedule $this */
return $this->call(function () use ($callback, $logFile, $parameters) {
$output = new BufferedOutput();
try {
App::call($callback, compact('output') + $parameters);
} finally {
shell_exec("echo -n '{$output->fetch()}' >> $logFile");
}
});
});
}
}
You can also pass $parameters (second parameter of Schedule::call()) to Schedule::callWithOutput() as third parameter.
If you're not executing Artisan commands, you can write output with $output->write() or $output->writeln().
I can see if a script is running in the console with the App::runningInConsole() command, but I would like to know (for audit logging purposes) which command has been run from console.
To add some context - I need to log whenever a system or user accesses a certain type of data. For users, that's pretty simple - I can use Auth::user() and get their IP address from Request, but for console commands, it's a little more difficult.
I can figure out if a console command is running, which is good, but I need to be able to record which console command is running.
I do not see a way to get that information directly Laravel. I'm also not sure how that would be possible, as you can have one command executing other commands or even creating a new application instance like some test tools do.
There is however a way to achieve that using plain PHP. You can check the server/environment variables to identify how application was executed.
Have a look at
dd($_SERVER['argv']);
If you run a command this should give you the command name in $_SERVER['argv'][1]
You can find more information here: http://php.net/manual/en/reserved.variables.argv.php
To get console command together with arguments you should use something like that:
$command = \Request::server('argv', null);
if (is_array($command)) {
$command = implode(' ', $command);
}
In Laravel you can use \Request:server to get $_SERVER variables.
The problem with $SERVER['arg'] is that it doesnt work when you execute the command on the GUI.
I fixed the problem adding the next code on the command.
private function getAttributes()
{
$arguments = $this->arguments();
unset($arguments[0]);
$arguments = collect($arguments)->implode(' ');
$options = collect($this->options())->filter(function($item) {
return $item;
})->map(function($item, $key) {
$return = '--'.$key;
if($item!==true) {
$return .= '='.$item;
}
return $return;
})->implode(' ');
return $arguments.' '.$options;
}
and you get it calling $this->getAttributes() and you will get all the command with the attributes