Browse Source

Merge branch 'master' into 3.next

Mark Story 8 years ago
parent
commit
f175297f86

+ 1 - 0
.travis.yml

@@ -41,6 +41,7 @@ matrix:
       env: PHPSTAN=1 DEFAULT=0
 
 before_install:
+  - echo cakephp version && tail -1 VERSION.txt
   - phpenv config-rm xdebug.ini
 
   - if [ $DB = 'mysql' ]; then mysql -u root -e 'CREATE DATABASE cakephp_test;'; fi

+ 4 - 0
src/Database/Schema/PostgresSchema.php

@@ -196,6 +196,10 @@ class PostgresSchema extends BaseSchema
             return null;
         }
 
+        if (strpos($default, 'NULL::') === 0) {
+            return null;
+        }
+
         // Remove quotes and postgres casts
         return preg_replace(
             "/^'(.*)'(?:::.*)$/",

+ 6 - 2
src/Http/BaseApplication.php

@@ -124,7 +124,10 @@ abstract class BaseApplication implements
             $name = str_replace('/', '\\', $name) . '\\' . 'Plugin';
         }
         if (!class_exists($name)) {
-            throw new InvalidArgumentException("The `{$name}` plugin cannot be found");
+            throw new InvalidArgumentException(
+                "The plugin class `{$name}` cannot be found. " .
+                'Ensure your autoloader is correct.'
+            );
         }
         $plugin = new $name($config);
         if (!$plugin instanceof PluginInterface) {
@@ -163,9 +166,10 @@ abstract class BaseApplication implements
     public function routes($routes)
     {
         if (!Router::$initialized) {
-            require $this->configDir . '/routes.php';
             // Prevent routes from being loaded again
             Router::$initialized = true;
+
+            require $this->configDir . '/routes.php';
         }
     }
 

+ 4 - 3
src/ORM/Rule/ExistsIn.php

@@ -92,6 +92,7 @@ class ExistsIn
             $this->_repository = $repository;
         }
 
+        $fields = $this->_fields;
         $source = $target = $this->_repository;
         $isAssociation = $target instanceof Association;
         $bindingKey = $isAssociation ? (array)$target->getBindingKey() : (array)$target->getPrimaryKey();
@@ -118,9 +119,9 @@ class ExistsIn
 
         if ($this->_options['allowNullableNulls']) {
             $schema = $source->getSchema();
-            foreach ($this->_fields as $i => $field) {
+            foreach ($fields as $i => $field) {
                 if ($schema->getColumn($field) && $schema->isNullable($field) && $entity->get($field) === null) {
-                    unset($bindingKey[$i], $this->_fields[$i]);
+                    unset($bindingKey[$i], $fields[$i]);
                 }
             }
         }
@@ -131,7 +132,7 @@ class ExistsIn
         );
         $conditions = array_combine(
             $primary,
-            $entity->extract($this->_fields)
+            $entity->extract($fields)
         );
 
         return $target->exists($conditions);

+ 13 - 13
src/Routing/Route/Route.php

@@ -631,19 +631,6 @@ class Route
         unset($context['params']);
         $hostOptions = array_intersect_key($url, $context);
 
-        // Check for properties that will cause an
-        // absolute url. Copy the other properties over.
-        if (isset($hostOptions['_scheme']) ||
-            isset($hostOptions['_port']) ||
-            isset($hostOptions['_host'])
-        ) {
-            $hostOptions += $context;
-
-            if ($hostOptions['_port'] == $context['_port']) {
-                unset($hostOptions['_port']);
-            }
-        }
-
         // Apply the _host option if possible
         if (isset($this->options['_host'])) {
             if (!isset($hostOptions['_host']) && strpos($this->options['_host'], '*') === false) {
@@ -659,6 +646,19 @@ class Route
             }
         }
 
+        // Check for properties that will cause an
+        // absolute url. Copy the other properties over.
+        if (isset($hostOptions['_scheme']) ||
+            isset($hostOptions['_port']) ||
+            isset($hostOptions['_host'])
+        ) {
+            $hostOptions += $context;
+
+            if (getservbyname($hostOptions['_scheme'], 'tcp') === $hostOptions['_port']) {
+                unset($hostOptions['_port']);
+            }
+        }
+
         // If no base is set, copy one in.
         if (!isset($hostOptions['_base']) && isset($context['_base'])) {
             $hostOptions['_base'] = $context['_base'];

+ 38 - 35
tests/TestCase/Database/Schema/PostgresSchemaTest.php

@@ -97,119 +97,128 @@ SQL;
         return [
             // Timestamp
             [
-                'TIMESTAMP',
+                ['type' => 'TIMESTAMP'],
                 ['type' => 'timestamp', 'length' => null]
             ],
             [
-                'TIMESTAMP WITHOUT TIME ZONE',
+                ['type' => 'TIMESTAMP WITHOUT TIME ZONE'],
                 ['type' => 'timestamp', 'length' => null]
             ],
             // Date & time
             [
-                'DATE',
+                ['type' => 'DATE'],
                 ['type' => 'date', 'length' => null]
             ],
             [
-                'TIME',
+                ['type' => 'TIME'],
                 ['type' => 'time', 'length' => null]
             ],
             [
-                'TIME WITHOUT TIME ZONE',
+                ['type' => 'TIME WITHOUT TIME ZONE'],
                 ['type' => 'time', 'length' => null]
             ],
             // Integer
             [
-                'SMALLINT',
+                ['type' => 'SMALLINT'],
                 ['type' => 'smallinteger', 'length' => 5]
             ],
             [
-                'INTEGER',
+                ['type' => 'INTEGER'],
                 ['type' => 'integer', 'length' => 10]
             ],
             [
-                'SERIAL',
+                ['type' => 'SERIAL'],
                 ['type' => 'integer', 'length' => 10]
             ],
             [
-                'BIGINT',
+                ['type' => 'BIGINT'],
                 ['type' => 'biginteger', 'length' => 20]
             ],
             [
-                'BIGSERIAL',
+                ['type' => 'BIGSERIAL'],
                 ['type' => 'biginteger', 'length' => 20]
             ],
             // Decimal
             [
-                'NUMERIC',
+                ['type' => 'NUMERIC'],
                 ['type' => 'decimal', 'length' => null, 'precision' => null]
             ],
             [
-                'DECIMAL(10,2)',
+                ['type' => 'NUMERIC', 'default' => 'NULL::numeric'],
+                ['type' => 'decimal', 'length' => null, 'precision' => null, 'default' => null]
+            ],
+            [
+                ['type' => 'DECIMAL(10,2)', 'column_precision' => 10, 'column_scale' => 2],
                 ['type' => 'decimal', 'length' => 10, 'precision' => 2]
             ],
             // String
             [
-                'VARCHAR',
+                ['type' => 'VARCHAR'],
                 ['type' => 'string', 'length' => null]
             ],
             [
-                'VARCHAR(10)',
+                ['type' => 'VARCHAR(10)'],
                 ['type' => 'string', 'length' => 10]
             ],
             [
-                'CHARACTER VARYING',
+                ['type' => 'CHARACTER VARYING'],
                 ['type' => 'string', 'length' => null]
             ],
             [
-                'CHARACTER VARYING(10)',
+                ['type' => 'CHARACTER VARYING(10)'],
                 ['type' => 'string', 'length' => 10]
             ],
             [
-                'CHAR(10)',
+                ['type' => 'CHARACTER VARYING(255)', 'default' => 'NULL::character varying'],
+                ['type' => 'string', 'length' => 255, 'default' => null]
+            ],
+            [
+                ['type' => 'CHAR(10)'],
                 ['type' => 'string', 'fixed' => true, 'length' => 10]
             ],
             [
-                'CHARACTER(10)',
+                ['type' => 'CHARACTER(10)'],
                 ['type' => 'string', 'fixed' => true, 'length' => 10]
             ],
             [
-                'MONEY',
+                ['type' => 'MONEY'],
                 ['type' => 'string', 'length' => null]
             ],
             // UUID
             [
-                'UUID',
+                ['type' => 'UUID'],
                 ['type' => 'uuid', 'length' => null]
             ],
             [
-                'INET',
+                ['type' => 'INET'],
                 ['type' => 'string', 'length' => 39]
             ],
             // Text
             [
-                'TEXT',
+                ['type' => 'TEXT'],
                 ['type' => 'text', 'length' => null]
             ],
             // Blob
             [
-                'BYTEA',
+                ['type' => 'BYTEA'],
                 ['type' => 'binary', 'length' => null]
             ],
             // Float
             [
-                'REAL',
+                ['type' => 'REAL'],
                 ['type' => 'float', 'length' => null]
             ],
             [
-                'DOUBLE PRECISION',
+                ['type' => 'DOUBLE PRECISION'],
                 ['type' => 'float', 'length' => null]
             ],
+            // Json
             [
-                'JSON',
+                ['type' => 'JSON'],
                 ['type' => 'json', 'length' => null]
             ],
             [
-                'JSONB',
+                ['type' => 'JSONB'],
                 ['type' => 'json', 'length' => null]
             ],
         ];
@@ -221,11 +230,10 @@ SQL;
      * @dataProvider convertColumnProvider
      * @return void
      */
-    public function testConvertColumn($type, $expected)
+    public function testConvertColumn($field, $expected)
     {
-        $field = [
+        $field += [
             'name' => 'field',
-            'type' => $type,
             'null' => 'YES',
             'default' => 'Default value',
             'comment' => 'Comment section',
@@ -241,11 +249,6 @@ SQL;
             'collate' => 'ja_JP.utf8',
         ];
 
-        if ($field['type'] === 'NUMERIC' || strpos($field['type'], 'DECIMAL') !== false) {
-            $field['column_precision'] = $expected['length'];
-            $field['column_scale'] = $expected['precision'];
-        }
-
         $driver = $this->getMockBuilder('Cake\Database\Driver\Postgres')->getMock();
         $dialect = new PostgresSchema($driver);
 

+ 25 - 0
tests/TestCase/Http/BaseApplicationTest.php

@@ -22,6 +22,7 @@ use Cake\Http\Response;
 use Cake\Http\ServerRequestFactory;
 use Cake\Routing\RouteBuilder;
 use Cake\Routing\RouteCollection;
+use Cake\Routing\Router;
 use Cake\TestSuite\TestCase;
 use InvalidArgumentException;
 use TestPlugin\Plugin as TestPlugin;
@@ -211,4 +212,28 @@ class BaseApplicationTest extends TestCase
             'Nested plugin should have bootstrap run'
         );
     }
+
+    /**
+     * Ensure that Router::$initialized is toggled even if the routes
+     * file fails. This prevents the routes file from being re-parsed
+     * during the error handling process.
+     *
+     * @return void
+     */
+    public function testRouteHookInitializesRouterOnError()
+    {
+        $app = $this->getMockForAbstractClass(
+            'Cake\Http\BaseApplication',
+            [TEST_APP . 'invalid_routes' . DS]
+        );
+        $builder = Router::createRouteBuilder('/');
+        try {
+            $app->routes($builder);
+
+            $this->fail('invalid_routes/routes.php file should raise an error.');
+        } catch (\InvalidArgumentException $e) {
+            $this->assertTrue(Router::$initialized, 'Should be toggled to prevent duplicate route errors');
+            $this->assertContains('route class', $e->getMessage());
+        }
+    }
 }

+ 47 - 77
tests/TestCase/ORM/RulesCheckerIntegrationTest.php

@@ -890,9 +890,9 @@ class RulesCheckerIntegrationTest extends TestCase
     /**
      * Tests new allowNullableNulls flag with author id set to null
      *
-     * @return
+     * @return void
      */
-    public function testExistsInAllowNullableNullsWithAuthorIdNullA()
+    public function testExistsInAllowNullableNullsOn()
     {
         $entity = new Entity([
             'id' => 10,
@@ -913,9 +913,9 @@ class RulesCheckerIntegrationTest extends TestCase
     /**
      * Tests new allowNullableNulls flag with author id set to null
      *
-     * @return
+     * @return void
      */
-    public function testExistsInAllowNullableNullsWithAuthorIdNullB()
+    public function testExistsInAllowNullableNullsOff()
     {
         $entity = new Entity([
             'id' => 10,
@@ -938,7 +938,7 @@ class RulesCheckerIntegrationTest extends TestCase
      *
      * @return
      */
-    public function testExistsInAllowNullableNullsWithAuthorIdNullC()
+    public function testExistsInAllowNullableNullsDefaultValue()
     {
         $entity = new Entity([
             'id' => 10,
@@ -959,7 +959,7 @@ class RulesCheckerIntegrationTest extends TestCase
      *
      * @return
      */
-    public function testExistsInAllowNullableNullsWithAuthorIdNullD()
+    public function testExistsInAllowNullableNullsCustomMessage()
     {
         $entity = new Entity([
             'id' => 10,
@@ -980,35 +980,11 @@ class RulesCheckerIntegrationTest extends TestCase
     }
 
     /**
-     * Tests new allowNullableNulls flag with author id set to null
-     *
-     * @return
-     */
-    public function testExistsInAllowNullableNullsWithAuthorIdNullE()
-    {
-        $entity = new Entity([
-            'id' => 10,
-            'author_id' => null,
-            'site_id' => 1,
-            'name' => 'New Site Article without Author',
-        ]);
-        $table = $this->getTableLocator()->get('SiteArticles');
-        $table->belongsTo('SiteAuthors');
-        $rules = $table->rulesChecker();
-
-        $rules->add($rules->existsIn(['author_id', 'site_id'], 'SiteAuthors', [
-            'allowNullableNulls' => true,
-            'message' => 'Niente'
-        ]));
-        $this->assertInstanceOf('Cake\ORM\Entity', $table->save($entity));
-    }
-
-    /**
      * Tests new allowNullableNulls flag with author id set to 1
      *
      * @return
      */
-    public function testExistsInAllowNullableNullsWithAuthorId1A()
+    public function testExistsInAllowNullableNullsOnAllKeysSet()
     {
         $entity = new Entity([
             'id' => 10,
@@ -1029,7 +1005,7 @@ class RulesCheckerIntegrationTest extends TestCase
      *
      * @return
      */
-    public function testExistsInAllowNullableNullsWithAuthorIdB()
+    public function testExistsInAllowNullableNullsOffAllKeysSet()
     {
         $entity = new Entity([
             'id' => 10,
@@ -1050,28 +1026,7 @@ class RulesCheckerIntegrationTest extends TestCase
      *
      * @return
      */
-    public function testExistsInAllowNullableNullsWithAuthorId1C()
-    {
-        $entity = new Entity([
-            'id' => 10,
-            'author_id' => 1,
-            'site_id' => 1,
-            'name' => 'New Site Article with Author',
-        ]);
-        $table = $this->getTableLocator()->get('SiteArticles');
-        $table->belongsTo('SiteAuthors');
-        $rules = $table->rulesChecker();
-
-        $rules->add($rules->existsIn(['author_id', 'site_id'], 'SiteAuthors'));
-        $this->assertInstanceOf('Cake\ORM\Entity', $table->save($entity));
-    }
-
-    /**
-     * Tests new allowNullableNulls flag with author id set to 1
-     *
-     * @return
-     */
-    public function testExistsInAllowNullableNullsWithAuthorId1E()
+    public function testExistsInAllowNullableNullsOnAllKeysCustomMessage()
     {
         $entity = new Entity([
             'id' => 10,
@@ -1090,15 +1045,15 @@ class RulesCheckerIntegrationTest extends TestCase
     }
 
     /**
-     * Tests new allowNullableNulls flag with author id set to 1
+     * Tests new allowNullableNulls flag with author id set to 99999999 (does not exist)
      *
      * @return
      */
-    public function testExistsInAllowNullableNullsWithAuthorId1F()
+    public function testExistsInAllowNullableNullsOnInvalidKey()
     {
         $entity = new Entity([
             'id' => 10,
-            'author_id' => 1,
+            'author_id' => 99999999,
             'site_id' => 1,
             'name' => 'New Site Article with Author',
         ]);
@@ -1107,22 +1062,24 @@ class RulesCheckerIntegrationTest extends TestCase
         $rules = $table->rulesChecker();
 
         $rules->add($rules->existsIn(['author_id', 'site_id'], 'SiteAuthors', [
-            'allowNullableNulls' => false,
-            'message' => 'will not error']));
-        $this->assertInstanceOf('Cake\ORM\Entity', $table->save($entity));
+            'allowNullableNulls' => true,
+            'message' => 'will error']));
+        $this->assertFalse($table->save($entity));
+        $this->assertEquals(['author_id' => ['_existsIn' => 'will error']], $entity->getErrors());
     }
 
     /**
      * Tests new allowNullableNulls flag with author id set to 99999999 (does not exist)
+     * and site_id set to 99999999 (does not exist)
      *
      * @return
      */
-    public function testExistsInAllowNullableNullsWithAuthorId1G()
+    public function testExistsInAllowNullableNullsOnInvalidKeys()
     {
         $entity = new Entity([
             'id' => 10,
             'author_id' => 99999999,
-            'site_id' => 1,
+            'site_id' => 99999999,
             'name' => 'New Site Article with Author',
         ]);
         $table = $this->getTableLocator()->get('SiteArticles');
@@ -1137,16 +1094,16 @@ class RulesCheckerIntegrationTest extends TestCase
     }
 
     /**
-     * Tests new allowNullableNulls flag with author id set to 99999999 (does not exist)
+     * Tests new allowNullableNulls flag with author id set to 1 (does exist)
      * and site_id set to 99999999 (does not exist)
      *
      * @return
      */
-    public function testExistsInAllowNullableNullsWithAuthorId1H()
+    public function testExistsInAllowNullableNullsOnInvalidKeySecond()
     {
         $entity = new Entity([
             'id' => 10,
-            'author_id' => 99999999,
+            'author_id' => 1,
             'site_id' => 99999999,
             'name' => 'New Site Article with Author',
         ]);
@@ -1162,28 +1119,41 @@ class RulesCheckerIntegrationTest extends TestCase
     }
 
     /**
-     * Tests new allowNullableNulls flag with author id set to 1 (does exist)
-     * and site_id set to 99999999 (does not exist)
+     * Tests new allowNullableNulls with saveMany
      *
      * @return
      */
-    public function testExistsInAllowNullableNullsWithAuthorId1I()
+    public function testExistsInAllowNullableNullsSaveMany()
     {
-        $entity = new Entity([
-            'id' => 10,
-            'author_id' => 1,
-            'site_id' => 99999999,
-            'name' => 'New Site Article with Author',
-        ]);
+        $entities = [
+            new Entity([
+                'id' => 1,
+                'author_id' => null,
+                'site_id' => 1,
+                'name' => 'New Site Article without Author',
+            ]),
+            new Entity([
+                'id' => 2,
+                'author_id' => 1,
+                'site_id' => 1,
+                'name' => 'New Site Article with Author',
+            ]),
+        ];
         $table = $this->getTableLocator()->get('SiteArticles');
         $table->belongsTo('SiteAuthors');
         $rules = $table->rulesChecker();
 
         $rules->add($rules->existsIn(['author_id', 'site_id'], 'SiteAuthors', [
             'allowNullableNulls' => true,
-            'message' => 'will error']));
-        $this->assertFalse($table->save($entity));
-        $this->assertEquals(['author_id' => ['_existsIn' => 'will error']], $entity->getErrors());
+            'message' => 'will error with array_combine warning']));
+        $result = $table->saveMany($entities);
+        $this->assertCount(2, $result);
+
+        $this->assertInstanceOf(Entity::class, $result[0]);
+        $this->assertEmpty($result[0]->getErrors());
+
+        $this->assertInstanceOf(Entity::class, $result[1]);
+        $this->assertEmpty($result[1]->getErrors());
     }
 
     /**

+ 34 - 1
tests/TestCase/Routing/Route/RouteTest.php

@@ -702,13 +702,15 @@ class RouteTest extends TestCase
             ['controller' => 'posts', 'action' => 'index', '_host' => 'example.com'],
             $context
         );
+        // Http has port 80 as default, do not include it in the url
         $this->assertEquals('http://example.com/posts/index', $result);
 
         $result = $route->match(
             ['controller' => 'posts', 'action' => 'index', '_scheme' => 'webcal'],
             $context
         );
-        $this->assertEquals('webcal://foo.com/posts/index', $result);
+        // Webcal is not on port 80 by default, include it in url
+        $this->assertEquals('webcal://foo.com:80/posts/index', $result);
 
         $result = $route->match(
             ['controller' => 'posts', 'action' => 'index', '_port' => '8080'],
@@ -734,6 +736,26 @@ class RouteTest extends TestCase
             $context
         );
         $this->assertEquals('https://example.com:8080/dir/posts/index', $result);
+
+        $context = [
+            '_host' => 'foo.com',
+            '_scheme' => 'http',
+            '_port' => 8080,
+            '_base' => ''
+        ];
+        $result = $route->match(
+            [
+                'controller' => 'posts',
+                'action' => 'index',
+                '_port' => '8080',
+                '_host' => 'example.com',
+                '_scheme' => 'https',
+                '_base' => '/dir'
+            ],
+            $context
+        );
+        // Https scheme is not on port 8080 by default, include the port
+        $this->assertEquals('https://example.com:8080/dir/posts/index', $result);
     }
 
     /**
@@ -800,6 +822,17 @@ class RouteTest extends TestCase
             '_host' => 'foo.example.com'
         ]);
         $this->assertSame('http://foo.example.com/fallback', $result);
+
+        $result = $route->match([
+            'controller' => 'Articles',
+            'action' => 'index',
+        ], [
+            '_scheme' => 'https',
+            '_host' => 'foo.example.com',
+            '_port' => 8080
+        ]);
+        // When the port and scheme in the context are not present in the original url, they should be added
+        $this->assertSame('https://foo.example.com:8080/fallback', $result);
     }
 
     /**

+ 7 - 0
tests/test_app/invalid_routes/routes.php

@@ -0,0 +1,7 @@
+<?php
+/**
+ * Test routes file with routes that trigger a missing route class error.
+ * Application requests should have InvalidArgument error rendered.
+ */
+$routes->setRouteClass('DoesNotExist');
+$routes->get('/', ['controller' => 'Pages']);