diff --git a/.travis.yml.sig b/.travis.yml.sig index 8d0777867..c2300c5a2 100644 Binary files a/.travis.yml.sig and b/.travis.yml.sig differ diff --git a/CHANGELOG b/CHANGELOG index a65592cfc..4388e2cd1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,7 @@ UNRELEASED CHANGES: * Add Linkedin URL in the Contact object returned by the API * Improve localization: add plural forms, localize every needed messages * Split app.js in 3 files, and load translations files for Vue in separate files +* Add a new monica:update command RELEASED VERSIONS: diff --git a/Procfile b/Procfile index 08cfcef95..a6161ab11 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,3 @@ web: vendor/bin/heroku-php-nginx -C nginx_app.conf /public queue: php artisan queue:work --sleep=3 --tries=3 -release: php artisan migrate --force +release: php artisan monica:update --force -vvv diff --git a/app/Console/Commands/Helpers/CommandExecutor.php b/app/Console/Commands/Helpers/CommandExecutor.php new file mode 100644 index 000000000..0208762db --- /dev/null +++ b/app/Console/Commands/Helpers/CommandExecutor.php @@ -0,0 +1,53 @@ +command = $command; + } + + public function exec($message, $commandline) + { + $this->command->info($message); + $this->command->line($commandline); + exec($commandline.' 2>&1', $output); + if ($this->command->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { + foreach ($output as $line) { + $this->command->line($line); + } + } + $this->command->line(''); + } + + public function artisan($message, $commandline, array $arguments = []) + { + $this->command->info($message); + $info = ''; + foreach ($arguments as $key => $value) { + $info = $info.' '.$key.'='.$value; + } + $this->command->line('php artisan '.$commandline.$info); + if ($this->command->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { + $this->command->call($commandline, $arguments); + } else { + $this->command->callSilent($commandline, $arguments); + } + $this->command->line(''); + } +} diff --git a/app/Console/Commands/Helpers/CommandExecutorInterface.php b/app/Console/Commands/Helpers/CommandExecutorInterface.php new file mode 100644 index 000000000..63233074d --- /dev/null +++ b/app/Console/Commands/Helpers/CommandExecutorInterface.php @@ -0,0 +1,10 @@ +confirmToProceed('Checking version deactivated', function () { + return ! config('monica.check_version') && $this->getLaravel()->environment() == 'production'; + })) { return false; } @@ -55,13 +57,18 @@ class PingVersionServer extends Command // Send the JSON try { + $this->log('Call url:'.config('monica.weekly_ping_server_url')); $client = new Client(); $response = $client->post(config('monica.weekly_ping_server_url'), [ 'json' => $data, ]); } catch (\GuzzleHttp\Exception\ConnectException $e) { + $this->log('ConnectException...'); + return; } catch (\GuzzleHttp\Exception\TransferException $e) { + $this->log('TransferException...'); + return; } @@ -71,6 +78,8 @@ class PingVersionServer extends Command if (json_last_error() !== JSON_ERROR_NONE) { // JSON is invalid // The function json_last_error returns the last error occurred during the JSON encoding and decoding + $this->log('json error...'); + return; } @@ -79,6 +88,8 @@ class PingVersionServer extends Command return; } + $this->log('instance version:'.$instance->current_version); + $this->log('current version:'.$json['latest_version']); if ($json['latest_version'] != $instance->current_version) { $instance->latest_version = $json['latest_version']; $instance->latest_release_notes = $json['notes']; @@ -90,4 +101,11 @@ class PingVersionServer extends Command $instance->save(); } } + + public function log($string) + { + if ($this->getOutput()->getVerbosity() >= OutputInterface::VERBOSITY_VERBOSE) { + $this->info($string); + } + } } diff --git a/app/Console/Commands/SetupProduction.php b/app/Console/Commands/SetupProduction.php index d26536ebc..9bb0710ce 100644 --- a/app/Console/Commands/SetupProduction.php +++ b/app/Console/Commands/SetupProduction.php @@ -38,21 +38,11 @@ class SetupProduction extends Command * If the .env file does not exist, then key generation * will fail. So we create one if it does not already exist. */ - if (! file_exists('.env')) { - touch('.env'); + if (! file_exists(__DIR__.'/../../../.env')) { + touch(__DIR__.'/../../../.env'); } - $this->callSilent('migrate', ['--force' => true]); - $this->info('✓ Performed migrations'); - - $this->call('db:seed', ['--class' => 'ActivityTypesTableSeeder', '--force' => true]); - $this->info('✓ Filled the Activity Types table'); - - $this->call('db:seed', ['--class' => 'CountriesSeederTable', '--force' => true]); - $this->info('✓ Filled the Countries table'); - - $this->callSilent('storage:link'); - $this->info('✓ Symlinked the storage folder for the avatars'); + $this->callSilent('monica:update', ['--force' => true]); $this->line(''); $this->line('-----------------------------'); diff --git a/app/Console/Commands/SetupTest.php b/app/Console/Commands/SetupTest.php index 5c9a91144..5f4b7a976 100644 --- a/app/Console/Commands/SetupTest.php +++ b/app/Console/Commands/SetupTest.php @@ -32,22 +32,14 @@ class SetupTest extends Command return; } - $this->callSilent('migrate:fresh'); - $this->info('✓ Performed migrations'); + $this->artisan('✓ Performing migrations', 'migrate:fresh'); - $this->call('db:seed', ['--class' => 'ActivityTypesTableSeeder']); - $this->info('✓ Filled the Activity Types table'); - - $this->call('db:seed', ['--class' => 'CountriesSeederTable']); - $this->info('✓ Filled the Countries table'); - - $this->callSilent('storage:link'); - $this->info('✓ Symlinked the storage folder for the avatars'); + $this->artisan('✓ Filling the Activity Types table', 'db:seed', ['--class' => 'ActivityTypesTableSeeder']); + $this->artisan('✓ Filling the Countries table', 'db:seed', ['--class' => 'CountriesSeederTable']); + $this->artisan('✓ Symlink the storage folder', 'storage:link'); if (! $this->option('skipSeed')) { - $this->call('db:seed', ['--class' => 'FakeContentTableSeeder']); - $this->info(''); - $this->info('✓ Filled database with fake data'); + $this->artisan('✓ Filling database with fake data', 'db:seed', ['--class' => 'FakeContentTableSeeder']); } $this->line(''); @@ -64,4 +56,21 @@ class SetupTest extends Command $this->info('Setup is done. Have fun.'); } + + public function exec($message, $command) + { + $this->info($message); + $this->line($command); + exec($command, $output); + $this->line(implode('\n', $output)); + $this->line(''); + } + + public function artisan($message, $command, array $arguments = []) + { + $this->info($message); + $this->line('php artisan '.$command); + $this->callSilent($command, $arguments); + $this->line(''); + } } diff --git a/app/Console/Commands/Update.php b/app/Console/Commands/Update.php new file mode 100644 index 000000000..b52595324 --- /dev/null +++ b/app/Console/Commands/Update.php @@ -0,0 +1,99 @@ +commandExecutor = $commandExecutor ?: new CommandExecutor($this); + parent::__construct(); + } + + /** + * Execute the console command. + * + * @return mixed + */ + public function handle() + { + if ($this->confirmToProceed()) { + try { + $this->commandExecutor->artisan('✓ Maintenance mode: on', 'down', [ + '--message' => 'Upgrading Monica v'.config('monica.app_version'), + '--retry' => '10', + ]); + + // Clear or rebuild all cache + $this->commandExecutor->artisan('✓ Resetting application cache', 'cache:clear'); + if ($this->getLaravel()->environment() == 'production') { + $this->commandExecutor->artisan('✓ Resetting config cache', 'config:cache'); + $this->commandExecutor->artisan('✓ Resetting route cache', 'route:cache'); + if ($this->getLaravel()->version() > '5.6') { + $this->commandExecutor->artisan('✓ Resetting view cache', 'view:cache'); + } else { + $this->commandExecutor->artisan('✓ Resetting view cache', 'view:clear'); + } + } else { + $this->commandExecutor->artisan('✓ Clear config cache', 'config:clear'); + $this->commandExecutor->artisan('✓ Clear route cache', 'route:clear'); + $this->commandExecutor->artisan('✓ Clear view cache', 'view:clear'); + } + + if ($this->option('composer-install') === true) { + $this->commandExecutor->exec('✓ Updating composer dependencies', 'composer install --no-interaction --no-suggest --ignore-platform-reqs'.($this->option('composer-install') === false ? '--no-dev' : '')); + } + + $this->commandExecutor->artisan('✓ Performing migrations', 'migrate', ['--force' => 'true']); + + if (DB::table('activity_types')->count() == 0) { + $this->commandExecutor->artisan('✓ Filling the Activity Types table', 'db:seed', ['--class' => 'ActivityTypesTableSeeder', '--force' => 'true']); + } + if (DB::table('countries')->count() == 0) { + $this->commandExecutor->artisan('✓ Filling the Countries table', 'db:seed', ['--class' => 'CountriesSeederTable', '--force' => 'true']); + } + if ($this->getLaravel()->environment() != 'testing' && ! file_exists(public_path('storage'))) { + $this->commandExecutor->artisan('✓ Symlink the storage folder', 'storage:link'); + } + } finally { + $this->commandExecutor->artisan('✓ Maintenance mode: off', 'up'); + } + + $this->line('Monica v'.config('monica.app_version').' is set up, enjoy.'); + } + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 0e69addfe..948afb73b 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -13,18 +13,18 @@ class Kernel extends ConsoleKernel * @var array */ protected $commands = [ - // Commands\Inspire::class, - 'App\Console\Commands\SendNotifications', - 'App\Console\Commands\SendReminders', 'App\Console\Commands\CalculateStatistics', - 'App\Console\Commands\ImportCSV', - 'App\Console\Commands\SetupProduction', - 'App\Console\Commands\ImportVCards', - 'App\Console\Commands\PingVersionServer', - 'App\Console\Commands\SetupTest', 'App\Console\Commands\Deactivate2FA', 'App\Console\Commands\GetVersion', + 'App\Console\Commands\ImportCSV', + 'App\Console\Commands\ImportVCards', 'App\Console\Commands\LangGenerate', + 'App\Console\Commands\PingVersionServer', + 'App\Console\Commands\SendNotifications', + 'App\Console\Commands\SendReminders', + 'App\Console\Commands\SetupProduction', + 'App\Console\Commands\SetupTest', + 'App\Console\Commands\Update', ]; /** diff --git a/config/database.php b/config/database.php index 2eb23c71c..62f3143a0 100644 --- a/config/database.php +++ b/config/database.php @@ -157,8 +157,7 @@ if (env('HEROKU')) { $db['connections']['heroku'] = [ 'driver' => 'mysql', 'host' => $url['host'], - 'port' => $url['port'], - 'database' => substr($url['path'], 1), + 'database' => starts_with($url['path'], '/') ? str_after($url['path'], '/') : $url['path'], 'username' => $url['user'], 'password' => $url['pass'], 'charset' => 'utf8', @@ -166,6 +165,9 @@ if (env('HEROKU')) { 'strict' => false, 'schema' => 'public', ]; + if (array_key_exists('port', $url)) { + $db['connections']['heroku']['port'] = $url['port']; + } } return $db; diff --git a/database/seeds/ActivityTypesTableSeeder.php b/database/seeds/ActivityTypesTableSeeder.php index e410e15fe..bfa3d2433 100644 --- a/database/seeds/ActivityTypesTableSeeder.php +++ b/database/seeds/ActivityTypesTableSeeder.php @@ -11,14 +11,15 @@ class ActivityTypesTableSeeder extends Seeder */ public function run() { + $now = now(); DB::table('activity_types')->truncate(); DB::table('activity_type_groups')->truncate(); // SIMPLE ACTIVITIES DB::table('activity_type_groups')->insert([ 'key' => 'simple_activities', - 'created_at' => now(), - 'updated_at' => now(), + 'created_at' => $now, + 'updated_at' => $now, ]); DB::table('activity_types')->insert([ @@ -26,8 +27,8 @@ class ActivityTypesTableSeeder extends Seeder 'location_type' => 'outside', 'icon' => 'hang_out', 'activity_type_group_id' => 1, - 'created_at' => now(), - 'updated_at' => now(), + 'created_at' => $now, + 'updated_at' => $now, ]); DB::table('activity_types')->insert([ @@ -35,8 +36,8 @@ class ActivityTypesTableSeeder extends Seeder 'location_type' => 'my_place', 'icon' => 'movie_home', 'activity_type_group_id' => 1, - 'created_at' => now(), - 'updated_at' => now(), + 'created_at' => $now, + 'updated_at' => $now, ]); DB::table('activity_types')->insert([ @@ -44,15 +45,15 @@ class ActivityTypesTableSeeder extends Seeder 'location_type' => 'my_place', 'icon' => 'talk_home', 'activity_type_group_id' => 1, - 'created_at' => now(), - 'updated_at' => now(), + 'created_at' => $now, + 'updated_at' => $now, ]); // SPORT DB::table('activity_type_groups')->insert([ 'key' => 'sport', - 'created_at' => now(), - 'updated_at' => now(), + 'created_at' => $now, + 'updated_at' => $now, ]); DB::table('activity_types')->insert([ @@ -60,15 +61,15 @@ class ActivityTypesTableSeeder extends Seeder 'location_type' => 'outside', 'icon' => 'sport', 'activity_type_group_id' => 2, - 'created_at' => now(), - 'updated_at' => now(), + 'created_at' => $now, + 'updated_at' => $now, ]); // FOOD DB::table('activity_type_groups')->insert([ 'key' => 'food', - 'created_at' => now(), - 'updated_at' => now(), + 'created_at' => $now, + 'updated_at' => $now, ]); DB::table('activity_types')->insert([ @@ -76,8 +77,8 @@ class ActivityTypesTableSeeder extends Seeder 'location_type' => 'his_place', 'icon' => 'ate_his_place', 'activity_type_group_id' => 3, - 'created_at' => now(), - 'updated_at' => now(), + 'created_at' => $now, + 'updated_at' => $now, ]); DB::table('activity_types')->insert([ @@ -85,8 +86,8 @@ class ActivityTypesTableSeeder extends Seeder 'location_type' => 'outside', 'icon' => 'bar', 'activity_type_group_id' => 3, - 'created_at' => now(), - 'updated_at' => now(), + 'created_at' => $now, + 'updated_at' => $now, ]); DB::table('activity_types')->insert([ @@ -94,8 +95,8 @@ class ActivityTypesTableSeeder extends Seeder 'location_type' => 'my_place', 'icon' => 'ate_home', 'activity_type_group_id' => 3, - 'created_at' => now(), - 'updated_at' => now(), + 'created_at' => $now, + 'updated_at' => $now, ]); DB::table('activity_types')->insert([ @@ -103,8 +104,8 @@ class ActivityTypesTableSeeder extends Seeder 'location_type' => 'outside', 'icon' => 'picknicked', 'activity_type_group_id' => 3, - 'created_at' => now(), - 'updated_at' => now(), + 'created_at' => $now, + 'updated_at' => $now, ]); DB::table('activity_types')->insert([ @@ -112,15 +113,15 @@ class ActivityTypesTableSeeder extends Seeder 'location_type' => 'outside', 'icon' => 'restaurant', 'activity_type_group_id' => 3, - 'created_at' => now(), - 'updated_at' => now(), + 'created_at' => $now, + 'updated_at' => $now, ]); // CULTURAL DB::table('activity_type_groups')->insert([ 'key' => 'cultural_activities', - 'created_at' => now(), - 'updated_at' => now(), + 'created_at' => $now, + 'updated_at' => $now, ]); DB::table('activity_types')->insert([ @@ -128,8 +129,8 @@ class ActivityTypesTableSeeder extends Seeder 'location_type' => 'outside', 'icon' => 'theater', 'activity_type_group_id' => 4, - 'created_at' => now(), - 'updated_at' => now(), + 'created_at' => $now, + 'updated_at' => $now, ]); DB::table('activity_types')->insert([ @@ -137,8 +138,8 @@ class ActivityTypesTableSeeder extends Seeder 'location_type' => 'outside', 'icon' => 'concert', 'activity_type_group_id' => 4, - 'created_at' => now(), - 'updated_at' => now(), + 'created_at' => $now, + 'updated_at' => $now, ]); DB::table('activity_types')->insert([ @@ -146,8 +147,8 @@ class ActivityTypesTableSeeder extends Seeder 'location_type' => 'outside', 'icon' => 'play', 'activity_type_group_id' => 4, - 'created_at' => now(), - 'updated_at' => now(), + 'created_at' => $now, + 'updated_at' => $now, ]); DB::table('activity_types')->insert([ @@ -155,8 +156,8 @@ class ActivityTypesTableSeeder extends Seeder 'location_type' => 'outside', 'icon' => 'museum', 'activity_type_group_id' => 4, - 'created_at' => now(), - 'updated_at' => now(), + 'created_at' => $now, + 'updated_at' => $now, ]); } } diff --git a/phpunit.xml b/phpunit.xml index 36510a4ef..d656e5c5f 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -25,6 +25,10 @@ ./tests/Api + + + ./tests/Commands + diff --git a/phpunitpostgres.xml b/phpunitpostgres.xml index 9f84e3ba3..094e8bd30 100644 --- a/phpunitpostgres.xml +++ b/phpunitpostgres.xml @@ -25,6 +25,10 @@ ./tests/Api + + + ./tests/Commands + diff --git a/scripts/docker/test-server.sh b/scripts/docker/test-server.sh index 9a4b7d909..9b9b1c2fe 100755 --- a/scripts/docker/test-server.sh +++ b/scripts/docker/test-server.sh @@ -18,7 +18,9 @@ if [[ -z ${APP_KEY:-} || "$APP_KEY" == "ChangeMeBy32KeyLengthOrGenerated" ]]; th else echo "APP_KEY already set" fi -${ARTISAN} setup:production --force + +# Run migrations +${ARTISAN} monica:update --force # Run cron crond -b & diff --git a/tests/Commands/CommandExecutorTester.php b/tests/Commands/CommandExecutorTester.php new file mode 100644 index 000000000..a37e5ae88 --- /dev/null +++ b/tests/Commands/CommandExecutorTester.php @@ -0,0 +1,35 @@ +buffer = collect([]); + } + + public function exec($message, $commandline) + { + $this->buffer->push(['message' => $message, 'command' => $commandline]); + } + + public function artisan($message, $commandline, array $arguments = []) + { + $info = ''; + foreach ($arguments as $key => $value) { + $info = $info.' '.$key.'='.$value; + } + $this->buffer->push(['message' =>$message, 'command' => 'php artisan '.$commandline.$info]); + } +} diff --git a/tests/Commands/UpdateCommandTest.php b/tests/Commands/UpdateCommandTest.php new file mode 100644 index 000000000..4f02dc12c --- /dev/null +++ b/tests/Commands/UpdateCommandTest.php @@ -0,0 +1,46 @@ +setLaravel($this->createApplication()); + + $command->run(new \Symfony\Component\Console\Input\ArrayInput([]), new \Symfony\Component\Console\Output\NullOutput()); + + $this->assertCount(7, $commandExecutor->buffer); + $this->assertCommandContains($commandExecutor->buffer[0], 'Maintenance mode: on', 'php artisan down'); + $this->assertCommandContains($commandExecutor->buffer[1], 'Resetting application cache', 'php artisan cache:clear'); + $this->assertCommandContains($commandExecutor->buffer[5], 'Performing migrations', 'php artisan migrate'); + $this->assertCommandContains($commandExecutor->buffer[6], 'Maintenance mode: off', 'php artisan up'); + } + + public function test_update_command_composer() + { + $commandExecutor = new CommandExecutorTester(); + $command = new Update($commandExecutor); + $command->setLaravel($this->createApplication()); + + $command->run(new \Symfony\Component\Console\Input\ArrayInput(['--composer-install' => true]), new \Symfony\Component\Console\Output\NullOutput()); + + $this->assertCount(8, $commandExecutor->buffer); + $this->assertCommandContains($commandExecutor->buffer[0], 'Maintenance mode: on', 'php artisan down'); + $this->assertCommandContains($commandExecutor->buffer[1], 'Resetting application cache', 'php artisan cache:clear'); + $this->assertCommandContains($commandExecutor->buffer[5], 'Updating composer dependencies', 'composer install'); + $this->assertCommandContains($commandExecutor->buffer[6], 'Performing migrations', 'php artisan migrate'); + $this->assertCommandContains($commandExecutor->buffer[7], 'Maintenance mode: off', 'php artisan up'); + } + + public function assertCommandContains($array, $message, $command) + { + $this->assertContains($message, $array['message']); + $this->assertContains($command, $array['command']); + } +} diff --git a/tests/Unit/ContactTest.php b/tests/Unit/ContactTest.php index a5d307abe..e94482a7d 100644 --- a/tests/Unit/ContactTest.php +++ b/tests/Unit/ContactTest.php @@ -842,9 +842,10 @@ class ContactTest extends FeatureTestCase public function test_it_sets_a_relationship_between_two_contacts() { - $contact = factory(Contact::class)->create(['account_id' => 1]); - $partner = factory(Contact::class)->create(['account_id' => 1]); - $relationshipType = factory('App\RelationshipType')->create(['account_id' => 1]); + $account = factory('App\Account')->create([]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); + $partner = factory(Contact::class)->create(['account_id' => $account->id]); + $relationshipType = factory('App\RelationshipType')->create(['account_id' => $account->id]); $contact->setRelationship($partner, $relationshipType->id); @@ -921,12 +922,13 @@ class ContactTest extends FeatureTestCase public function test_it_deletes_relationship_between_two_contacts_and_deletes_the_contact() { - $contact = factory(Contact::class)->create(['account_id' => 1]); + $account = factory('App\Account')->create([]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); $partner = factory(Contact::class)->create([ - 'account_id' => 1, + 'account_id' => $account->id, 'is_partial' => true, ]); - $relationshipType = factory('App\RelationshipType')->create(['account_id' => 1]); + $relationshipType = factory('App\RelationshipType')->create(['account_id' => $account->id]); $contact->setRelationship($partner, $relationshipType->id); @@ -944,12 +946,13 @@ class ContactTest extends FeatureTestCase public function test_it_deletes_relationship_between_two_contacts_and_doesnt_delete_the_contact() { - $contact = factory(Contact::class)->create(['account_id' => 1]); + $account = factory('App\Account')->create([]); + $contact = factory(Contact::class)->create(['account_id' => $account->id]); $partner = factory(Contact::class)->create([ - 'account_id' => 1, + 'account_id' => $account->id, 'is_partial' => false, ]); - $relationshipType = factory('App\RelationshipType')->create(['account_id' => 1]); + $relationshipType = factory('App\RelationshipType')->create(['account_id' => $account->id]); $contact->setRelationship($partner, $relationshipType->id);