Inspections

Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

$inspect('Example Inspection', function () {

    // Try changing this to: contains('example.org')
    $this->assert('bob.smith@example.com')->contains('example.com');

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

$inspect('Tell_Message', function() {

    $this->try('_()', function() {

        // @todo

    });

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

// ---------------------------------------------------------------------------------------------

$inspect('Tell_Bridge_Card', function() {

    // @todo

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

// -------------------------------------------------------------------------------------------------

class Inspect_Test_Auth_Bearer extends Tell_Auth_Bearer
{
    public function authenticate()
    {
        if ('rkbsRv9iVvla2unTCXkwNdRTWxZrfRmA' === $this->token()) {
            return [
                'first_name' => 'Bob',
                'last_name'  => 'Smith',
                'email'      => 'bob@example.com',
            ];
        }

        return FALSE;
    }

    public $test = [
        'onAuthenticate'        => [],
        'onAuthenticateSuccess' => [],
        'onAuthenticateFailure' => [],
        'onAuthenticateRevoke'  => [],
        'onPersist'             => [],
        'onPersistSuccess'      => [],
        'onPersistFailure'      => [],
        'onAuthorize'           => [],
        'onAuthorizeSuccess'    => [],
    ];

    public function onAuthenticate(...$args)
    {
        $this->test['onAuthenticate'] = $args;
    }

    public function onAuthenticateSuccess(...$args)
    {
        $this->test['onAuthenticateSuccess'] = $args;
    }

    public function onAuthenticateFailure(...$args)
    {
        $this->test['onAuthenticateFailure'] = $args;
    }

    public function onAuthenticateRevoke(...$args)
    {
        $this->test['onAuthenticateRevoke'] = $args;
    }

    public function onPersist(...$args)
    {
        $this->test['onPersist'] = $args;
    }

    public function onPersistSuccess(...$args)
    {
        $this->test['onPersistSuccess'] = $args;
    }

    public function onPersistFailure(...$args)
    {
        $this->test['onPersistFailure'] = $args;
    }

    public function onAuthorize(...$args)
    {
        $this->test['onAuthorize'] = $args;
    }

    public function onAuthorizeSuccess(...$args)
    {
        $this->test['onAuthorizeSuccess'] = $args;
    }
}

// -------------------------------------------------------------------------------------------------

$inspect('Tell_Auth_Bearer >> Instantiate', function(Inspect_Test_Auth_Bearer $auth) {

    $this->assert($auth->test['onAuthenticate'])->array()->empty();

    $this->assert($auth->test['onAuthenticateSuccess'])->array()->empty();

    $this->assert($auth->test['onAuthenticateFailure'])->array()->hasKey(0)->hasKey(1);

    $this->assert($auth->test['onAuthenticateFailure'][0])->false();

    $this->assert($auth->test['onAuthenticateFailure'][1])->instanceOf($auth);

    $this->assert($auth->test['onPersist'])->array()->empty();

    $this->assert($auth->test['onPersistSuccess'])->array()->empty();

    $this->assert($auth->test['onPersistFailure'])->array()->empty();

    $this->assert($auth->test['onAuthorize'])->array()->empty();

    $this->assert($auth->test['onAuthorizeSuccess'])->array()->empty();

});

// -------------------------------------------------------------------------------------------------

$inspect('Tell_Auth_Bearer >> Valid Bearer', function(Inspect_Test_Auth_Bearer $auth) {

    $this->assert($auth->test['onAuthenticate'])->array()->hasKey(0)->hasKey(1);

    $this->assert($auth->test['onAuthenticate'][0])->equals([
        'first_name' => 'Bob',
        'last_name'  => 'Smith',
        'email'      => 'bob@example.com',
    ]);

    $this->assert($auth->test['onAuthenticate'][1])->instanceOf($auth);

    $this->assert($auth->test['onAuthenticateSuccess'])->equals($auth->test['onAuthenticate']);

    $this->assert($auth->test['onAuthenticateFailure'])->array()->empty();

    $this->assert($auth->test['onPersist'])->array()->empty();

    $this->assert($auth->test['onPersistSuccess'])->array()->empty();

    $this->assert($auth->test['onPersistFailure'])->array()->empty();

    $this->assert($auth->test['onAuthorize'])->array()->empty();

    $this->assert($auth->test['onAuthorizeSuccess'])->array()->empty();

})->request([
    'method' => 'GET',
    'server' => [
        'PHP_AUTH_TOKEN' => 'rkbsRv9iVvla2unTCXkwNdRTWxZrfRmA',
    ],
]);

// -------------------------------------------------------------------------------------------------
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

$inspect('Tell_Jwt', function() {

    $alive = [
        'exp'  => strtotime('+5 minutes', strtotime($this->date)),
        'data' => [
            'user_id'    => 123,
            'first_name' => 'Çelik',
            'last_name'  => 'Gonçalves',
        ],
    ];

    $expired = [
        'exp'  => strtotime('-1 minute', strtotime($this->date)),
        'data' => [
            'user_id'    => 123,
            'first_name' => 'Çelik',
            'last_name'  => 'Gonçalves',
        ],
    ];

    $tests = [
        [
            'throw'   => NULL,
            'payload' => $alive,
            'alg'     => 'HS256',
            'jwt'     => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NDQ2MzUxMDAsImRhdGEiOnsidXNlcl9pZCI6MTIzLCJmaXJzdF9uYW1lIjoiXHUwMGM3ZWxpayIsImxhc3RfbmFtZSI6Ikdvblx1MDBlN2FsdmVzIn19.ky0_9EMJiR0VG0KGfNzZktYAw1itplhWB0ADL93F0cw',
        ],
        [
            'throw'   => NULL,
            'payload' => $alive,
            'alg'     => 'HS384',
            'jwt'     => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzM4NCJ9.eyJleHAiOjE2NDQ2MzUxMDAsImRhdGEiOnsidXNlcl9pZCI6MTIzLCJmaXJzdF9uYW1lIjoiXHUwMGM3ZWxpayIsImxhc3RfbmFtZSI6Ikdvblx1MDBlN2FsdmVzIn19.JXG8ZDtnqAo8CB-1-lbIOqynrkQke7Qu4o_9dFZ9c_K_x-PUIQNAeVOYBZegen1z',
        ],
        [
            'throw'   => NULL,
            'payload' => $alive,
            'alg'     => 'HS512',
            'jwt'     => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJleHAiOjE2NDQ2MzUxMDAsImRhdGEiOnsidXNlcl9pZCI6MTIzLCJmaXJzdF9uYW1lIjoiXHUwMGM3ZWxpayIsImxhc3RfbmFtZSI6Ikdvblx1MDBlN2FsdmVzIn19.-WtrSAKiq7v3IS2baU57tSw-dMsQokuvf0-2tT8mq1EgGb485P0I1LQR4j8w5Z-QmNjziBZMa8r705JNEmT9Xw',
        ],
        [
            'throw'   => Tell_Jwt_Exception_Expired::class, // Token expired 1 minute ago
            'payload' => $expired,
            'alg'     => 'HS256',
            'jwt'     => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NDQ2MzQ3NDAsImRhdGEiOnsidXNlcl9pZCI6MTIzLCJmaXJzdF9uYW1lIjoiXHUwMGM3ZWxpayIsImxhc3RfbmFtZSI6Ikdvblx1MDBlN2FsdmVzIn19.avmYNA3BqlSPDwgonooqGpQCmOTtJcLyqUNyUmDH6Lk',
        ],
        [
            'throw'   => Tell_Jwt_Exception_Invalid::class, // Messed up header segment
            'payload' => $alive,
            'alg'     => 'HS256',
            'jwt'     => 'AAAAAAAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NDQ2MzUxMDAsImRhdGEiOnsidXNlcl9pZCI6MTIzLCJmaXJzdF9uYW1lIjoiXHUwMGM3ZWxpayIsImxhc3RfbmFtZSI6Ikdvblx1MDBlN2FsdmVzIn19.ky0_9EMJiR0VG0KGfNzZktYAw1itplhWB0ADL93F0cw',
        ],
        [
            'throw'   => Tell_Jwt_Exception_Invalid::class, // Missing segment
            'payload' => $alive,
            'alg'     => 'HS256',
            'jwt'     => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9eyJleHAiOjE2NDQ2MzUxMDAsImRhdGEiOnsidXNlcl9pZCI6MTIzLCJmaXJzdF9uYW1lIjoiXHUwMGM3ZWxpayIsImxhc3RfbmFtZSI6Ikdvblx1MDBlN2FsdmVzIn19.ky0_9EMJiR0VG0KGfNzZktYAw1itplhWB0ADL93F0cw',
        ],
        [
            'throw'   => NULL,
            'payload' => $alive,
            'alg'     => 'RS256',
            'jwt'     => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJleHAiOjE2NDQ2MzUxMDAsImRhdGEiOnsidXNlcl9pZCI6MTIzLCJmaXJzdF9uYW1lIjoiXHUwMGM3ZWxpayIsImxhc3RfbmFtZSI6Ikdvblx1MDBlN2FsdmVzIn19.O-jfcjL01JOzdzgjP35oduD_k5dR62X_jhTm2I29ORq7mBqFhouNdl1CuIblNmnyw_X5TALSsFxrPWmGXQZ1xID4CP3l0lJAWTl--KzKbVOlN3jTv6b-Jgu9nRzojXEroZScxtrZnIS2wdGnkcs7jY65NujA5ttR4wHpjJ-eoCZ5QQo3D6tFlo-8tXsWeXt_p0yD8BmKnaekhKBDRrrPxFdMPX6ziQAx-Q_isJey-Q1l-q7H0JsGVKRw96a1h_NRArHRSNqQvZ1exuzLM5-RPG-iBarMfrWIY44kDIJ5TlIcnuSArNn95XlxxzP7ugfkcLSixH7WRvikbnvp7H0dGg',
        ],
        [
            'throw'   => NULL,
            'payload' => $alive,
            'alg'     => 'RS384',
            'jwt'     => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzM4NCJ9.eyJleHAiOjE2NDQ2MzUxMDAsImRhdGEiOnsidXNlcl9pZCI6MTIzLCJmaXJzdF9uYW1lIjoiXHUwMGM3ZWxpayIsImxhc3RfbmFtZSI6Ikdvblx1MDBlN2FsdmVzIn19.buNeUn__x4tkRouBc5AAh5X0e5rZeY8JRd4kC2Npt9oLkEIqH4cv4lPDEakN5HWhxEd68LQ1S8O488bY1a25_qfXP3P7yZo9i1XSelcGwMSIYwqzg4raYB7G2Nmfo0lTueNJdAHH4um2JSljJbjJh6PUAwMNOTNErfvb7lpVMCEs6Bty_nAuZ5ZX5rfndnyXSK5GmzToYAvtLvYtY_GExkrQOQCg24Xnar9HHiVn2O3EIM2g1ywSp0wzm2b67nq8nf6Girv5F_hsdDEtcVukechhDYm66wuhSGXGp4Hmi-GMo04_ovGBKMTP0-s7MHdkGhjIYHL9qTpk945D5cioDA',
        ],
        [
            'throw'   => NULL,
            'payload' => $alive,
            'alg'     => 'RS512',
            'jwt'     => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJleHAiOjE2NDQ2MzUxMDAsImRhdGEiOnsidXNlcl9pZCI6MTIzLCJmaXJzdF9uYW1lIjoiXHUwMGM3ZWxpayIsImxhc3RfbmFtZSI6Ikdvblx1MDBlN2FsdmVzIn19.DRcP3-cr5WmTN-gF_8xCJi7t2qA8pq4q4O-1gJQTqAkwiWDvdcFdipyAdtar5jkOHtultyKOCaWdq-HFdJP0NzkSmT2HXZQ7Ex56lXUBt2vD0Rmj1kx4iGjuY3ZnpSmHd8Z9MElj78Ru2eeMoCC5Aq4Nvzz29bso7jsk5TM0B6ncvT4Xn-06kUe_fZ9gZ1Nr1hvFmkCZ3UZCG9o6ivPt63uonLV3gJsnwsxgumuGRikdzBAZ6mv1sfSyE3m9cIvyep4h2jyIu3lxu84BjUxzsaASjoQa7ecxdANbyL8gD_-AHKL55kSSJrKMLrdXuWOCSh_RCYYGYzJnWDOGQ38R6w',
        ],
    ];

    Tell_Jwt::time(strtotime($this->date));

    Tell_Jwt::leeway(0);

    $this->try('encode()', function() use ($tests) {

        foreach ($tests as $t) {

            if ($t['throw']) {
                continue;
            }

            $key = $t['alg'][0] === 'H' ? $this->hash_key : $this->private_key;

            $jwt = Tell_Jwt::encode($t['payload'], $key, $t['alg']);

            $bad = function() use ($t, $key) {
                return Tell_Jwt::encode($t['payload'], $key, 'HS9999');
            };

            $this->assert($jwt)->equals($t['jwt']);

            $this->assert($bad)->exception(Tell_Jwt_Exception::class);

        }

    })->then('decode()', function() use ($tests) {

        foreach ($tests as $t) {

            $key = $t['alg'][0] === 'H' ? $this->hash_key : $this->public_key;

            if ($t['throw']) {

                $run = function() use ($t, $key) {
                    return Tell_Jwt::decode($t['jwt'], $key);
                };

                $this->assert($run)->exception($t['throw']);

            } else {

                $this->assert(Tell_Jwt::decode($t['jwt'], $key))->equals($t['payload']);

            }

        }

    });

})->vars([
    'date'        => '2022-02-12 03:00:00',
    'hash_key'    => 'XM4bXpjk6hiqZ8HxtswBgIIBK7L7qjHM',
    'private_key' => file_get_contents(__DIR__ . '/../keep/private.pem'),
    'public_key'  => file_get_contents(__DIR__ . '/../keep/public.pem'),
]);
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

$inspect('Tell_Image_Info', function() {

    $this->try('isGif()', function() {
        $this->assert((new Tell_Image_Info($this->path_gif))->isGif())->true();
        $this->assert((new Tell_Image_Info($this->path_jpg))->isGif())->false();
        $this->assert((new Tell_Image_Info($this->path_jpeg))->isGif())->false();
        $this->assert((new Tell_Image_Info($this->path_png))->isGif())->false();
        $this->assert($this->path_gif)->imageGif();
    });

    $this->try('isJpeg()', function() {
        $this->assert((new Tell_Image_Info($this->path_gif))->isJpeg())->false();
        $this->assert((new Tell_Image_Info($this->path_jpg))->isJpg())->true();
        $this->assert((new Tell_Image_Info($this->path_jpg))->isJpeg())->true();
        $this->assert((new Tell_Image_Info($this->path_jpeg))->isJpg())->true();
        $this->assert((new Tell_Image_Info($this->path_jpeg))->isJpeg())->true();
        $this->assert((new Tell_Image_Info($this->path_png))->isJpeg())->false();
        $this->assert($this->path_jpg)->imageJpg();
        $this->assert($this->path_jpg)->imageJpeg();
        $this->assert($this->path_jpeg)->imageJpg();
        $this->assert($this->path_jpeg)->imageJpeg();
    });

    $this->try('isPng()', function() {
        $this->assert((new Tell_Image_Info($this->path_gif))->isPng())->false();
        $this->assert((new Tell_Image_Info($this->path_jpg))->isPng())->false();
        $this->assert((new Tell_Image_Info($this->path_jpeg))->isPng())->false();
        $this->assert((new Tell_Image_Info($this->path_png))->isPng())->true();
        $this->assert($this->path_png)->imagePng();
    });

    $this->try('isImage()', function() {
        $this->assert((new Tell_Image_Info($this->path_gif))->isImage())->true();
        $this->assert((new Tell_Image_Info($this->path_jpg))->isImage())->true();
        $this->assert((new Tell_Image_Info($this->path_jpeg))->isImage())->true();
        $this->assert((new Tell_Image_Info($this->path_png))->isImage())->true();
        $this->assert($this->path_gif)->image();
        $this->assert($this->path_jpg)->image();
        $this->assert($this->path_jpeg)->image();
        $this->assert($this->path_png)->image();
    });

})->vars([
    'path_png'  => Tell_Asset::path('images/blossoms.png'),
    'path_gif'  => Tell_Asset::path('images/dolphins.gif'),
    'path_jpeg' => Tell_Asset::path('images/shark.jpeg'),
    'path_jpg'  => Tell_Asset::path('images/whale.jpg'),
    'path_txt'  => Tell_Asset::path('docs/ipsum.txt'),
    'path_pdf'  => Tell_Asset::path('docs/skookum.pdf'),
]);
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

$inspect('Tell_File', function() {

    $shark = $this->path_shark;

    $whale = $this->path_whale;

    $vscode = $this->path_vscode;

    $sqlite = $this->path_sqlite3;

    $base = $this->path_cache;

    $dA = 'test_dir_a' . DS;

    $dB = 'test_dir_b' . DS;

    $dC = 'test_dir_c' . DS;

    $fA = 'test_file_a.txt';

    $fB = 'test_file_b.md';

    $fC = 'test_file_c';

    $fD = basename($shark);

    $this->try('buffer()', function() {

        $this->assert(Tell_File::buffer($this->path_buffer, [
            'name'   => 'Bob',
            'active' => TRUE,
        ]))->equals('Hello, Bob! Your account is active.');

        $this->assert(Tell_File::buffer($this->path_buffer, [
            'name'   => 'Jane',
            'active' => FALSE,
        ]))->equals('Hello, Jane! Your account is not active.');

    });

    $this->try('newDirectory()', function() use ($base, $dA, $dB) {

        $this->assert(Tell_File::newDirectory($base . $dA))->true();

        $this->assert(Tell_File::newDirectory($base . $dB . $dA))->true();

        $this->assert($base . $dA)->notFile()->directory();

        $this->assert($base . $dB)->notFile()->directory();

        $this->assert($base . $dB . $dA)->notFile()->directory();

    })->then('newFile()', function() use ($base, $dA, $dB, $fA, $fB, $fC) {

        $this->assert(Tell_File::newFile($base . $dA . $fA, 'hello world'))->true();

        $this->assert(Tell_File::newFile($base . $dB . $dA . $fB, 'hello again'))->true();

        $this->assert(Tell_File::newFile($base . $dA . $fC, 'foobar'))->true();

        $this->assert($base . $dA . $fA)->notDirectory()->file();

        $this->assert($base . $dB . $dA . $fB)->notDirectory()->file();

        $this->assert($base . $dA . $fC)->notDirectory()->file();

    })->then('copy()', function() use ($shark, $base, $dA, $dB, $dC, $fA, $fB) {

        $this->assert(Tell_File::copy($base . $dA . $fA, /**/ $base . $dB . $dA . $fA))->true();

        $this->assert(Tell_File::copy($base . $dB . $dA . $fB, /**/ $base . $dA . $fB))->true();

        $this->assert(Tell_File::copy($base . $dB, /**/ $base . $dC))->true();

        $this->assert(Tell_File::copy($shark, /**/ $base . $dC))->true();

    })->then('extension()', function() use ($whale, $shark, $base, $dA, $dC, $fA, $fB, $fC, $fD) {

        $this->validate($whale)->extensions('jpg')->assert();

        $this->validate($shark)->extensions('jpg')->assert();

        $this->validate($base . $dA . $fA)->extensions('txt')->assert();

        $this->validate($base . $dA . $fB)->extensions('md')->assert();

        $this->validate($base . $dA . $fC)->extensions('')->assert();

        $this->validate($base . $dC . $fD)->extensions('jpg')->assert();

    })->then('mime()', function() use ($whale, $shark, $vscode, $sqlite, $base, $dA, $dC, $fA, $fB, $fC, $fD) {

        $mimes = [
            '*'       => 'application/octet-stream',
            'ico'     => 'image/x-icon',
            'jpg'     => 'image/jpeg',
            'jpeg'    => 'image/jpeg',
            'md'      => 'text/markdown',
            'mp4'     => 'video/mp4',
            'pdf'     => 'application/pdf',
            'sqlite3' => 'application/vnd.sqlite3',
            'txt'     => 'text/plain',
        ];

        $this->validate($whale)->mimes($mimes['jpeg'])->assert();

        $this->validate($whale)->mimes($mimes['jpeg'])->assert();

        $this->validate($shark)->mimes($mimes['jpg'])->assert();

        $this->validate($vscode)->mimes($mimes['ico'])->assert();

        $this->validate($sqlite)->mimes($mimes['sqlite3'])->assert();

        $this->validate($base . $dA . $fA)->mimes($mimes['txt'])->assert();

        $this->validate($base . $dA . $fB)->mimes($mimes['md'])->assert();

        $this->validate($base . $dA . $fC)->mimes($mimes['*'])->assert();

        $this->validate($base . $dC . $fD)->mimes($mimes['jpg'])->assert();

    })->then('sha256()', function() use ($shark, $base, $dC, $fD) {

        $hashA = Tell_File::sha256($shark);

        $hashB = Tell_File::sha256($base . $dC . $fD);

        $this->assert($hashA)->equals($hashB);

    })->then('sha512()', function() use ($shark, $base, $dC, $fD) {

        $hashA = Tell_File::sha512($shark);

        $hashB = Tell_File::sha512($base . $dC . $fD);

        $this->assert($hashA)->equals($hashB);

    })->then('delete()', function() use ($base, $dA, $dB, $dC, $fA, $fB, $fC) {

        $this->assert(Tell_File::delete($base . $fA))->true();

        $this->assert(Tell_File::delete($base . $fB))->true();

        $this->assert(Tell_File::delete($base . $fC))->true();

        $this->assert($base . $fA)->notFile()->notDirectory();

        $this->assert($base . $fA)->notFile()->notDirectory();

        $this->assert($base . $fC)->notFile()->notDirectory();

        $this->assert(Tell_File::delete($base . $dA))->true();

        $this->assert(Tell_File::delete($base . $dB))->true();

        $this->assert(Tell_File::delete($base . $dC . $dA))->true();

        $this->assert($base . $dA)->notDirectory()->notFile();

        $this->assert($base . $dB)->notDirectory()->notFile();

        $this->assert($base . $dC . $dA)->notDirectory()->notFile();

        $this->assert($base . $dC)->directory()->notFile();

        $this->assert(Tell_File::delete($base . $dC))->true();

        $this->assert($base . $dC)->notDirectory()->notFile();

    });

})->vars([
    'path_buffer'  => Tell_Asset::path('scripts/buffer.php'),
    'path_whale'   => Tell_Asset::path('images/whale.jpg'),
    'path_shark'   => Tell_Asset::path('images/shark.jpeg'),
    'path_skookum' => Tell_Asset::path('docs/skookum.pdf'),
    'path_vscode'  => Tell_Asset::path('images/vscode.ico'),
    'path_ipsum'   => Tell_Asset::path('docs/ipsum.txt'),
    'path_sqlite3' => Tell_Asset::path('sql/tell_php_test.sqlite3'),
    'path_video'   => Tell_Asset::path('videos/volcano.mp4'),
    'path_cache'   => CACHE_PATH . DS . 'inspect' . DS,
]);

$inspect('Tell_File_Search', function() {

    // logger($this->path_asset);

    // (new Tell_File_Search())
    //     ->directories()
    //     ->inside($this->path_asset)
    //     ->depth(0)
    //     ->results();

})->vars([
    'path_asset' => Tell_Asset::directory(),
]);
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

// -------------------------------------------------------------------------------------------------

class Inspect_Test_Auth_Digest extends Tell_Auth_Digest
{
    public function authenticate()
    {
        if ($this->validate()) {
            return [
                'first_name' => 'Bob',
                'last_name'  => 'Smith',
                'email'      => 'bob@example.com',
            ];
        }

        return FALSE;
    }

    public function findPassword(string $username)
        : ? string
    {
        if ($username === 'bob@example.com') {
            return 'testing';
        }

        return NULL;
    }

    public $test = [
        'onAuthenticate'        => [],
        'onAuthenticateSuccess' => [],
        'onAuthenticateFailure' => [],
        'onAuthenticateRevoke'  => [],
        'onPersist'             => [],
        'onPersistSuccess'      => [],
        'onPersistFailure'      => [],
        'onAuthorize'           => [],
        'onAuthorizeSuccess'    => [],
    ];

    public function onAuthenticate(...$args)
    {
        $this->test['onAuthenticate'] = $args;
    }

    public function onAuthenticateSuccess(...$args)
    {
        $this->test['onAuthenticateSuccess'] = $args;
    }

    public function onAuthenticateFailure(...$args)
    {
        $this->test['onAuthenticateFailure'] = $args;
    }

    public function onAuthenticateRevoke(...$args)
    {
        $this->test['onAuthenticateRevoke'] = $args;
    }

    public function onPersist(...$args)
    {
        $this->test['onPersist'] = $args;
    }

    public function onPersistSuccess(...$args)
    {
        $this->test['onPersistSuccess'] = $args;
    }

    public function onPersistFailure(...$args)
    {
        $this->test['onPersistFailure'] = $args;
    }

    public function onAuthorize(...$args)
    {
        $this->test['onAuthorize'] = $args;
    }

    public function onAuthorizeSuccess(...$args)
    {
        $this->test['onAuthorizeSuccess'] = $args;
    }
}

// -------------------------------------------------------------------------------------------------

$inspect('Tell_Auth_Digest >> Instantiate', function(Inspect_Test_Auth_Digest $auth) {

    $this->assert($auth->test['onAuthenticate'])->array()->empty();

    $this->assert($auth->test['onAuthenticateSuccess'])->array()->empty();

    $this->assert($auth->test['onAuthenticateFailure'])->array()->hasKey(0)->hasKey(1);

    $this->assert($auth->test['onAuthenticateFailure'][0])->false();

    $this->assert($auth->test['onAuthenticateFailure'][1])->instanceOf($auth);

    $this->assert($auth->test['onPersist'])->array()->empty();

    $this->assert($auth->test['onPersistSuccess'])->array()->empty();

    $this->assert($auth->test['onPersistFailure'])->array()->empty();

    $this->assert($auth->test['onAuthorize'])->array()->empty();

    $this->assert($auth->test['onAuthorizeSuccess'])->array()->empty();

});

// -------------------------------------------------------------------------------------------------

$inspect('Tell_Auth_Jwt >> Valid Digest', function(Inspect_Test_Auth_Digest $auth) {

    $this->assert($auth->test['onAuthenticate'])->array()->hasKey(0)->hasKey(1);

    $this->assert($auth->test['onAuthenticate'][0])->equals([
        'first_name' => 'Bob',
        'last_name'  => 'Smith',
        'email'      => 'bob@example.com',
    ]);

    $this->assert($auth->test['onAuthenticate'][1])->instanceOf($auth);

    $this->assert($auth->test['onAuthenticateSuccess'])->equals($auth->test['onAuthenticate']);

    $this->assert($auth->test['onAuthenticateFailure'])->array()->empty();

    $this->assert($auth->test['onPersist'])->array()->empty();

    $this->assert($auth->test['onPersistSuccess'])->array()->empty();

    $this->assert($auth->test['onPersistFailure'])->array()->empty();

    $this->assert($auth->test['onAuthorize'])->array()->empty();

    $this->assert($auth->test['onAuthorizeSuccess'])->array()->empty();

})->request([
    'method' => 'GET',
    'server' => [
        'PHP_AUTH_DIGEST' => 'username="bob@example.com", realm="Protected Area", nonce="gf8RcDCkvNvODtyDRw56KAlcO0dm3x0v", uri="/test/auth/middleware/digest", algorithm=MD5, response="ca175b73ced4115be91847e5ceb9aff1", opaque="2929b8e007e9c3edd69d915068815d71", qop=auth, nc=00000003, cnonce="0c4ce06db26f3f58"',
    ],
]);

// -------------------------------------------------------------------------------------------------
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

$inspect('Tell_Inspect_Suite_Client', function() {

    $this->try('An HTTP request with an expectation')->client(function() {

        $this->post('test/webhook/post', function(Tell_Request $input) {

            $client = new Tell_Client_Request();

            $client->header('Authorization', $input->post('code'));

            $client->query([
                'status'  => 'Active',
                'account' => 12,
            ]);

            $client->data([
                'first_name' => $input->post('first_name'),
                'last_name'  => $input->post('last_name'),
                'email'      => $input->post('email'),
                'country'    => 'US',
            ]);

            return $client;

        })->expecting(function($response) {

            $this->assert($response)->instanceOf(Tell_Client_Response::class);

            $this->assert($response->code())->equals(200);

            $this->assert($response->isJson())->true();

            $this->assert($response->header('authorization'))->equals('feVUrB4f0j5ScMd3');

            $this->assert($response->header('Authorization'))->equals('feVUrB4f0j5ScMd3');

            $this->assert($response->body())->iterable();

            $this->assert($response->body('query.account'))->equals('12');

            $this->assert($response->body('input.email'))->equals('bob-smith@foobar.com');

            $this->assert($response->body('input.country'))->equals('US');

            $this->assert($response->body('input.Country'))->empty();

            return TRUE;

        })->then('An HTTP request after the expectation')->client(function() {

            $response = $this->get('test/webhook/get/' . Tell_Request::post('code'))->result;

            $this->assert($response)->instanceOf(Tell_Client_Response::class);

            $this->assert($response->code())->equals(200);

            $this->assert($response->isJson())->true();

            $this->assert($response->body())->arr();

        });

    })->then('An isolated HTTP request', function() {

        $response = $this->client()->post('test/webhook/broke')->result;

        $this->assert($response)->instanceOf(Tell_Client_Response::class);

        $this->assert($response->code())->equals(200);

        $this->assert($response->header('authorization'))->notEquals('feVUrB4f0j5ScMd3');

        $this->assert($response->isJson());

        $this->assert($response->body())->array();

    });

})->request([
    'post' => [
        'code'       => 'feVUrB4f0j5ScMd3',
        'first_name' => 'Bob',
        'last_name'  => 'Smith',
        'email'      => 'bob-smith@foobar.com',
    ],
])->onlyWhen(function() {
    return ! Tell_Request::isCli();
});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

$inspect('Tell_Mutate', function() {

    $this->try('Closures', function() {

        // @todo

    });

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

$inspect('Tell_Mutate_Response >> Normalizer Methods', function() {

    $request = Mockery::mock(Tell_Request::class);

    $mutate = new Tell_Mutate_Response($request, [], []);

    $rawCorsValues = [ // value, expect
        [['abc', 123],      'abc, 123'],
        [['abc, 123'],      'abc, 123'],
        [['abc', 123, '*'], '*'],
        [0,                  NULL],
        ['0',                NULL],
        [FALSE,              NULL],
        [NULL,               NULL],
        [TRUE,               '1'],
    ];

    $pathRules = [ // path, rule, expect
        ['/',                    '*',             TRUE],
        ['/foo/123',             '*',             TRUE],
        ['/foo/123/bar',         '*',             TRUE],
        ['/foo/123/bar/doe',     '/foo/**',       TRUE],
        ['/foo/123/bar/doe',     '/foo/*',        FALSE],
        ['/foo/123/bar',         '/foo/*/doe',    FALSE],
        ['/foo/123/bar',         '/foo/*/bar',    TRUE],
        ['/foo/123/bar/doe',     '/foo/*/bar',    FALSE],
        ['/foo/123/bar/doe',     '/foo/*/doe',    FALSE],
        ['/foo/123/bar/doe',     '/foo/**/doe',   TRUE],
        ['/foo/123/bar/doe.png', '/foo/**/*.png', TRUE],
        ['/foo/123/bar/doe.png', '/foo/**/*.jpg', FALSE],
    ];

    $this->try('normalizeCorsValue()')->many($rawCorsValues, function($test) use ($mutate) {

        list($value, $expect) = $test;

        return $expect === $mutate->normalizeCorsValue($value, TRUE);

    })->then('normalizeCorsPolicy()', function() use ($mutate) {

        $this->assert($mutate->normalizeCorsPolicy([
            'access-control-allow-credentials' => TRUE,
        ]))->equals([
            'Access-Control-Allow-Credentials' => 'true',
            'Access-Control-Allow-Headers'     => NULL,
            'Access-Control-Allow-Methods'     => NULL,
            'Access-Control-Expose-Headers'    => NULL,
            'Access-Control-Max-Age'           => '0',
        ]);

        $this->assert($mutate->normalizeCorsPolicy([
            'Access-Control-Expose-Headers' => 'Content-Encoding',
        ]))->equals([
            'Access-Control-Allow-Credentials' => NULL,
            'Access-Control-Allow-Headers'     => NULL,
            'Access-Control-Allow-Methods'     => NULL,
            'Access-Control-Expose-Headers'    => 'Content-Encoding',
            'Access-Control-Max-Age'           => '0',
        ]);

        $this->assert($mutate->normalizeCorsPolicy([
            'Access-Control-Expose-Headers' => ['Content-Encoding', 'Kuma-Revision'],
            'access-control-max-age'        => 600,
        ]))->equals([
            'Access-Control-Allow-Credentials' => NULL,
            'Access-Control-Allow-Headers'     => NULL,
            'Access-Control-Allow-Methods'     => NULL,
            'Access-Control-Expose-Headers'    => 'Content-Encoding, Kuma-Revision',
            'Access-Control-Max-Age'           => '600',
        ]);

    })->then('pathRule()')->many($pathRules, function($test) use ($mutate) {

        list($path, $rule, $expect) = $test;

        return $expect === $mutate->pathRule($path, $rule);

    })->then('appendVaryHeader()', function() use ($mutate) {

        $response = new Tell_Response_Plain("I'm just a big nothin!");

        $response = $mutate->appendVaryHeader($response, 'Origin');

        $response = $mutate->appendVaryHeader($response, 'Access-Control-Request-Method');

        $response = $mutate->appendVaryHeader($response, 'Access-Control-Request-Headers');

        $vary = $response->header('Vary');

        $this->assert($vary)->equals('Origin, Access-Control-Request-Method, Access-Control-Request-Headers');

    })->then('pickHeaderPolicy()', function() use ($mutate) {

        $policies = [
            [
                'paths'   => ['/dist/*.js', '/assets/**/.jsx', '/assets/**/*.js'],
                'headers' => [
                    'Content-Type' => 'text/javascript',
                    'X-Powered-By' => 'tell-php',
                ],
            ],
            [
                'paths'   => ['/widgets/**'],
                'headers' => [
                    'X-Powered-By'    => FALSE,
                    'X-Frame-Options' => 'SAMEORIGIN',
                ],
            ],
            [
                'paths'   => ['/users/**'],
                'headers' => [
                    'X-Powered-By'    => FALSE,
                    'X-Frame-Options' => 'DENY',
                ],
            ],
        ];

        $this->assert($mutate->pickHeaderPolicy('/assets/js/main.js', $policies))->equals([
            'Content-Type' => 'text/javascript',
            'X-Powered-By' => 'tell-php',
        ]);

        $this->assert($mutate->pickHeaderPolicy('/dist/app.js', $policies))->equals([
            'Content-Type' => 'text/javascript',
            'X-Powered-By' => 'tell-php',
        ]);

        $this->assert($mutate->pickHeaderPolicy('/widgets/form/survey/food', $policies))->equals([
            'X-Powered-By'    => FALSE,
            'X-Frame-Options' => 'SAMEORIGIN',
        ]);

        $this->assert($mutate->pickHeaderPolicy('/users/123/notes', $policies))->equals([
            'X-Powered-By'    => FALSE,
            'X-Frame-Options' => 'DENY',
        ]);

        $this->assert($mutate->pickHeaderPolicy('/admin', $policies))->equals($mutate->defaultHeaderPolicy());

    })->then('pickCorsPolicy()', function() use ($mutate) {

        $origin = 'https://dashboard.example.com:8087';

        $path = '/auth/refresh/jwt';

        $empty = [
            [
                'paths'   => [],
                'origins' => [],
                'headers' => [],
            ],
        ];

        $rightOriginWrongPathDepth = [
            [
                'paths'   => ['/auth/*'],
                'origins' => ['*.example.com:*'],
                'headers' => [],
            ],
        ];

        $rightOriginRightPathDepth = [
            [
                'paths'   => ['/auth/**'],      // matches
                'origins' => ['*.example.com'], // no-match
                'headers' => [],
            ],
            [
                'paths'   => ['/foo/**'],         // no-match
                'origins' => ['*.example.com:*'], // matches
                'headers' => [],
            ],
            [
                'paths'   => ['/auth/**'],        // matches
                'origins' => ['*.example.com:*'], // matches
                'headers' => [
                    'Access-Control-Allow-Credentials' => TRUE,
                    'Access-Control-Allow-Methods'     => ['GET', 'POST', 'PUT', 'DELETE'],
                ],
            ],
        ];

        $this->assert($mutate->pickCorsPolicy($origin, $path, $empty))->null();

        $this->assert($mutate->pickCorsPolicy($origin, $path, $rightOriginWrongPathDepth))->null();

        $policy = $mutate->pickCorsPolicy($origin, $path, $rightOriginRightPathDepth);

        $this->assert($policy)->array()->equals([
            'Access-Control-Allow-Credentials' => TRUE,
            'Access-Control-Allow-Headers'     => NULL,
            'Access-Control-Allow-Methods'     => ['GET', 'POST', 'PUT', 'DELETE'],
            'Access-Control-Expose-Headers'    => NULL,
            'Access-Control-Max-Age'           => '0',
        ]);

    });

});

$inspect('Tell_Mutate_Response >> Standard Request', function() {

    $this->try('headerMutation()', function(Tell_Request $request) {

        $mutate = new Tell_Mutate_Response($request, [], []);

        $response = new Tell_Response_Plain("I'm just a big nothin!");

        $headerPolicy = [
            'content-type'              => 'text/plain',
            'upgrade-insecure-requests' => TRUE,
            'x-powered-by'              => 'tell-php',
        ];

        $response = $mutate->headerMutation($response, $headerPolicy);

        $this->assert($response)->instanceOf(Tell_Response_Plain::class);

        $this->assert($response->header('Content-Type'))->equals('text/plain');

        $this->assert($response->header('Upgrade-Insecure-Requests'))->equals('1');

        $this->assert($response->header('X-Powered-By'))->equals('tell-php');

    });

});

$inspect('Tell_Mutate_Response >> CORS Preflight Request', function() {

    $this->try('corsMutation()', function(Tell_Request $request) {

        $mutate = new Tell_Mutate_Response($request, [], []);

        $response = new Tell_Response_Plain("I'm just a big nothin!");

        $corsPolicy = $mutate->normalizeCorsPolicy([
            'Access-Control-Allow-Credentials' => 'true',
            'Access-Control-Allow-Headers'     => ['Content-Type', 'X-Requested-With'],
            'Access-Control-Allow-Methods'     => NULL,
            'Access-Control-Expose-Headers'    => 'Content-Encoding, Kuma-Revision',
            'Access-Control-Max-Age'           => '600',
        ]);

        $response = $mutate->corsMutation($response, $corsPolicy);

        $this->assert($response)->instanceOf(Tell_Response_Empty::class);

        $this->assert($response->header('Access-Control-Allow-Headers'))->equals('Content-Type, X-Requested-With');

        $this->assert($response->header('Access-Control-Max-Age'))->equals('600');

        $this->assert($response->header('Access-Control-Allow-Origin'))->equals('https://dashboard.example.com:8087');

        $this->assert($response->header('Access-Control-Allow-Credentials'))->equals('true');

        $headerPolicy = [ // should not apply to CORS preflight requests
            'content-type'              => 'text/plain',
            'upgrade-insecure-requests' => TRUE,
            'x-powered-by'              => 'tell-php',
        ];

        $response = $mutate->headerMutation($response, $headerPolicy);

        $this->assert($response)->instanceOf(Tell_Response_Empty::class);

        $this->assert($response->header('Content-Type'))->equals('text/plain');

    })->then('__invoke()', function(Tell_Request $request) {

        $corsPolicies = [
            [
                'paths'   => ['/auth/**'],
                'origins' => ['dashboard.example.com:*'],
                'headers' => [
                    'Access-Control-Allow-Credentials' => TRUE,
                    'Access-Control-Allow-Headers'     => '*',
                    'Access-Control-Allow-Methods'     => '*',
                    'Access-Control-Expose-Headers'    => '*',
                    'Access-Control-Max-Age'           => 600,
                ],
            ],
        ];

        $headerPolicies = [ // none should apply to CORS preflight requests
            [
                'paths'   => ['/auth/**'],
                'headers' => [
                    'X-Powered-By' => 'tell-php-auth-areas',
                ],
            ],
            [
                'paths'   => '*',
                'headers' => [
                    'X-Powered-By' => 'tell-php-everywhere-else',
                ],
            ],
        ];

        $mutate = new Tell_Mutate_Response($request, $corsPolicies, $headerPolicies);

        $response = $mutate->__invoke(new Tell_Response_Plain("I'm just a big nothin!"));

        $this->assert($response)->instanceOf(Tell_Response_Empty::class);

        $this->assert($response->header('Access-Control-Allow-Origin'))->equals('https://dashboard.example.com:8087');

        $this->assert($response->header('Access-Control-Allow-Methods'))->equals('POST');

        $this->assert($response->header('Access-Control-Allow-Headers'))->equals('Authorization, X-Requested-With, Content-Type');

        $this->assert($response->header('Access-Control-Allow-Credentials'))->equals('true');

        $this->assert($response->header('Access-Control-Max-Age'))->equals('600');

        $this->assert($response->header('Vary'))->equals('Access-Control-Allow-Headers, Access-Control-Allow-Methods, Origin');

    });

})->request([
    'method'  => 'OPTIONS',
    'uri'     => '/auth/refresh/jwt',
    'headers' => [
        'Access-Control-Request-Headers' => 'Authorization, X-Requested-With, Content-Type',
        'Access-Control-Request-Method'  => 'POST',
        'Origin'                         => 'https://dashboard.example.com:8087',
    ],
]);

$inspect('Tell_Mutate_Response >> CORS Request', function() {

    $this->try('__invoke()', function(Tell_Request $request) {

        $corsPolicies = [
            [
                'paths'   => ['/auth/**'],      // matches
                'origins' => ['*.example.com'], // no-match
                'headers' => [],
            ],
            [
                'paths'   => ['/foo/**'],         // no-match
                'origins' => ['*.example.com:*'], // matches
                'headers' => [],
            ],
            [
                'paths'   => ['/auth/**'],                               // matches
                'origins' => ['example.com', 'dashboard.example.com:*'], // matches
                'headers' => [
                    'Access-Control-Allow-Credentials' => TRUE,
                    'Access-Control-Allow-Headers'     => '*',
                    'Access-Control-Allow-Methods'     => '*',
                    'Access-Control-Expose-Headers'    => 'Content-Encoding, Kuma-Revision',
                    'Access-Control-Max-Age'           => 600,
                ],
            ],
        ];

        $headerPolicies = [
            [
                'paths'   => ['/foo/**'], // no-match
                'headers' => [],
            ],
            [
                'paths'   => ['/auth/**'], // matches
                'headers' => [
                    'X-Powered-By' => 'tell-php-auth-areas',
                ],
            ],
            [
                'paths'   => '*', // also matches, but first match in policy list is always chosen
                'headers' => [
                    'X-Powered-By' => 'tell-php-everywhere-else',
                ],
            ],
        ];

        $mutate = new Tell_Mutate_Response($request, $corsPolicies, $headerPolicies);

        $response = $mutate->__invoke(new Tell_Response_Plain("I'm just a big nothin!"));

        $this->assert($response)->instanceOf(Tell_Response_Plain::class);

        $this->assert($response->header('Access-Control-Allow-Origin'))->equals('https://dashboard.example.com:8087');

        $this->assert($response->header('Access-Control-Expose-Headers'))->equals('Content-Encoding, Kuma-Revision');

        $this->assert($response->header('Access-Control-Allow-Credentials'))->equals('true');

        $this->assert($response->header('Vary'))->equals('Origin');

        $this->assert($response->header('X-Powered-By'))->equals('tell-php-auth-areas');

    });

})->request([
    'method'  => 'POST',
    'uri'     => '/auth/refresh/jwt',
    'headers' => [
        'Access-Control-Request-Headers' => 'Authorization, X-Requested-With, Content-Type',
        'Access-Control-Request-Method'  => 'POST',
        'Origin'                         => 'https://dashboard.example.com:8087',
    ],
]);
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

$inspect('Tell_Assert', function() {

    $this->try('instanceOf(), notInstanceOf()', function() {

        $db     = new TestDb($this->config);
        $user   = new TestUser($db);
        $order  = new TestOrder($db);
        $online = new TestOnlineOrder($db, $user);

        $this->assert($db)
            ->instanceOf(TestDb::class)
            ->notInstanceOf(TestUser::class)
            ->notInstanceOf($user);

        $this->assert($user)
            ->instanceOf(TestUser::class)
            ->notInstanceOf(TestDb::class)
            ->notInstanceOf($db);

        $this->assert($order)
            ->instanceOf(TestOrder::class)
            ->notInstanceOf(TestOnlineOrder::class)
            ->notInstanceOf($online);

        $this->assert($online)
            ->instanceOf(TestOrder::class)
            ->instanceOf(TestOnlineOrder::class)
            ->notInstanceOf(TestUser::class)
            ->notInstanceOf($user);

    });

})->vars([
    'config' => [
        'driver'   => 'mysql',
        'database' => 'foobar',
        'host'     => 'localhost',
        'port'     => NULL,
        'socket'   => NULL,
        'username' => 'foobar_admin',
        'password' => 'i-am-a-nice-shark-not-a-mindless-eating-machine...',
    ],
]);

class TestDb
{
    protected $config = [];

    public function __construct(array $config)
    {
        $this->config = $config;
    }
}

class TestUser
{
    protected $db = NULL;

    public function __construct(TestDb $db)
    {
        $this->db = $db;
    }
}

class TestOrder
{
    protected $db = NULL;

    public function __construct(TestDb $db)
    {
        $this->db = $db;
    }
}

class TestOnlineOrder extends TestOrder
{
    protected $user = NULL;

    public function __construct(TestDb $db, TestUser $user)
    {
        parent::__construct($db);

        $this->user = $user;
    }
}
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

$inspect('Tell_Uuid', function() {

    $this->try('generate(), binToUuid(), uuidToBin()')->many(5000, function() {

        $uuid = Tell_Uuid::generate();

        if ( ! (bool) preg_match('/^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i', $uuid)) {
            return FALSE;
        }

        $bin = Tell_Uuid::uuidToBin($uuid);

        $undo = Tell_Uuid::binToUuid($bin);

        return strlen($bin) === 16 && $uuid === $undo;

    });

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

// ---------------------------------------------------------------------------------------------

$inspect('Tell_Bridge_Contact', function() {

    // @todo

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

Tell_Jwt::time(1677126824);

// -------------------------------------------------------------------------------------------------

class Inspect_Test_Auth_Jwt extends Tell_Auth_Jwt
{
    const RECORD_TABLE   = '_test_users';
    const FIELD_ID       = 'id';
    const FIELD_PASSWORD = 'password';
    const SECRET_JWT     = 'U2Fv3HJKpfMuVTkyXM2LxclDudAyjyvD';
    const SECRET_PERSIST = 'nP7NnrM6Fk4Y3PMLVdIuznoDNQZIVMbT';

    public function jwtPayload(array $data)
        : array
    {
        return [
            'sub' => $data['id'],
        ];
    }

    public function findById($id)
        : ? array
    {
        if (123 !== (int) $id) {
            return NULL;
        }

        return [
            'id'         => 123,
            'first_name' => 'Bob',
            'last_name'  => 'Smith',
            'email'      => 'bob@example.com',
            'password'   => Tell_Password::hash('testing'),
        ];
    }

    public function findByUser($user)
        : ? array
    {
        if ('bob@example.com' !== $user) {
            return NULL;
        }

        return [
            'id'         => 123,
            'first_name' => 'Bob',
            'last_name'  => 'Smith',
            'email'      => 'bob@example.com',
            'password'   => Tell_Password::hash('testing'),
        ];
    }

    protected function persistHashKey()
        : string
    {
        return self::SECRET_PERSIST;
    }

    protected function jwtDecode(string $jwt)
        : array
    {
        return Tell_Jwt::decode($jwt, self::SECRET_JWT);
    }

    protected function jwtEncode(array $payload)
        : string
    {
        return Tell_Jwt::encode($payload, self::SECRET_JWT);
    }

    public $test = [
        'onAuthenticate'        => [],
        'onAuthenticateSuccess' => [],
        'onAuthenticateFailure' => [],
        'onAuthenticateRevoke'  => [],
        'onPersist'             => [],
        'onPersistSuccess'      => [],
        'onPersistFailure'      => [],
        'onAuthorize'           => [],
        'onAuthorizeSuccess'    => [],
    ];

    public function onAuthenticate(...$args)
    {
        $this->test['onAuthenticate'] = $args;
    }

    public function onAuthenticateSuccess(...$args)
    {
        $this->test['onAuthenticateSuccess'] = $args;
    }

    public function onAuthenticateFailure(...$args)
    {
        $this->test['onAuthenticateFailure'] = $args;
    }

    public function onAuthenticateRevoke(...$args)
    {
        $this->test['onAuthenticateRevoke'] = $args;
    }

    public function onPersist(...$args)
    {
        $this->test['onPersist'] = $args;
    }

    public function onPersistSuccess(...$args)
    {
        $this->test['onPersistSuccess'] = $args;
    }

    public function onPersistFailure(...$args)
    {
        $this->test['onPersistFailure'] = $args;
    }

    public function onAuthorize(...$args)
    {
        $this->test['onAuthorize'] = $args;
    }

    public function onAuthorizeSuccess(...$args)
    {
        $this->test['onAuthorizeSuccess'] = $args;
    }
}

// -------------------------------------------------------------------------------------------------

$inspect('Tell_Auth_Jwt >> Instantiate', function(Inspect_Test_Auth_Jwt $auth) {

    $this->assert($auth->test['onAuthenticate'])->array()->empty();

    $this->assert($auth->test['onAuthenticateSuccess'])->array()->empty();

    $this->assert($auth->test['onAuthenticateFailure'])->array()
        ->hasKey(0)
        ->hasKey(1)
        ->hasKey(2)
        ->hasKey(3);

    $this->assert($auth->test['onAuthenticateFailure'][0])->false();

    $this->assert($auth->test['onAuthenticateFailure'][1])->equals('jwt_invalid');

    $this->assert($auth->test['onAuthenticateFailure'][2])->null();

    $this->assert($auth->test['onAuthenticateFailure'][3])->instanceOf($auth);

    $this->assert($auth->test['onAuthenticateRevoke'])->array()->empty();

    $this->assert($auth->test['onPersist'])->array()->empty();

    $this->assert($auth->test['onPersistSuccess'])->array()->empty();

    $this->assert($auth->test['onPersistFailure'])->array()->empty();

    $this->assert($auth->test['onAuthorize'])->array()->empty();

    $this->assert($auth->test['onAuthorizeSuccess'])->array()->empty();

})->swap(function() {

    $db = Mockery::mock(Tell_Db::class);

    $persist = Mockery::mock(Tell_Auth_Persist::class);
    $persist->shouldReceive('hashKey')->andReturnSelf();
    $persist->shouldReceive('graft')->andReturn(NULL);

    return [
        'Tell_Db'           => $db,
        'Tell_Auth_Persist' => $persist,
    ];

});

// -------------------------------------------------------------------------------------------------

$inspect('Tell_Auth_Jwt >> Valid Access Token', function(Inspect_Test_Auth_Jwt $auth) {

    $payload = [
        'sub' => 123,
        'iss' => 'dbaf009e4e3a94c648b37a2a789c0e6e',
        'exp' => 1677127124,
    ];

    $storage = [
        'sub' => 123,
        'iss' => 'dbaf009e4e3a94c648b37a2a789c0e6e',
        'exp' => 1677127124,
    ];

    $this->assert($auth->test['onAuthenticate'])->array()
        ->hasKey(0)
        ->hasKey(1);

    $this->assert($auth->test['onAuthenticateSuccess'])->array()
        ->hasKey(0)
        ->hasKey(1);

    $this->assert($auth->test['onAuthenticate'][0])->equals($payload);

    $this->assert($auth->test['onAuthenticateSuccess'][0])->equals($payload);

    $this->assert($auth->isAuthenticated())->true();

    $this->assert($auth->get())->equals($storage);

})->request([
    'server' => [
        'PHP_AUTH_TOKEN' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEyMywiaXNzIjoiZGJhZjAwOWU0ZTNhOTRjNjQ4YjM3YTJhNzg5YzBlNmUiLCJleHAiOjE2NzcxMjcxMjR9.0Za23rAIRVTDTpgmM-Op8MbYGwS1BYyKfF1xtGuHwm4',
    ],
])->swap(function() {

    $db = Mockery::mock(Tell_Db::class);

    $persist = Mockery::mock(Tell_Auth_Persist::class);
    $persist->shouldReceive('hashKey')->andReturnSelf();
    $persist->shouldReceive('graft')->andReturn(NULL);

    return [
        'Tell_Db'           => $db,
        'Tell_Auth_Persist' => $persist,
    ];

});

// -------------------------------------------------------------------------------------------------

$inspect('Tell_Auth_Jwt >> Valid Simulated Refresh Cycle', function(Inspect_Test_Auth_Jwt $auth) {

    $this->assert($auth->test['onAuthenticate'])->array()->empty();

    $this->assert($auth->test['onAuthenticateSuccess'])->array()->empty();

    $this->assert($auth->test['onAuthenticateFailure'])->array()->empty();

    $this->assert($auth->test['onAuthenticateRevoke'])->array()->empty();

    $this->assert($auth->test['onPersist'])->array()->empty();

    $this->assert($auth->test['onPersistSuccess'])->array()->empty();

    $this->assert($auth->test['onPersistFailure'])->array()->empty();

    $this->assert($auth->test['onAuthorize'])->array()->empty();

    $this->assert($auth->test['onAuthorizeSuccess'])->array()->empty();

    $refresh = $auth->refreshCycle();

    $this->assert($refresh)->instanceOf(Tell_Response_Json::class);

    $this->assert($refresh->json)->string();

    $json = Tell_Json::decode($refresh->json, TRUE);

    $this->assert($json)->array()
        ->hasKey('status')
        ->hasKey('access_token')
        ->hasKey('access_expires')
        ->hasKey('refresh_token')
        ->hasKey('refresh_expires');

})->request([
    'server' => [
        'PHP_AUTH_TOKEN' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NzgzMzY0MjQsImp0aSI6IjRaMW5lQ1JzamtoZ0lNdEZLWUh4NTE5VWVGa1dzandmIiwic3ViIjoiOGxvOHpQV0NuQmVkMmsxNWZhNGhkdUhIVGNGWEF0b3UiLCJpc3MiOiJkYmFmMDA5ZTRlM2E5NGM2NDhiMzdhMmE3ODljMGU2ZSJ9.3n9NQFQFPEdniOVsEfLcb9ylWs9taQk9KQqk9Ryy-SE',
    ],
])->swap(function() {

    $record = [
        'id'              => 1,
        'parent_table'    => '_test_users',
        'parent_record'   => 123,
        'access_selector' => '8lo8zPWCnBed2k15fa4hduHHTcFXAtou',
        'access_token'    => 'ef783f86600c2dcab429b3e11778ddd924a0929449279b3a827eb871a1ecd72e',
        'class_name'      => 'Inspect_Test_Auth_Jwt',
        'last_ip'         => '172.16.238.1',
        'created'         => '2023-02-23 04:33:44',
        'expires'         => '2023-03-09 04:33:44',
    ];

    $refresh = [
        Tell_Security::random(32),
        Tell_Security::random(32),
        1678300000,
    ];

    $db = Mockery::mock(Tell_Db::class);

    $persist = Mockery::mock(Tell_Auth_Persist::class);
    $persist->shouldReceive('hashKey')->andReturnSelf();
    $persist->shouldReceive('graft')->andReturn(NULL);
    $persist->shouldReceive('load')->andReturn($record);
    $persist->shouldReceive('delete')->andReturn(TRUE);
    $persist->shouldReceive('create')->andReturn($refresh);

    return [
        'Tell_Db'           => $db,
        'Tell_Auth_Persist' => $persist,
    ];

});

// -------------------------------------------------------------------------------------------------

$inspect('Tell_Auth_Jwt >> Valid Simulated Refresh Delete', function(Inspect_Test_Auth_Jwt $auth) {

    $this->assert($auth->test['onAuthenticate'])->array()->empty();

    $this->assert($auth->test['onAuthenticateSuccess'])->array()->empty();

    $this->assert($auth->test['onAuthenticateFailure'])->array()->empty();

    $this->assert($auth->test['onAuthenticateRevoke'])->array()->empty();

    $this->assert($auth->test['onPersist'])->array()->empty();

    $this->assert($auth->test['onPersistSuccess'])->array()->empty();

    $this->assert($auth->test['onPersistFailure'])->array()->empty();

    $this->assert($auth->test['onAuthorize'])->array()->empty();

    $this->assert($auth->test['onAuthorizeSuccess'])->array()->empty();

    $delete = $auth->refreshDelete();

    $this->assert($delete)->instanceOf(Tell_Response_Empty::class);

    Tell_Jwt::time(NULL);

})->request([
    'server' => [
        'PHP_AUTH_TOKEN' => 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NzgzMzY0MjQsImp0aSI6IjRaMW5lQ1JzamtoZ0lNdEZLWUh4NTE5VWVGa1dzandmIiwic3ViIjoiOGxvOHpQV0NuQmVkMmsxNWZhNGhkdUhIVGNGWEF0b3UiLCJpc3MiOiJkYmFmMDA5ZTRlM2E5NGM2NDhiMzdhMmE3ODljMGU2ZSJ9.3n9NQFQFPEdniOVsEfLcb9ylWs9taQk9KQqk9Ryy-SE',
    ],
])->swap(function() {

    $db = Mockery::mock(Tell_Db::class);

    $persist = Mockery::mock(Tell_Auth_Persist::class);
    $persist->shouldReceive('hashKey')->andReturnSelf();
    $persist->shouldReceive('graft')->andReturn(NULL);
    $persist->shouldReceive('delete')->andReturn(TRUE);

    return [
        'Tell_Db'           => $db,
        'Tell_Auth_Persist' => $persist,
    ];

});

// -------------------------------------------------------------------------------------------------
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

class Inspect_Cron_Schedule extends Tell_Cron_Schedule
{
    public function __call($name, $arguments)
    {
        return $this->$name(...$arguments);
    }
}

$inspect('Tell_Cron_Schedule', function() {

    $tab = function(string $schedule, $now)  {
        return new Inspect_Cron_Schedule($schedule, $now);
    };

    $bad = function(string $schedule, $now, $method = NULL) {
        return function() use ($schedule, $now, $method) {
            return $method
                ? (new Inspect_Cron_Schedule($schedule, $now))->$method()
                : new Inspect_Cron_Schedule($schedule, $now);
        };
    };

    $this->assert($bad('5 4 * *', time()))->exception(Tell_Cron_Exception::class);

    $this->try('matchMinute()', function() use ($tab, $bad) {

        // Every 59 minutes
        $this->assert($tab('*/59 * * * *', '2023-02-27 02:58:10')->matchMinute())->false();
        $this->assert($tab('*/59 * * * *', '2023-02-27 02:59:10')->matchMinute())->true();
        $this->assert($tab('*/59 * * * *', '2023-02-27 03:00:05')->matchMinute())->true();
        $this->assert($tab('*/59 * * * *', '2023-02-27 03:01:00')->matchMinute())->false();

        // At 5 minutes past the hour
        $this->assert($tab('5 * * * *', '2023-02-27 00:04:59')->matchMinute())->false();
        $this->assert($tab('5 * * * *', '2023-02-27 00:05:10')->matchMinute())->true();
        $this->assert($tab('5 * * * *', '2023-02-27 00:06:00')->matchMinute())->false();
        $this->assert($tab('5 * * * *', '2023-02-27 12:05:59')->matchMinute())->true();

        // At 5, 25, and 45 minutes past the hour
        $this->assert($tab('5,25,45 * * * *', '2023-02-27 12:05:59')->matchMinute())->true();
        $this->assert($tab('5,25,45 * * * *', '2023-02-27 12:06:01')->matchMinute())->false();
        $this->assert($tab('5,25,45 * * * *', '2023-02-27 12:24:30')->matchMinute())->false();
        $this->assert($tab('5,25,45 * * * *', '2023-02-27 12:25:00')->matchMinute())->true();
        $this->assert($tab('5,25,45 * * * *', '2023-02-27 12:45:10')->matchMinute())->true();
        $this->assert($tab('5,25,45 * * * *', '2023-02-27 12:46:10')->matchMinute())->false();

        // Every minute
        $this->assert($tab('* * * * *', '2023-02-16 12:10:00')->matchMinute())->true();

        // 0-59 max range
        $this->assert($bad('*/60 * * * *', '2023-02-20 02:59:59', 'matchMinute'))->exception(Tell_Cron_Exception::class);
        $this->assert($bad('0-60 * * * *', '2023-02-20 02:59:59', 'matchMinute'))->exception(Tell_Cron_Exception::class);
        $this->assert($bad('60 * * * *', '2023-02-20 02:59:59', 'matchMinute'))->exception(Tell_Cron_Exception::class);
        $this->assert($bad('abc * * * *', '2023-02-20 02:59:59', 'matchMinute'))->exception(Tell_Cron_Exception::class);

    })->then('matchHour()', function() use ($tab, $bad) {

        // Every minute, every 2 hours, between 12:00 AM and 05:59 AM
        $this->assert($tab('* 0-5/2 * * *', '2023-02-27 00:00:00')->matchHour())->true();
        $this->assert($tab('* 0-5/2 * * *', '2023-02-27 00:30:07')->matchHour())->true();
        $this->assert($tab('* 0-5/2 * * *', '2023-02-27 01:30:07')->matchHour())->false();
        $this->assert($tab('* 0-5/2 * * *', '2023-02-27 02:30:30')->matchHour())->true();
        $this->assert($tab('* 0-5/2 * * *', '2023-02-27 04:59:00')->matchHour())->true();
        $this->assert($tab('* 0-5/2 * * *', '2023-02-27 05:59:00')->matchHour())->false();
        $this->assert($tab('* 0-5/2 * * *', '2023-02-27 06:00:00')->matchHour())->false();
        $this->assert($tab('* 0-5/2 * * *', '2023-02-27 23:59:59')->matchHour())->false();

        // Every minute, between 05:00 AM and 05:59 AM
        $this->assert($tab('* 5 * * *', '2023-02-20 04:59:02')->matchHour())->false();
        $this->assert($tab('* 5 * * *', '2023-02-20 05:00:02')->matchHour())->true();
        $this->assert($tab('* 5 * * *', '2023-02-20 05:59:02')->matchHour())->true();
        $this->assert($tab('* 5 * * *', '2023-02-20 06:00:02')->matchHour())->false();

        // Every minute, between 12:00 PM and 02:59 PM
        $this->assert($tab('* 12-14 * * *', '2023-02-20 11:59:59')->matchHour())->false();
        $this->assert($tab('* 12-14 * * *', '2023-02-20 12:00:00')->matchHour())->true();
        $this->assert($tab('* 12-14 * * *', '2023-02-20 13:30:00')->matchHour())->true();
        $this->assert($tab('* 12-14 * * *', '2023-02-20 14:59:59')->matchHour())->true();
        $this->assert($tab('* 12-14 * * *', '2023-02-20 15:00:00')->matchHour())->false();

        // Every minute
        $this->assert($tab('* * * * *', '2023-02-16 12:10:00')->matchHour())->true();

        // 0-23 max range
        $this->assert($bad('* 12-24 * * *', '2023-02-20 02:59:59', 'matchHour'))->exception(Tell_Cron_Exception::class);
        $this->assert($bad('* 24 * * *', '2023-02-20 02:59:59', 'matchHour'))->exception(Tell_Cron_Exception::class);

    })->then('matchDayMonth()', function() use ($tab, $bad) {

        // Every minute, on day 2 of the month
        $this->assert($tab('* * 2 * *', '2023-02-01 23:59:00')->matchDayMonth())->false();
        $this->assert($tab('* * 2 * *', '2023-02-02 00:00:00')->matchDayMonth())->true();
        $this->assert($tab('* * 2 * *', '2023-02-02 00:01:10')->matchDayMonth())->true();
        $this->assert($tab('* * 2 * *', '2023-02-02 00:02:03')->matchDayMonth())->true();
        $this->assert($tab('* * 2 * *', '2023-02-02 23:59:00')->matchDayMonth())->true();
        $this->assert($tab('* * 2 * *', '2023-02-03 00:00:00')->matchDayMonth())->false();

        // Every minute, every 3 days, between day 2 and 15 of the month
        $this->assert($tab('* * 2-15/3 * *', '2023-03-01 23:59:00')->matchDayMonth())->false();
        $this->assert($tab('* * 2-15/3 * *', '2023-03-02 00:00:00')->matchDayMonth())->true();
        $this->assert($tab('* * 2-15/3 * *', '2023-03-03 12:10:00')->matchDayMonth())->false();
        $this->assert($tab('* * 2-15/3 * *', '2023-03-04 12:10:00')->matchDayMonth())->false();
        $this->assert($tab('* * 2-15/3 * *', '2023-03-05 12:10:00')->matchDayMonth())->true();
        $this->assert($tab('* * 2-15/3 * *', '2023-03-14 12:10:00')->matchDayMonth())->true();
        $this->assert($tab('* * 2-15/3 * *', '2023-03-15 12:10:00')->matchDayMonth())->false();

        // Every minute, on day 5, 15, and 31 of the month
        $this->assert($tab('* * 5,15,31 * *', '2023-02-04 12:10:00')->matchDayMonth())->false();
        $this->assert($tab('* * 5,15,31 * *', '2023-02-05 12:10:00')->matchDayMonth())->true();
        $this->assert($tab('* * 5,15,31 * *', '2023-02-06 12:10:00')->matchDayMonth())->false();
        $this->assert($tab('* * 5,15,31 * *', '2023-02-14 12:10:00')->matchDayMonth())->false();
        $this->assert($tab('* * 5,15,31 * *', '2023-02-15 12:10:00')->matchDayMonth())->true();
        $this->assert($tab('* * 5,15,31 * *', '2023-02-16 12:10:00')->matchDayMonth())->false();
        $this->assert($tab('* * 5,15,31 * *', '2023-02-31 12:10:00')->matchDayMonth())->false();

        // Every minute
        $this->assert($tab('* * * * *', '2023-02-16 12:10:00')->matchDayMonth())->true();

        // 1-31 max range
        $this->assert($bad('* * 0-31 * *', '2023-02-20 02:59:59', 'matchDayMonth'))->exception(Tell_Cron_Exception::class);
        $this->assert($bad('* * 1-32 * *', '2023-02-20 02:59:59', 'matchDayMonth'))->exception(Tell_Cron_Exception::class);
        $this->assert($bad('* * 0 * *', '2023-02-20 02:59:59', 'matchDayMonth'))->exception(Tell_Cron_Exception::class);
        $this->assert($bad('* * 32 * *', '2023-02-20 02:59:59', 'matchDayMonth'))->exception(Tell_Cron_Exception::class);

    })->then('matchMonth()', function() use ($tab, $bad) {

        // Every minute, January through March
        $this->assert($tab('* * * jan-mar *', '2022-12-31 23:59:59')->matchMonth())->false();
        $this->assert($tab('* * * jan-mar *', '2023-01-01 00:00:00')->matchMonth())->true();
        $this->assert($tab('* * * jan-mar *', '2023-01-15 10:05:13')->matchMonth())->true();
        $this->assert($tab('* * * jan-mar *', '2023-01-31 10:05:13')->matchMonth())->true();
        $this->assert($tab('* * * jan-mar *', '2023-02-10 10:05:13')->matchMonth())->true();
        $this->assert($tab('* * * jan-mar *', '2023-02-28 10:05:13')->matchMonth())->true();
        $this->assert($tab('* * * jan-mar *', '2023-03-31 23:59:59')->matchMonth())->true();
        $this->assert($tab('* * * jan-mar *', '2023-04-01 00:00:00')->matchMonth())->false();
        $this->assert($tab('* * * 1-3 *', '2022-12-31 23:59:59')->matchMonth())->false();
        $this->assert($tab('* * * 1-3 *', '2023-01-01 00:00:00')->matchMonth())->true();
        $this->assert($tab('* * * 1-3 *', '2023-01-15 10:05:13')->matchMonth())->true();
        $this->assert($tab('* * * 1-3 *', '2023-01-31 10:05:13')->matchMonth())->true();
        $this->assert($tab('* * * 1-3 *', '2023-02-10 10:05:13')->matchMonth())->true();
        $this->assert($tab('* * * 1-3 *', '2023-02-28 10:05:13')->matchMonth())->true();
        $this->assert($tab('* * * 1-3 *', '2023-03-31 23:59:59')->matchMonth())->true();
        $this->assert($tab('* * * 1-3 *', '2023-04-01 00:00:00')->matchMonth())->false();

        // Every minute, only in January, March, and August
        $this->assert($tab('* * * jan,mar,aug *', '2023-01-02 13:35:02')->matchMonth())->true();
        $this->assert($tab('* * * jan,mar,aug *', '2023-02-02 13:35:02')->matchMonth())->false();
        $this->assert($tab('* * * jan,mar,aug *', '2023-03-02 13:35:02')->matchMonth())->true();
        $this->assert($tab('* * * jan,mar,aug *', '2023-07-02 13:35:02')->matchMonth())->false();
        $this->assert($tab('* * * jan,mar,aug *', '2023-08-02 13:35:02')->matchMonth())->true();
        $this->assert($tab('* * * jan,mar,aug *', '2023-09-02 13:35:02')->matchMonth())->false();
        $this->assert($tab('* * * 1,3,8 *', '2023-01-02 13:35:02')->matchMonth())->true();
        $this->assert($tab('* * * 1,3,8 *', '2023-02-02 13:35:02')->matchMonth())->false();
        $this->assert($tab('* * * 1,3,8 *', '2023-03-02 13:35:02')->matchMonth())->true();
        $this->assert($tab('* * * 1,3,8 *', '2023-07-02 13:35:02')->matchMonth())->false();
        $this->assert($tab('* * * 1,3,8 *', '2023-08-02 13:35:02')->matchMonth())->true();
        $this->assert($tab('* * * 1,3,8 *', '2023-09-02 13:35:02')->matchMonth())->false();

        // Every minute
        $this->assert($tab('* * * * *', '2023-09-02 13:35:02')->matchMonth())->true();

        // 1-12 max range
        $this->assert($bad('* * * 0 *', '2023-02-20 02:59:59', 'matchMonth'))->exception(Tell_Cron_Exception::class);
        $this->assert($bad('* * * 13 *', '2023-02-20 02:59:59', 'matchMonth'))->exception(Tell_Cron_Exception::class);
        $this->assert($bad('* * * 0-12 *', '2023-02-20 02:59:59', 'matchMonth'))->exception(Tell_Cron_Exception::class);
        $this->assert($bad('* * * 5-3 *', '2023-02-20 02:59:59', 'matchMonth'))->exception(Tell_Cron_Exception::class);
        $this->assert($bad('* * * jan-foo *', '2023-02-20 02:59:59', 'matchMonth'))->exception(Tell_Cron_Exception::class);
        $this->assert($bad('* * * may-jan *', '2023-02-20 02:59:59', 'matchMonth'))->exception(Tell_Cron_Exception::class);

    })->then('matchDayWeek()', function() use ($tab, $bad) {

        // Every minute, Monday through Friday
        $this->assert($tab('* * * * mon-fri', '2023-02-26 23:59:59')->matchDayWeek())->false(); // sun
        $this->assert($tab('* * * * mon-fri', '2023-02-27 14:10:30')->matchDayWeek())->true();  // mon
        $this->assert($tab('* * * * mon-fri', '2023-02-28 14:10:30')->matchDayWeek())->true();  // tue
        $this->assert($tab('* * * * mon-fri', '2023-03-01 14:10:30')->matchDayWeek())->true();  // wed
        $this->assert($tab('* * * * mon-fri', '2023-03-02 14:10:30')->matchDayWeek())->true();  // thu
        $this->assert($tab('* * * * mon-fri', '2023-03-03 23:59:59')->matchDayWeek())->true();  // fri
        $this->assert($tab('* * * * mon-fri', '2023-03-04 00:00:00')->matchDayWeek())->false(); // sat
        $this->assert($tab('* * * * 1-5', '2023-02-26 23:59:59')->matchDayWeek())->false();     // sun
        $this->assert($tab('* * * * 1-5', '2023-02-27 14:10:30')->matchDayWeek())->true();      // mon
        $this->assert($tab('* * * * 1-5', '2023-02-28 14:10:30')->matchDayWeek())->true();      // tue
        $this->assert($tab('* * * * 1-5', '2023-03-01 14:10:30')->matchDayWeek())->true();      // wed
        $this->assert($tab('* * * * 1-5', '2023-03-02 14:10:30')->matchDayWeek())->true();      // thu
        $this->assert($tab('* * * * 1-5', '2023-03-03 23:59:59')->matchDayWeek())->true();      // fri
        $this->assert($tab('* * * * 1-5', '2023-03-04 00:00:00')->matchDayWeek())->false();     // sat

        // Every minute, every 2 days of the week, Monday through Friday
        $this->assert($tab('* * * * 1-5/2', '2023-02-28 10:05:00')->matchDayWeek())->false(); // tue
        $this->assert($tab('* * * * 1-5/2', '2023-03-01 10:05:00')->matchDayWeek())->true();  // wed
        $this->assert($tab('* * * * 1-5/2', '2023-03-02 10:05:00')->matchDayWeek())->false(); // thu
        $this->assert($tab('* * * * 1-5/2', '2023-03-03 10:05:00')->matchDayWeek())->true();  // fri
        $this->assert($tab('* * * * 1-5/2', '2023-03-04 10:05:00')->matchDayWeek())->false(); // sat
        $this->assert($tab('* * * * 1-5/2', '2023-03-05 10:05:00')->matchDayWeek())->false(); // sun
        $this->assert($tab('* * * * 1-5/2', '2023-03-06 10:05:00')->matchDayWeek())->true();  // mon

        // Every minute, only on Tuesday and Thursday
        $this->assert($tab('* * * * tue,thu', '2023-02-28 12:00:00')->matchDayWeek())->true();  // tue
        $this->assert($tab('* * * * tue,thu', '2023-03-01 12:00:00')->matchDayWeek())->false(); // wed
        $this->assert($tab('* * * * tue,thu', '2023-03-02 12:00:00')->matchDayWeek())->true();  // thu
        $this->assert($tab('* * * * tue,thu', '2023-03-03 12:00:00')->matchDayWeek())->false(); // fri
        $this->assert($tab('* * * * 2,4', '2023-02-28 12:00:00')->matchDayWeek())->true();      // tue
        $this->assert($tab('* * * * 2,4', '2023-03-01 12:00:00')->matchDayWeek())->false();     // wed
        $this->assert($tab('* * * * 2,4', '2023-03-02 12:00:00')->matchDayWeek())->true();      // thu
        $this->assert($tab('* * * * 2,4', '2023-03-03 12:00:00')->matchDayWeek())->false();     // fri

        // Every minute
        $this->assert($tab('* * * * *', '2023-03-06 10:05:00')->matchDayWeek())->true();

        // 0-6 max range
        $this->assert($bad('* * * * 7', '2023-02-28 12:30:00', 'matchDayWeek'))->exception(Tell_Cron_Exception::class);
        $this->assert($bad('* * * * 0-7', '2023-02-28 12:30:00', 'matchDayWeek'))->exception(Tell_Cron_Exception::class);
        $this->assert($bad('* * * * 5-3', '2023-02-28 12:30:00', 'matchDayWeek'))->exception(Tell_Cron_Exception::class);
        $this->assert($bad('* * * * 0-foo', '2023-02-28 12:30:00', 'matchDayWeek'))->exception(Tell_Cron_Exception::class);
        $this->assert($bad('* * * * foo', '2023-02-28 12:30:00', 'matchDayWeek'))->exception(Tell_Cron_Exception::class);

    })->then('matches()', function() use ($tab, $bad) {

        // Every 5 minutes
        $this->assert($tab('*/5 * * * *', '2023-03-06 10:05:03')->matches())->true();
        $this->assert($tab('*/5 * * * *', '2023-03-06 10:06:03')->matches())->false();
        $this->assert($tab('*/5 * * * *', '2023-03-06 10:10:53')->matches())->true();

        // At 04:15 PM, Monday through Friday
        $this->assert($tab('15 16 * * mon-fri', '2023-03-06 16:14:59')->matches())->false(); // mon
        $this->assert($tab('15 16 * * mon-fri', '2023-03-06 16:15:06')->matches())->true();  // mon
        $this->assert($tab('15 16 * * mon-fri', '2023-03-07 16:15:00')->matches())->true();  // tue
        $this->assert($tab('15 16 * * mon-fri', '2023-03-08 16:15:03')->matches())->true();  // wed
        $this->assert($tab('15 16 * * mon-fri', '2023-03-09 16:15:01')->matches())->true();  // thu
        $this->assert($tab('15 16 * * mon-fri', '2023-03-10 16:15:00')->matches())->true();  // fri
        $this->assert($tab('15 16 * * mon-fri', '2023-03-11 16:15:00')->matches())->false(); // sat
        $this->assert($tab('15 16 * * 1-5', '2023-03-06 16:14:59')->matches())->false();     // mon
        $this->assert($tab('15 16 * * 1-5', '2023-03-06 16:15:06')->matches())->true();      // mon
        $this->assert($tab('15 16 * * 1-5', '2023-03-07 16:15:00')->matches())->true();      // tue
        $this->assert($tab('15 16 * * 1-5', '2023-03-08 16:15:03')->matches())->true();      // wed
        $this->assert($tab('15 16 * * 1-5', '2023-03-09 16:15:01')->matches())->true();      // thu
        $this->assert($tab('15 16 * * 1-5', '2023-03-10 16:15:00')->matches())->true();      // fri
        $this->assert($tab('15 16 * * 1-5', '2023-03-11 16:15:00')->matches())->false();     // sat

        // At 7, 27, and 47 minutes past the hour, between 09:00 AM and 04:59 PM, Monday through Friday
        $this->assert($tab('7,27,47 9-16 * * mon-fri', '2023-02-26 09:07:00')->matches())->false(); // sun
        $this->assert($tab('7,27,47 9-16 * * mon-fri', '2023-02-26 14:27:01')->matches())->false(); // sun
        $this->assert($tab('7,27,47 9-16 * * mon-fri', '2023-02-26 16:47:01')->matches())->false(); // sun
        $this->assert($tab('7,27,47 9-16 * * mon-fri', '2023-02-27 09:07:00')->matches())->true();  // mon
        $this->assert($tab('7,27,47 9-16 * * mon-fri', '2023-02-27 14:27:01')->matches())->true();  // mon
        $this->assert($tab('7,27,47 9-16 * * mon-fri', '2023-02-27 16:47:01')->matches())->true();  // mon
        $this->assert($tab('7,27,47 9-16 * * mon-fri', '2023-02-27 17:47:01')->matches())->false(); // mon
        $this->assert($tab('7,27,47 9-16 * * mon-fri', '2023-02-27 08:07:01')->matches())->false(); // mon
        $this->assert($tab('7,27,47 9-16 * * 1-5', '2023-02-26 09:07:00')->matches())->false();     // sun
        $this->assert($tab('7,27,47 9-16 * * 1-5', '2023-02-26 14:27:01')->matches())->false();     // sun
        $this->assert($tab('7,27,47 9-16 * * 1-5', '2023-02-26 16:47:01')->matches())->false();     // sun
        $this->assert($tab('7,27,47 9-16 * * 1-5', '2023-02-27 09:07:00')->matches())->true();      // mon
        $this->assert($tab('7,27,47 9-16 * * 1-5', '2023-02-27 14:27:01')->matches())->true();      // mon
        $this->assert($tab('7,27,47 9-16 * * 1-5', '2023-02-27 16:47:01')->matches())->true();      // mon
        $this->assert($tab('7,27,47 9-16 * * 1-5', '2023-02-27 17:47:01')->matches())->false();     // mon
        $this->assert($tab('7,27,47 9-16 * * 1-5', '2023-02-27 08:07:01')->matches())->false();     // mon

    });

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

$inspect('Tell_Uri', function() {

    $this->try('_()', function() {

        // @todo

    });

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

// ---------------------------------------------------------------------------------------------

$inspect('Tell_Client - Parameters', function() {

    $good = function($request, $response, $method, Closure $extra = NULL) {

        $data = function($key) use ($request) {
            return Tell_Arr::get($request->toArr(), $key);
        };

        $this->assert($request)->instanceOf(Tell_Client_Request::class);

        $this->assert($response)->instanceOf(Tell_Client_Response::class);

        $this->assert($response->code())->equals(200);

        $this->assert($response->body('method'))->equals($method);

        $this->assert($response->isJson())->true();

        $this->assert($response->header('Authorization'))->equals($data('headers.authorization.1'));

        $this->assert($response->body())->iterable();

        $this->assert($response->body('query.account'))->equals((string) $data('query.account'));

        $this->assert($response->body('input.email'))->equals($data('data.email'));

        $this->assert($response->body('input.country'))->equals($data('data.country'));

        $this->assert($response->body('input.Country'))->empty();

        if ($extra) {
            $extra->bindTo($this)->__invoke($data, $response);
        }

    };

    $bad = function($request, $response) {

        $this->assert($request)->instanceOf(Tell_Client_Request::class);

        $this->assert($response)->instanceOf(Tell_Client_Response::class);

        $this->assert($response->code())->equals(404);

    };

    $emptyBody = function($request, $response) {

        $this->assert($request)->instanceOf(Closure::class);

        $this->assert($response)->instanceOf(Tell_Client_Response::class);

        $this->assert($response->body('body'))->equals('');

    };

    $postBody = function($request, $response) {

        $this->assert($request)->instanceOf(Closure::class);

        $this->assert($response)->instanceOf(Tell_Client_Response::class);

        $this->assert($response->body('body'))->equals('first_name=Bob&last_name=Smith&email=bob-smith%40foobar.com&country=US');

        $this->assert($response->body('headers.Content-Type'))->equals('application/x-www-form-urlencoded');

    };

    $data = (new Tell_Client_Request())
        ->header('Authorization', $this->code)
        ->query(['status' => 'Active', 'account' => 12])
        ->data([
            'first_name' => $this->first_name,
            'last_name'  => $this->last_name,
            'email'      => $this->email,
            'country'    => 'US',
        ]);

    $this->try('cURL Driver', function() use ($data, $good, $bad, $emptyBody, $postBody) {

        $config = ['driver' => 'curl'];

        $good($data, (new Tell_Client($config))->get('test/webhook/get/123', $data), 'GET', $emptyBody);

        $bad($data, (new Tell_Client($config))->get('test/webhook/post', $data), 'GET');

        $good($data, (new Tell_Client($config))->post('test/webhook/post', $data), 'POST', $postBody);

    });

    $this->try('Socket Driver', function() use ($data, $good, $bad, $emptyBody, $postBody) {

        $config = ['driver' => 'socket'];

        $good($data, (new Tell_Client($config))->get('test/webhook/get/123', $data), 'GET', $emptyBody);

        $bad($data, (new Tell_Client($config))->get('test/webhook/post', $data), 'GET');

        $good($data, (new Tell_Client($config))->post('test/webhook/post', $data), 'POST', $postBody);

    });

})->vars([
    'code'       => 'feVUrB4f0j5ScMd3',
    'first_name' => 'Bob',
    'last_name'  => 'Smith',
    'email'      => 'bob-smith@foobar.com',
])->onlyWhen(function() {
    return ! Tell_Request::isCli();
});

// ---------------------------------------------------------------------------------------------

$inspect('Tell_Client - JSON', function() {

    $good = function($request, $response, $method, Closure $extra = NULL) {

        $data = function($key) use ($request) {
            return Tell_Arr::get($request->toArr(), $key);
        };

        $this->assert($request)->instanceOf(Tell_Client_Request::class);

        $this->assert($response)->instanceOf(Tell_Client_Response::class);

        $this->assert($response->code())->equals(200);

        $this->assert($response->body('method'))->equals($method);

        $this->assert($response->isJson())->true();

        $this->assert($response->header('Authorization'))->equals($data('headers.authorization.1'));

        $this->assert($response->body())->iterable();

        $this->assert($response->body('query.account'))->equals((string) $data('query.account'));

        $this->assert($response->body('input.email'))->equals($data('data.email'));

        $this->assert($response->body('input.country'))->equals($data('data.country'));

        $this->assert($response->body('input.Country'))->empty();

        if ($extra) {
            $extra->bindTo($this)->__invoke($data, $response);
        }

    };

    $emptyBody = function($request, $response) {

        $this->assert($request)->instanceOf(Closure::class);

        $this->assert($response)->instanceOf(Tell_Client_Response::class);

        $this->assert($response->body('body'))->equals('');

    };

    $jsonBody = function($request, $response) {

        $this->assert($request)->instanceOf(Closure::class);

        $this->assert($response)->instanceOf(Tell_Client_Response::class);

        $this->assert($response->body('body'))->equals('{"first_name":"Bob","last_name":"Smith","email":"bob-smith@foobar.com","country":"CA"}');

        $this->assert($response->body('headers.Content-Type'))->equals('application/json');

    };

    $json = (new Tell_Client_Request())
        ->header('Authorization', $this->code)
        ->query(['status' => 'Disabled', 'account' => 65])
        ->json([
            'first_name' => $this->first_name,
            'last_name'  => $this->last_name,
            'email'      => $this->email,
            'country'    => 'CA',
        ]);

    $this->try('cURL Driver', function() use ($json, $good, $emptyBody, $jsonBody) {

        $config = ['driver' => 'curl'];

        $good($json, (new Tell_Client($config))->get('test/webhook/json', $json), 'GET', $emptyBody);

        $good($json, (new Tell_Client($config))->post('test/webhook/json', $json), 'POST', $jsonBody);

    });

    $this->try('Socket Driver', function() use ($json, $good, $emptyBody, $jsonBody) {

        $config = ['driver' => 'socket'];

        $good($json, (new Tell_Client($config))->get('test/webhook/json', $json), 'GET', $emptyBody);

        $good($json, (new Tell_Client($config))->post('test/webhook/json', $json), 'POST', $jsonBody);

    });

})->vars([
    'code'       => 'feVUrB4f0j5ScMd3',
    'first_name' => 'Bob',
    'last_name'  => 'Smith',
    'email'      => 'bob-smith@foobar.com',
])->onlyWhen(function() {
    return ! Tell_Request::isCli();
});

// ---------------------------------------------------------------------------------------------

$inspect('Tell_Client - Binary', function() {

    $good = function($request, $response, $method) {

        $this->assert($request)->instanceOf(Tell_Client_Request::class);

        $this->assert($response)->instanceOf(Tell_Client_Response::class);

        $this->assert($response->code())->equals(200);

        $this->assert($response->body('method'))->equals($method);

        $this->assert(base64_decode($response->body('body')))->equals($this->image);

    };

    $data = (new Tell_Client_Request())
        ->header('Authorization', $this->code)
        ->query(['status' => 'Active', 'account' => 15])
        ->data($this->image);

    $this->try('cURL Driver', function() use ($data, $good) {

        $config = ['driver' => 'curl'];

        $good($data, (new Tell_Client($config))->post('test/webhook/binary', $data), 'POST');

        $good($data, (new Tell_Client($config))->put('test/webhook/binary', $data), 'PUT');

        $good($data, (new Tell_Client($config))->delete('test/webhook/binary', $data), 'DELETE');

        $good($data, (new Tell_Client($config))->patch('test/webhook/binary', $data), 'PATCH');

    });

    $this->try('Socket Driver', function() use ($data, $good) {

        $config = ['driver' => 'socket'];

        $good($data, (new Tell_Client($config))->post('test/webhook/binary', $data), 'POST');

        $good($data, (new Tell_Client($config))->put('test/webhook/binary', $data), 'PUT');

        $good($data, (new Tell_Client($config))->delete('test/webhook/binary', $data), 'DELETE');

        $good($data, (new Tell_Client($config))->patch('test/webhook/binary', $data), 'PATCH');

    });

})->vars([
    'code'  => 'kRPlYbudWQ7gd79v',
    'image' => file_get_contents(__DIR__ . '/../assets/images/shark.jpeg'),
])->onlyWhen(function() {
    return ! Tell_Request::isCli();
});

// ---------------------------------------------------------------------------------------------

$inspect('Tell_Client - Empty', function() {

    $good = function($request, $response, $method) {

        $this->assert($request)->instanceOf(Tell_Client_Request::class);

        $this->assert($response)->instanceOf(Tell_Client_Response::class);

        $this->assert($response->code())->equals(200);

        $this->assert($response->header('authorization'))->equals($this->code);

        $this->assert($response->header('x-request-method'))->equals($method);

        $this->assert($response->body('body'))->null();

    };

    $data = (new Tell_Client_Request())
        ->header('Authorization', $this->code)
        ->query(['status' => 'Active', 'account' => 15]);

    $this->try('cURL Driver', function() use ($data, $good) {

        $config = ['driver' => 'curl'];

        $good($data, (new Tell_Client($config))->head('test/webhook/empty', $data), 'HEAD');

        $good($data, (new Tell_Client($config))->delete('test/webhook/empty', $data), 'DELETE');

    });

    $this->try('Socket Driver', function() use ($data, $good) {

        $config = ['driver' => 'socket'];

        $good($data, (new Tell_Client($config))->head('test/webhook/empty', $data), 'HEAD');

        $good($data, (new Tell_Client($config))->delete('test/webhook/empty', $data), 'DELETE');

    });

})->vars([
    'code' => 'kRPlYbudWQ7gd79v',
])->onlyWhen(function() {
    return ! Tell_Request::isCli();
});

// ---------------------------------------------------------------------------------------------
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

$inspect('Tell_Cookie', function() {

    $this->try('Tell_Cookie_Write', function() {

        $cookie = new Tell_Cookie_Write($this->name, $this->value, [
            'expires'  => 0,
            'path'     => '',
            'domain'   => '',
            'secure'   => FALSE,
            'httponly' => FALSE,
            'maxage'   => NULL,
            'samesite' => 'Lax',
        ], $this->hash_key);

        $this->assert($cookie->sign())->equals($this->signed);

        $this->assert($cookie->unsign())->equals($this->value);

        $this->assert($cookie->options())
            ->arr()
            ->hasKey('expires')
            ->hasKey('path')
            ->hasKey('domain')
            ->hasKey('secure')
            ->hasKey('httponly')
            ->hasKey('samesite')
            ->contains('Lax')
            ->contains('/');

    });

    $this->try('Tell_Cookie_Read', function() {

        $cookie = new Tell_Cookie_Read($this->name, $this->signed, $this->hash_key);

        $this->assert($cookie->__toString())->equals($this->value);

        $this->assert($cookie->unsign())->equals($this->value);

        $this->assert($cookie->sign())->equals($this->signed);

    });

})->vars([
    'name'      => 'TestTellCookie',
    'value'     => 'hello world',
    'hash_key'  => 'test-hash-key-123',
    'signed'    => '79a6a3b9474ba9874f903be5e19abf13df051b32_hello world',
    'signature' => '79a6a3b9474ba9874f903be5e19abf13df051b32',
]);
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

// -------------------------------------------------------------------------------------------------

class Inspect_Test_Auth_Session extends Tell_Auth_Session
{
    const RECORD_TABLE   = '_test_users';
    const FIELD_ID       = 'id';
    const FIELD_PASSWORD = 'password';
    const SECRET_COOKIE  = 'DQ89eTd2uueXWuzgDif9vgEKTEIJAO1g';
    const SECRET_PERSIST = 'WwZtarOG03HpjjYYZ3ZIllmTxoYJrvUE';

    public function isActive()
        : bool
    {
        return 'Active' === $this->get('status');
    }

    public function findById($id)
        : ? array
    {
        if (123 !== (int) $id) {
            return NULL;
        }

        return [
            'id'         => 123,
            'first_name' => 'Bob',
            'last_name'  => 'Smith',
            'email'      => 'bob@example.com',
            'password'   => '$2y$12$6xVfIWPWnxSa789yxPhvVeAkPtEOiL95jFkDyCUXvAW2hVfPmTyBO', // testing
        ];
    }

    public function findByUser($user)
        : ? array
    {
        if ('bob@example.com' !== $user) {
            return NULL;
        }

        return [
            'id'         => 123,
            'first_name' => 'Bob',
            'last_name'  => 'Smith',
            'email'      => 'bob@example.com',
            'password'   => '$2y$12$6xVfIWPWnxSa789yxPhvVeAkPtEOiL95jFkDyCUXvAW2hVfPmTyBO', // testing
        ];
    }

    protected function persistHashKey()
        : string
    {
        return self::SECRET_PERSIST;
    }

    protected function cookieHashKey()
        : string
    {
        return self::SECRET_COOKIE;
    }

    public $test = [
        'onAuthenticate'        => [],
        'onAuthenticateSuccess' => [],
        'onAuthenticateFailure' => [],
        'onAuthenticateRevoke'  => [],
        'onPersist'             => [],
        'onPersistSuccess'      => [],
        'onPersistFailure'      => [],
        'onAuthorize'           => [],
        'onAuthorizeSuccess'    => [],
    ];

    public function onAuthenticate(...$args)
    {
        $this->test['onAuthenticate'] = $args;
    }

    public function onAuthenticateSuccess(...$args)
    {
        $this->test['onAuthenticateSuccess'] = $args;
    }

    public function onAuthenticateFailure(...$args)
    {
        $this->test['onAuthenticateFailure'] = $args;
    }

    public function onAuthenticateRevoke(...$args)
    {
        $this->test['onAuthenticateRevoke'] = $args;
    }

    public function onPersist(...$args)
    {
        $this->test['onPersist'] = $args;
    }

    public function onPersistSuccess(...$args)
    {
        $this->test['onPersistSuccess'] = $args;
    }

    public function onPersistFailure(...$args)
    {
        $this->test['onPersistFailure'] = $args;
    }

    public function onAuthorize(...$args)
    {
        $this->test['onAuthorize'] = $args;
    }

    public function onAuthorizeSuccess(...$args)
    {
        $this->test['onAuthorizeSuccess'] = $args;
    }
}

// -------------------------------------------------------------------------------------------------

$inspect('Tell_Auth_Session >> Instantiate', function(Inspect_Test_Auth_Session $auth) {

    $this->assert($auth->test['onAuthenticate'])->array()->empty();

    $this->assert($auth->test['onAuthenticateSuccess'])->array()->empty();

    $this->assert($auth->test['onAuthenticateFailure'])->array()->empty();

    $this->assert($auth->test['onPersist'])->array()->empty();

    $this->assert($auth->test['onPersistSuccess'])->array()->empty();

    $this->assert($auth->test['onPersistFailure'])->array()->empty();

    $this->assert($auth->test['onAuthorize'])->array()->empty();

    $this->assert($auth->test['onAuthorizeSuccess'])->array()->empty();

})->swap(function() {

    $db = Mockery::mock(Tell_Db::class);

    $persist = Mockery::mock(Tell_Auth_Persist::class);
    $persist->shouldReceive('hashKey')->andReturnSelf();
    $persist->shouldReceive('graft')->andReturn(NULL);

    return [
        'Tell_Db'           => $db,
        'Tell_Auth_Persist' => $persist,
    ];

});

// -------------------------------------------------------------------------------------------------

$inspect('Tell_Auth_Session >> Login & Logout', function(Inspect_Test_Auth_Session $auth) {

    $this->assert($auth->login('bob@example.com', 'testing', TRUE))->true();

    $this->assert($auth->test['onAuthenticate'][0])->equals([
        'id'         => 123,
        'first_name' => 'Bob',
        'last_name'  => 'Smith',
        'email'      => 'bob@example.com',
        'password'   => '$2y$12$6xVfIWPWnxSa789yxPhvVeAkPtEOiL95jFkDyCUXvAW2hVfPmTyBO',
    ]);

    $this->assert($auth->test['onAuthenticate'][1])->instanceOf($auth);

    $this->assert($auth->test['onAuthenticateSuccess'])->array()->hasKey(0)->hasKey(1);

    $this->assert($auth->test['onAuthenticateFailure'])->array()->empty();

    $this->assert($auth->test['onAuthenticateRevoke'])->array()->empty();

    $this->assert($auth->test['onPersist'])->array()->empty();

    $this->assert($auth->test['onPersistSuccess'])->array()->empty();

    $this->assert($auth->test['onPersistFailure'])->array()->empty();

    $this->assert($auth->test['onAuthorize'])->array()->empty();

    $this->assert($auth->test['onAuthorizeSuccess'])->array()->empty();

    $this->assert($auth->isAuthenticated())->true();

    $this->assert($auth->logout())->true();

    $this->assert($auth->test['onAuthenticateRevoke'])->array()->hasKey(0)->hasKey(1);

    $this->assert($auth->isAuthenticated())->false();

})->swap(function() {

    $db = Mockery::mock(Tell_Db::class);

    $refresh = [
        Tell_Security::random(32),
        Tell_Security::random(32),
        1678300000,
    ];

    $persist = Mockery::mock(Tell_Auth_Persist::class);
    $persist->shouldReceive('hashKey')->andReturnSelf();
    $persist->shouldReceive('graft')->andReturn(NULL);
    $persist->shouldReceive('create')->andReturn($refresh);
    $persist->shouldReceive('delete')->andReturn(TRUE);

    return [
        'Tell_Db'           => $db,
        'Tell_Auth_Persist' => $persist,
    ];

});

// -------------------------------------------------------------------------------------------------

$inspect('Tell_Auth_Session >> Valid Persistent Cookie', function(Inspect_Test_Auth_Session $auth) {

    $this->assert($auth->test['onAuthenticate'])->array()
        ->hasKey(0)
        ->hasKey(1)
        ->hasKey(2)
        ->hasKey(3);

    $this->assert($auth->test['onAuthenticate'][0])->equals([
        'id'         => 123,
        'first_name' => 'Bob',
        'last_name'  => 'Smith',
        'email'      => 'bob@example.com',
        'password'   => '$2y$12$6xVfIWPWnxSa789yxPhvVeAkPtEOiL95jFkDyCUXvAW2hVfPmTyBO',
    ]);

    $this->assert($auth->test['onAuthenticate'][1])->equals([
        'id'              => 1,
        'parent_table'    => '_test_users',
        'parent_record'   => 123,
        'access_selector' => 'd5HwgHaB9AK0ZhaW9ZXxiMNBQVYg8tGw',
        'access_token'    => 'f7dd5af516104c5815f5cfd95497d9bdb1bab68d9c9f63f9b9bc52bbe9df6b9f',
        'class_name'      => 'Inspect_Test_Auth_Session',
        'last_ip'         => '172.16.238.1',
        'created'         => '2023-02-26 02:35:21',
        'expires'         => '2023-03-12 02:35:21',
    ]);

    $this->assert($auth->test['onAuthenticate'][2])->equals([
        'data'   => 'JbhhPAvb2GHMab7UHgSuMoOSnW3I46n5_ets84uaemGgnT9VZdVurMgPN7sAlg0GT',
        'select' => 'JbhhPAvb2GHMab7UHgSuMoOSnW3I46n5',
        'token'  => 'ets84uaemGgnT9VZdVurMgPN7sAlg0GT',
    ]);

    $this->assert($auth->test['onAuthenticate'][3])->instanceOf($auth);

    $this->assert($auth->test['onAuthenticateFailure'])->array()->empty();

    $this->assert($auth->test['onAuthenticateRevoke'])->array()->empty();

    $this->assert($auth->test['onPersist'])->equals($auth->test['onAuthenticate']);

    $this->assert($auth->test['onPersistSuccess'])->equals($auth->test['onAuthenticateSuccess']);

    $this->assert($auth->test['onPersistFailure'])->array()->empty();

    $this->assert($auth->test['onAuthorize'])->array()->empty();

    $this->assert($auth->test['onAuthorizeSuccess'])->array()->empty();

    $this->assert($auth->isAuthenticated())->true();

})->request(function() {

    $cookie = new Tell_Cookie_Read('_tell_auths__Inspect_Test_Auth_Session', '760418426d3517c7323d6328acbd49241f7b0215_JbhhPAvb2GHMab7UHgSuMoOSnW3I46n5_ets84uaemGgnT9VZdVurMgPN7sAlg0GT');

    return [
        'method' => 'GET',
        'cookie' => [
            '_tell_auths__Inspect_Test_Auth_Session' => $cookie,
        ],
    ];

})->swap(function() {

    $record = [
        'id'              => 1,
        'parent_table'    => '_test_users',
        'parent_record'   => 123,
        'access_selector' => 'd5HwgHaB9AK0ZhaW9ZXxiMNBQVYg8tGw',
        'access_token'    => 'f7dd5af516104c5815f5cfd95497d9bdb1bab68d9c9f63f9b9bc52bbe9df6b9f',
        'class_name'      => 'Inspect_Test_Auth_Session',
        'last_ip'         => '172.16.238.1',
        'created'         => '2023-02-26 02:35:21',
        'expires'         => '2023-03-12 02:35:21',
    ];

    $db = Mockery::mock(Tell_Db::class);

    $persist = Mockery::mock(Tell_Auth_Persist::class);
    $persist->shouldReceive('hashKey')->andReturnSelf();
    $persist->shouldReceive('graft')->andReturn(NULL);
    $persist->shouldReceive('load')->andReturn($record);

    return [
        'Tell_Db'           => $db,
        'Tell_Auth_Persist' => $persist,
    ];

});

// -------------------------------------------------------------------------------------------------
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

function eventFoo(Tell_Bridge_Event $e, $true, $false, $int, $float, $hello, $world)
{
    affirm($true)->true();

    affirm($false)->false();

    affirm($int)->equals(3);

    affirm($float)->equals(1.5);

    affirm($hello)->equals('hello');

    affirm($world)->equals('world');

    return __FUNCTION__;
}

function eventBarDoe($a, $female, $deer, $true, $float, $e)
{
    affirm($a)->equals('a');

    affirm($female)->equals('female');

    affirm($deer)->equals('deer');

    affirm($true)->true();

    affirm($float)->equals(2.5);

    affirm($e)->instanceOf(Tell_Bridge_Event::class);

    return __FUNCTION__;
}

function eventBarRei(Tell_Bridge_Event $e, $a, $drop, $of, $golden, $sun, $true, $float)
{
    affirm($a)->equals('a');

    affirm($drop)->equals('drop');

    affirm($of)->equals('of');

    affirm($golden)->equals('golden');

    affirm($sun)->equals('sun');

    affirm($true)->true();

    affirm($float)->equals(2.5);

    return __FUNCTION__;
}

function eventBarMei($a, $name, $i, $call, $myself, $true, $float, $e)
{
    affirm($a)->equals('a');

    affirm($name)->equals('name');

    affirm($i)->equals('I');

    affirm($call)->equals('call');

    affirm($myself)->equals('myself');

    affirm($true)->true();

    affirm($float)->equals(2.5);

    affirm($e)->instanceOf(Tell_Bridge_Event::class);

    return __FUNCTION__;
}

class Event_Foo
{
    public function __invoke(Tell_Bridge_Event $e, $true, $false, $int, $float, $hello, $world)
    {
        affirm(eventFoo(...func_get_args()))->equals('eventFoo');

        return __METHOD__;
    }
}

class Event_Bar
{
    public function doe($a, $female, $deer, $true, $float, $e)
    {
        affirm(eventBarDoe(...func_get_args()))->equals('eventBarDoe');

        return __METHOD__;
    }

    public function rei(Tell_Bridge_Event $e, $a, $drop, $of, $golden, $sun, $true, $float)
    {
        affirm(eventBarRei(...func_get_args()))->equals('eventBarRei');

        return __METHOD__;
    }

    public function mei($a, $name, $i, $call, $myself, $true, $float, $e)
    {
        affirm(eventBarMei(...func_get_args()))->equals('eventBarMei');

        return __METHOD__;
    }
}

$inspect('Tell_Event', function() {

    $this->try('Closures', function() {

        $event = new Tell_Event(new Tell_Container());

        $affirms = function($i, $am, $a, $nice, $shark, $false, $ten, $e) {

            affirm($e)->instanceOf(Tell_Bridge_Event::class);

            affirm(['foo', 'bar', 'fubar'])->contains($e->name);

            affirm($i)->equals('I');

            affirm($am)->equals('am');

            affirm($a)->equals('a');

            affirm($nice)->equals('nice');

            affirm($shark)->equals('shark');

            affirm($false)->false();

            affirm($ten)->equals(10);

        };

        $event->register('foo;bar', function($i, $am, $a, $nice, $shark, $false, $ten, $e) use ($affirms) {

            $affirms($i, $am, $a, $nice, $shark, $false, $ten, $e);

            return 'Listener A';

        });

        $event->register(['foo', 'fubar'], function(Tell_Bridge_Event $be, $i, $am, $a, $nice, $shark, $false, $ten, $e) use ($affirms) {

            affirm($be)->instanceOf(Tell_Bridge_Event::class);

            $affirms($i, $am, $a, $nice, $shark, $false, $ten, $e);

            return 'Listener B';

        });

        $event->register('foo', function($i, $am, $a, $nice, $shark, $false, $ten, $e) use ($affirms) {

            $affirms($i, $am, $a, $nice, $shark, $false, $ten, $e);

            $e->stop();

            return 'Listener C';

        });

        $event->register('foo', function($i, $am, $a, $nice, $shark, $false, $ten, $e) use ($affirms) {

            $affirms($i, $am, $a, $nice, $shark, $false, $ten, $e);

            return 'Listener Never Invoked';

        });

        $event->register('bar', function($i, $am, $a, $nice, $shark, $false, $ten, $e) use ($affirms) {

            $affirms($i, $am, $a, $nice, $shark, $false, $ten, $e);

            return 'Listener Priority';

        }, 9);

        if ($foo = $event->trigger('foo', 'I', 'am', 'a', 'nice', 'shark', FALSE, 10)) {

            $this->assert($foo)->instanceOf(Tell_Bridge_Event::class);

            $this->assert($foo->result())->equals('Listener C');

        }

        if ($bar = $event->trigger('bar', 'I', 'am', 'a', 'nice', 'shark', FALSE, 10)) {

            $this->assert($bar)->instanceOf(Tell_Bridge_Event::class);

            $this->assert($bar->result())->equals('Listener A');

            $this->assert($bar->result->pop())->equals('Listener A');

            $this->assert($bar->result->pop())->equals('Listener Priority');

        }

        if ($fubar = $event->trigger('fubar', 'I', 'am', 'a', 'nice', 'shark', FALSE, 10)) {

            $this->assert($fubar)->instanceOf(Tell_Bridge_Event::class);

            $this->assert($fubar->result())->equals('Listener B');

        }

    });

    $this->try('Functions', function() {

        $event = new Tell_Event(new Tell_Container());

        $event->register('foo', 'eventFoo');

        $event->register('doe', 'eventBarDoe');

        $event->register('rei', 'eventBarRei');

        $event->register('mei', 'eventBarMei');

        if ($foo = $event->trigger('foo', TRUE, FALSE, 3, 1.5, 'hello', 'world')) {

            $this->assert($foo)->instanceOf(Tell_Bridge_Event::class);

            $this->assert($foo->result())->equals('eventFoo');

        }

        if ($doe = $event->trigger('doe', 'a', 'female', 'deer', TRUE, 2.5)) {

            $this->assert($doe)->instanceOf(Tell_Bridge_Event::class);

            $this->assert($doe->result())->equals('eventBarDoe');

        }

        if ($rei = $event->trigger('rei', 'a', 'drop', 'of', 'golden', 'sun', TRUE, 2.5)) {

            $this->assert($rei)->instanceOf(Tell_Bridge_Event::class);

            $this->assert($rei->result())->equals('eventBarRei');

        }

        if ($mei = $event->trigger('mei', 'a', 'name', 'I', 'call', 'myself', TRUE, 2.5)) {

            $this->assert($mei)->instanceOf(Tell_Bridge_Event::class);

            $this->assert($mei->result())->equals('eventBarMei');

        }

        $event->remove('mei');

        if ($mei = $event->trigger('mei', 'a', 'name', 'I', 'call', 'myself', TRUE, 2.5)) {

            $this->assert($mei)->instanceOf(Tell_Bridge_Event::class);

            $this->assert($mei->result())->null();

        }

    });

    $this->try('Classes', function() {

        $event = new Tell_Event(new Tell_Container());

        $event->register('foo', Event_Foo::class);

        $event->register('bar', Event_Bar::class);

        $event->register('bar.doe', Event_Bar::class);

        $event->register('bar.rei', Event_Bar::class);

        $event->register('bar.mei', Event_Bar::class);

        if ($foo = $event->trigger('foo', TRUE, FALSE, 3, 1.5, 'hello', 'world')) {

            $this->assert($foo)->instanceOf(Tell_Bridge_Event::class);

            $this->assert($foo->result())->equals('Event_Foo::__invoke');

        }

        if ($bar = $event->trigger('bar', 'hello', 'world')) {

            $this->assert($bar)->instanceOf(Tell_Bridge_Event::class);

            $this->assert($bar->result())->instanceOf(Event_Bar::class);

        }

        if ($doe = $event->trigger('bar.doe', 'a', 'female', 'deer', TRUE, 2.5)) {

            $this->assert($doe)->instanceOf(Tell_Bridge_Event::class);

            $this->assert($doe->result())->equals('Event_Bar::doe');

        }

        if ($rei = $event->trigger('bar.rei', 'a', 'drop', 'of', 'golden', 'sun', TRUE, 2.5)) {

            $this->assert($rei)->instanceOf(Tell_Bridge_Event::class);

            $this->assert($rei->result())->equals('Event_Bar::rei');

        }

        if ($mei = $event->trigger('bar.mei', 'a', 'name', 'I', 'call', 'myself', TRUE, 2.5)) {

            $this->assert($mei)->instanceOf(Tell_Bridge_Event::class);

            $this->assert($mei->result())->equals('Event_Bar::mei');

        }

    });

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

$inspect('Tell_Translate', function() {

    $this->try('_()', function() {

        // @todo

    });

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

$inspect('Tell_Password', function() {

    $this->try('_()', function() {

        // @todo

    });

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

// ---------------------------------------------------------------------------------------------

$inspect('Tell_Bridge_Currency', function() {

    // @todo

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

$inspect('Tell_Log', function() {

    $this->try('_()', function() {

        // @todo

    });

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

$inspect('Tell_Schema_Table_Sqlite', function(Tell_Db $db) {

    // @todo [once SQLite driver is finished]

})->swap(function(Tell_Event $event) {
    return [
        'Tell_Db' => new Tell_Db([
            'driver'             => 'sqlite',
            'database'           => __DIR__ . '/../assets/sql/tell_php_test.sqlite3',
            'elevate_exceptions' => TRUE,
            'log_failed'         => TRUE,
            'log_success'        => FALSE,
            'log_limit'          => 1000,
        ], $event),
    ];
})->onlyWhen(function() {
    return extension_loaded('pdo_sqlite') && env('TELL_PHP_DEV');
});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

$inspect('Tell_Asset', function() {

    // @todo

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

$inspect('Tell_Crypt', function() {

    $this->try('__()', function() {

        // @todo

    });

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

$inspect('Tell_Str', function() {

    $this->try('flatten()', function() {

        $before = "The quick brown fox\n"
                . "jumps over\r\n"
                . "the lazy\n\n\n\n"
                . "dog.";

        $after = "The quick brown fox jumps over the lazy dog.";

        $this->assert(Tell_Str::flatten($before))->equals($after);

    });

    $this->try('isRightToLeft()', function() {

        $english = $this->langs['en'];
        $russian = $this->langs['ru'];
        $hebrew  = $this->langs['iw'];

        $this->assert(Tell_Str::isRightToLeft($english))->false();
        $this->assert(Tell_Str::isRightToLeft($russian))->false();
        $this->assert(Tell_Str::isRightToLeft($hebrew))->true();

    });

    $this->try('isAscii()', function() {

        $tests = [
            $this->hebrew_cp1255     => FALSE,
            $this->hebrew_utf8       => FALSE,
            $this->windows_1252      => FALSE,
            'žščř'                   => FALSE,
            'hëllo-world-123'        => FALSE,
            'hšello-world'           => FALSE,
            "hëllowo'rld"            => FALSE,
            '(123ľ'                  => FALSE,
            'HelloW.orld FooBar'     => TRUE,
            'Hello$WorldFooBar'      => TRUE,
            "hello-world\tfoo"       => TRUE,
            '$Hello World_Foo_123'   => TRUE,
            "Hello\tWorld\t_Foo_123" => TRUE,
        ];

        foreach ($tests as $str => $expect) {
            $this->assert(Tell_Str::isAscii($str))->equals($expect);
        }

    });

    $this->try('toAscii()', function() {

        $before = "El pingüino Wenceslao hizo kilómetros bajo exhaustiva lluvia y frío, añoraba a su querido cachorro.";
        $after  = "El pinguino Wenceslao hizo kilometros bajo exhaustiva lluvia y frio, anoraba a su querido cachorro.";

        $this->assert(Tell_Str::toAscii($before))->equals($after);
        $this->assert(Tell_Str::toAscii("hëllo wφrld!"))->equals('hello world!');
        $this->assert(Tell_Str::toAscii("Hοω arε yoυ?"))->equals('How are you?');
        $this->assert(Tell_Str::toAscii("I'm not féélíng wéll."))->equals("I'm not feeling well.");

    });

    $this->try('toUtf8()', function() {

        $before = $this->hebrew_cp1255;
        $after  = $this->hebrew_utf8;

        try {
            $this->assert(Tell_Str::toUtf8($before, 'CP1255'))->equals($after);
        } catch (DomainException $e) {
            // iconv() not installed, and mb_* functions don't support CP1255
        }

        $before = $this->windows_1252;
        $after  = "äöüßéèâ";

        $this->assert(Tell_Str::toUtf8($before, 'WINDOWS-1252'))->equals($after);

    });

    $this->try('ltrim()', function() {

        $good = [
            ["aaabbbccc", "ac", 2, "abbbccc"],
            ["aaabbbccc", "ac", 3, "bbbccc"],
            ["aaabbbccc", "ab", 4, "bbccc"],
            ["aaabbbccc", "ab", 5, "bccc"],
            ["aaabbbccc", "ab", 6, "ccc"],
            ["aaabbbccc", "abc", 7, "cc"],
            ["aaabbbccc", "abc", 8, "c"],
            ["aaabbbccc", "abc", 9, ""],
            ["aaabbbccc", "a", 0, "bbbccc"],
            ["aaabbbccc", "ab", 0, "ccc"],
        ];

        foreach ($good as list($before, $chars, $limit, $after)) {
            $this->assert(Tell_Str::ltrim($before, $chars, $limit))->equals($after);
        }

        $bad = [
            ["äöüßéèâ", "ä", 0, "öüßéèâ"], // No multibyte support at this time (@todo?)
        ];

        foreach ($bad as list($before, $chars, $limit, $after)) {
            $this->assert(Tell_Str::ltrim($before, $chars, $limit))->notEquals($after);
        }

    });

    $this->try('rtrim()', function() {

        $good = [
            ["aaabbbccc", "ca", 2, "aaabbbc"],
            ["aaabbbccc", "ca", 3, "aaabbb"],
            ["aaabbbccc", "ca", 4, "aaabbb"],
            ["aaabbbccc", "ab", 5, "aaabbbccc"],
            ["aaabbbccc", "abc", 0, ""],
        ];

        foreach ($good as list($before, $chars, $limit, $after)) {
            $this->assert(Tell_Str::rtrim($before, $chars, $limit))->equals($after);
        }

        $bad = [
            ["äöüßéèâ", "ä", 0, "äöüßéè"], // No multibyte support at this time (@todo?)
        ];

        foreach ($bad as list($before, $chars, $limit, $after)) {
            $this->assert(Tell_Str::ltrim($before, $chars, $limit))->notEquals($after);
        }

    });

    $this->try('trim()', function() {

        $good = [
            ["aaabbbccc", "ac", 1, "aabbbcc"],
            ["aaabbbccc", "ac", 2, "abbbc"],
            ["aaabbbccc", "ac", 3, "bbb"],
            ["aaabbbccc", "ab", 4, "bbccc"],
            ["aaabbbccc", "ab", 5, "bccc"],
            ["aaabbbccc", "ab", 6, "ccc"],
            ["aaabbbccc", "abc", 7, ""],
            ["aaabbbccc", "abc", 8, ""],
            ["aaabbbccc", "abc", 9, ""],
            ["aaabbbccc", "a", 0, "bbbccc"],
            ["aaabbbccc", "ab", 0, "ccc"],
        ];

        foreach ($good as list($before, $chars, $limit, $after)) {
            $this->assert(Tell_Str::trim($before, $chars, $limit))->equals($after);
        }

        $bad = [
            ["äöüßéèâ", "ä", 0, "öüßéè"], // No multibyte support at this time (@todo?)
        ];

        foreach ($bad as list($before, $chars, $limit, $after)) {
            $this->assert(Tell_Str::trim($before, $chars, $limit))->notEquals($after);
        }

    });

    $this->try('sanitize()', function() {

        $byte4 = '𠜎 𠜱 𠝹 𠱓 𠱸 𠲖 𠳏 𠳕 𠴕 𠵼 𠵿 𠸎 𠸏 𠹷 𠺝 𠺢 𠻗 𠻹 𠻺 𠼭 𠼮 𠽌 𠾴 𠾼 𠿪 𡁜 𡁯 𡁵 '
               . '𡁶 𡁻 𡃁 𡃉 𡇙 𢃇 𢞵 𢫕 𢭃 𢯊 𢱑 𢱕 𢳂 𢴈 𢵌 𢵧 𢺳 𣲷 𤓓 𤶸 𤷪 𥄫 𦉘 𦟌 𦧲 𦧺 𧨾 𨅝 '
               . '𨈇 𨋢 𨳊 𨳍 𨳒 𩶘';

        $this->assert(strlen($byte4))->equals(309);

        $this->assert(strlen(Tell_Str::sanitize($byte4)))->equals(123);

        $arrByte4 = [
            '',
            '𡁵',
            '𠸎 𠸏',
            '𠜎 𠜱 𠝹',
            '𢺳 𣲷 𤓓 𤶸',
            '𢞵 𢫕 𢭃 𢯊 𢱑',
            '𨈇 𨋢 𨳊 𨳍 𨳒 𩶘',
        ];

        foreach ($arrByte4 as $chars => $str) {

            $this->assert(strlen($str))->equals(($chars * 4) + max($chars - 1, 0));

            $this->assert(strlen(Tell_Str::sanitize($str)))->equals($chars + max($chars - 1, 0));

        }

        $cleanArrByte4 = Tell_Str::sanitize($arrByte4);

        foreach ($cleanArrByte4 as $chars => $str) {

            $this->assert(strlen($str))->equals($chars + max($chars - 1, 0));

        }

    });

    $this->try('truncate()', function() {

        $fr = "
            Portez ce vieux whisky au juge blond qui fume sur son île intérieure, à
            côté de l'alcôve ovoïde, où les bûches se consument dans l'âtre, ce
            qui lui permet de penser à la cænogenèse de l'être dont il est question
            dans la cause ambiguë entendue à Moÿ, dans un capharnaüm qui,
            pense-t-il, diminue çà et là la qualité de son œuvre."
        ;

        $langs = $this->langs;

    });

})->vars([
    'hebrew_cp1255' => Tell_Asset::contents('docs/hebrew-cp1255'),
    'hebrew_utf8'   => Tell_Asset::contents('docs/hebrew-utf-8'),
    'windows_1252'  => Tell_Asset::contents('docs/windows-1252'),
    'langs'         => [
        'da' => /* Danish   */ "Quizdeltagerne spiste jordbær med fløde, mens cirkusklovnen Wolther spillede på xylofon.",
        'en' => /* English  */ "The quick brown fox jumps over the lazy dog",
        'fr' => /* French   */ "Le cœur déçu mais l'âme plutôt naïve, Louÿs rêva de crapaüter en canoë au delà des îles, près du mälström où brûlent les novæ.",
        'de' => /* German   */ "Falsches Üben von Xylophonmusik quält jeden größeren Zwerg",
        'el' => /* Greek    */ "Γαζέες καὶ μυρτιὲς δὲν θὰ βρῶ πιὰ στὸ χρυσαφὶ ξέφωτο",
        'iw' => /* Hebrew   */ "? דג סקרן שט בים מאוכזב ולפתע מצא לו חברה איך הקליטה",
        'jp' => /* Japanese */ "イロハニホヘト チリヌルヲ ワカヨタレソ ツネナラム ウヰノオクヤマ ケフコエテ アサキユメミシ ヱヒモセスン",
        'ru' => /* Russian  */ "В чащах юга жил бы цитрус? Да, но фальшивый экземпляр! Съешь же ещё этих мягких французских булок да выпей чаю",
        'es' => /* Spanish  */ "El pingüino Wenceslao hizo kilómetros bajo exhaustiva lluvia y frío, añoraba a su querido cachorro.",
    ],
]);
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

// -------------------------------------------------------------------------------------------------

class Inspect_Test_Auth_Basic extends Tell_Auth_Basic
{
    public function authenticate()
    {
        if ('bob@example.com' === $this->username() && 'testing' === $this->password()) {
            return [
                'first_name' => 'Bob',
                'last_name'  => 'Smith',
                'email'      => 'bob@example.com',
            ];
        }

        return FALSE;
    }

    public $test = [
        'onAuthenticate'        => [],
        'onAuthenticateSuccess' => [],
        'onAuthenticateFailure' => [],
        'onAuthenticateRevoke'  => [],
        'onPersist'             => [],
        'onPersistSuccess'      => [],
        'onPersistFailure'      => [],
        'onAuthorize'           => [],
        'onAuthorizeSuccess'    => [],
    ];

    public function onAuthenticate(...$args)
    {
        $this->test['onAuthenticate'] = $args;
    }

    public function onAuthenticateSuccess(...$args)
    {
        $this->test['onAuthenticateSuccess'] = $args;
    }

    public function onAuthenticateFailure(...$args)
    {
        $this->test['onAuthenticateFailure'] = $args;
    }

    public function onAuthenticateRevoke(...$args)
    {
        $this->test['onAuthenticateRevoke'] = $args;
    }

    public function onPersist(...$args)
    {
        $this->test['onPersist'] = $args;
    }

    public function onPersistSuccess(...$args)
    {
        $this->test['onPersistSuccess'] = $args;
    }

    public function onPersistFailure(...$args)
    {
        $this->test['onPersistFailure'] = $args;
    }

    public function onAuthorize(...$args)
    {
        $this->test['onAuthorize'] = $args;
    }

    public function onAuthorizeSuccess(...$args)
    {
        $this->test['onAuthorizeSuccess'] = $args;
    }
}

// -------------------------------------------------------------------------------------------------

$inspect('Tell_Auth_Basic >> Instantiate', function(Inspect_Test_Auth_Basic $auth) {

    $this->assert($auth->test['onAuthenticate'])->array()->empty();

    $this->assert($auth->test['onAuthenticateSuccess'])->array()->empty();

    $this->assert($auth->test['onAuthenticateFailure'])->array()->hasKey(0)->hasKey(1);

    $this->assert($auth->test['onAuthenticateFailure'][0])->false();

    $this->assert($auth->test['onAuthenticateFailure'][1])->instanceOf($auth);

    $this->assert($auth->test['onPersist'])->array()->empty();

    $this->assert($auth->test['onPersistSuccess'])->array()->empty();

    $this->assert($auth->test['onPersistFailure'])->array()->empty();

    $this->assert($auth->test['onAuthorize'])->array()->empty();

    $this->assert($auth->test['onAuthorizeSuccess'])->array()->empty();

});

// -------------------------------------------------------------------------------------------------

$inspect('Tell_Auth_Basic >> Valid Basic Credentials', function(Inspect_Test_Auth_Basic $auth) {

    $this->assert($auth->test['onAuthenticate'])->array()->hasKey(0)->hasKey(1);

    $this->assert($auth->test['onAuthenticate'][0])->equals([
        'first_name' => 'Bob',
        'last_name'  => 'Smith',
        'email'      => 'bob@example.com',
    ]);

    $this->assert($auth->test['onAuthenticate'][1])->instanceOf($auth);

    $this->assert($auth->test['onAuthenticateSuccess'])->equals($auth->test['onAuthenticate']);

    $this->assert($auth->test['onAuthenticateFailure'])->array()->empty();

    $this->assert($auth->test['onPersist'])->array()->empty();

    $this->assert($auth->test['onPersistSuccess'])->array()->empty();

    $this->assert($auth->test['onPersistFailure'])->array()->empty();

    $this->assert($auth->test['onAuthorize'])->array()->empty();

    $this->assert($auth->test['onAuthorizeSuccess'])->array()->empty();

})->request([
    'method' => 'GET',
    'server' => [
        'PHP_AUTH_USER' => 'bob@example.com',
        'PHP_AUTH_PW'   => 'testing',
    ],
]);

// -------------------------------------------------------------------------------------------------
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

$inspect('Tell_Arr', function() {

    $this->try('get()', function() {

        $a = $this->a;

        $this->assert(Tell_Arr::get($a, 'item_name'))
            ->equals($a['item_name']);

        $this->assert(Tell_Arr::get($a, 'attachments'))
            ->equals($a['attachments']);

        $this->assert(Tell_Arr::get($a, 'attachments.images'))
            ->equals($a['attachments']['images']);

        $this->assert(Tell_Arr::get($a, 'attachments.images.2'))
            ->equals($a['attachments']['images'][2]);

        $this->assert(Tell_Arr::get($a, 'attachments.images.4'))
            ->equals($a['attachments']['images'][4]);

        $this->assert(Tell_Arr::get($a, 'attachments.images.5'))
            ->equals($a['attachments']['images'][5]);

    })->then('delete()', function() {

        $a = $this->a;

        Tell_Arr::delete($a, 'item_name');

        Tell_Arr::delete($a, 'attachments.images.2');

        Tell_Arr::delete($a, 'attachments.images.4');

        Tell_Arr::delete($a, 'attachments.images.5');

        $this->assert(Tell_Arr::get($a, 'item_name'))
            ->null();

        $this->assert(Tell_Arr::get($a, 'attachments.images.2'))
            ->null();

        $this->assert(Tell_Arr::get($a, 'attachments.images.4'))
            ->null();

        $this->assert(Tell_Arr::get($a, 'attachments.images.5'))
            ->null();

    })->then('set()', function() {

        $a = $this->a;

        Tell_Arr::set($a, FALSE, 'foobar.jpg');

        Tell_Arr::set($a, 'attachments.images.4', 'dolphins.jpg');

        Tell_Arr::set($a, 'attachments.images.5', 'cats.jpg');

        Tell_Arr::set($a, 'attachments.images.2', 'cats.jpg');

        Tell_Arr::set($a, 'attachments.files', [
            0 => 'trex-700.pdf',
            1 => 'blade-600x.pdf',
        ]);

        $this->assert(Tell_Arr::get($a, FALSE))
            ->equals('foobar.jpg');

        $this->assert(Tell_Arr::get($a, 0))
            ->equals('foobar.jpg');

        $this->assert(Tell_Arr::get($a, 'attachments.images.4'))
            ->equals('dolphins.jpg');

        $this->assert(Tell_Arr::get($a, 'attachments.images.5'))
            ->equals('cats.jpg');

        $this->assert(Tell_Arr::get($a, 'attachments.images.2'))
            ->equals('cats.jpg');

        $this->assert(Tell_Arr::get($a, 'attachments.files.0'))
            ->equals('trex-700.pdf');

        $this->assert(Tell_Arr::get($a, 'attachments.files.1'))
            ->equals('blade-600x.pdf');

    });

    $this->try('isAssociative()', function() {

        $a = $this->a;
        $d = $this->d;

        $this->assert(Tell_Arr::isAssociative($a))->true();
        $this->assert(Tell_Arr::isAssociative($a['attachments']))->true();
        $this->assert(Tell_Arr::isAssociative($a['attachments']['images']))->false();
        $this->assert(Tell_Arr::isAssociative($d))->false();

    });

    $this->try('isIndexed()', function() {

        $a = $this->a;
        $d = $this->d;

        $this->assert(Tell_Arr::isIndexed($a))->false();
        $this->assert(Tell_Arr::isIndexed($a['attachments']))->false();
        $this->assert(Tell_Arr::isIndexed($a['attachments']['images']))->true();
        $this->assert(Tell_Arr::isIndexed($d))->true();

    });

    $this->try('isMultiDimensional()', function() {

        $this->assert(Tell_Arr::isMultiDimensional([
            'a' => 'foo',
            'b' => ['bar'],
            'c' => 'doe',
        ]))->true();

        $this->assert(Tell_Arr::isMultiDimensional([
            'a' => 'foo',
            'b' => 'bar',
            'c' => 'doe',
        ]))->false();

    });

    $this->try('coalesce()', function() {

        $a = [
            'attachments' => [
                'avatar' => NULL,
                'resume' => 'bob-cv.pdf',
                'images' => [
                    0 => 'whales.jpg',
                    1 => 'horses.jpg',
                    2 => 'flowers.jpg',
                    3 => 'dogs.jpg',
                    4 => 'dolphins.jpg',
                    5 => 'cats.jpg',
                ],
            ],
        ];

        $b = [
            'first_name'  => 'Bob',
            'last_name'   => 'Smith',
            'attachments' => [
                'avatar' => 'gravatar.png',
                'resume' => 'bob-updated-cv.pdf',
                'images' => [
                    0 => 'whales.jpg',
                    1 => 'sharks.jpg',
                    2 => 'flowers.jpg',
                    3 => 'dogs.jpg',
                    4 => 'dolphins.jpg',
                    5 => 'cats.jpg',
                ],
            ],
        ];

        $c = Tell_Arr::coalesce($a, $b);

        $this->assert($c['first_name']               ?? NULL)->equals('Bob');
        $this->assert($c['last_name']                ?? NULL)->equals('Smith');
        $this->assert($c['attachments']['avatar']    ?? NULL)->equals('gravatar.png');
        $this->assert($c['attachments']['resume']    ?? NULL)->equals('bob-updated-cv.pdf');
        $this->assert($c['attachments']['images'][0] ?? NULL)->equals('whales.jpg');
        $this->assert($c['attachments']['images'][1] ?? NULL)->equals('sharks.jpg');

    });

    $this->try('diff()', function() {

        $a = [
            'attachments' => [
                'avatar' => NULL,
                'resume' => 'bob-cv.pdf',
                'images' => [
                    0 => 'whales.jpg',
                    1 => 'horses.jpg',
                    2 => 'flowers.jpg',
                    3 => 'dogs.jpg',
                    4 => 'dolphins.jpg',
                    5 => 'cats.jpg',
                ],
            ],
        ];

        $b = [
            'first_name'  => 'Bob',
            'last_name'   => 'Smith',
            'attachments' => [
                'avatar' => 'gravatar.png',
                'resume' => 'bob-updated-cv.pdf',
                'images' => [
                    0 => 'whales.jpg',
                    1 => 'sharks.jpg',
                    2 => 'flowers.jpg',
                    3 => 'dogs.jpg',
                    4 => 'dolphins.jpg',
                    5 => 'cats.jpg',
                ],
            ],
        ];

        $c = Tell_Arr::diff($a, $b);

        $this->assert($c)->hasKey('attachments.avatar');
        $this->assert($c['attachments']['resume']    ?? NULL)->equals('bob-cv.pdf');
        $this->assert($c['attachments']['images'][1] ?? NULL)->equals('horses.jpg');

    });

    $this->try('flatten()', function() {

        $a = [
            'first_name'  => 'Bob',
            'last_name'   => 'Smith',
            'attachments' => [
                'avatar' => 'gravatar.png',
                'resume' => 'bob-updated-cv.pdf',
                'images' => [
                    0 => 'whales.jpg',
                    1 => 'sharks.jpg',
                    2 => 'flowers.jpg',
                    3 => 'dogs.jpg',
                    4 => 'dolphins.jpg',
                    5 => 'cats.jpg',
                ],
            ],
        ];

        $b = Tell_Arr::flatten($a, TRUE);

        $c = Tell_Arr::flatten($a, FALSE);

        $this->assert($b['first_name'] ?? NULL)->equals('Bob');
        $this->assert($b[5] ?? NULL)->equals('cats.jpg');

        $this->assert($c[0] ?? NULL)->equals('Bob');
        $this->assert($c[9] ?? NULL)->equals('cats.jpg');

    });

    $this->try('merge()', function() {

        $b = $this->b;

        $c = $this->c;

        $d = Tell_Arr::merge($b, $c);

        $this->assert(Tell_Arr::get($d, 'images.aa_samples'))
            ->equals(4);

        $this->assert(Tell_Arr::get($d, 'i18n.currency'))
            ->equals('CAD');

        $this->assert(Tell_Arr::get($d, 'i18n.translate.domain'))
            ->equals('api');

        $this->assert(count(Tell_Arr::get($d, 'i18n.locales')))
            ->equals(4);

        $this->assert(count(Tell_Arr::merge($b['i18n']['locales'], $c['i18n']['locales'])))
            ->equals(4);


        $a = [
            'attachments' => [
                'avatar' => NULL,
                'resume' => 'bob-cv.pdf',
                'images' => [
                    0 => 'whales.jpg',
                    1 => 'horses.jpg',
                    2 => 'flowers.jpg',
                    3 => 'dogs.jpg',
                    4 => 'dolphins.jpg',
                    5 => 'cats.jpg',
                ],
            ],
        ];

        $b = [
            'first_name'  => 'Bob',
            'last_name'   => 'Smith',
            'attachments' => [
                'avatar' => 'gravatar.png',
                'resume' => 'bob-updated-cv.pdf',
                'images' => [
                    0 => 'whales.jpg',
                    1 => 'sharks.jpg',
                    2 => 'flowers.jpg',
                    3 => 'dogs.jpg',
                    4 => 'dolphins.jpg',
                    5 => 'cats.jpg',
                ],
            ],
        ];

        $c = Tell_Arr::merge($a, $b);

        $this->assert($c['first_name']                ?? NULL)->equals('Bob');
        $this->assert($c['last_name']                 ?? NULL)->equals('Smith');
        $this->assert($c['attachments']['avatar']     ?? NULL)->equals('gravatar.png');
        $this->assert($c['attachments']['resume']     ?? NULL)->equals('bob-updated-cv.pdf');
        $this->assert($c['attachments']['images'][0]  ?? NULL)->equals('whales.jpg');
        $this->assert($c['attachments']['images'][1]  ?? NULL)->equals('horses.jpg');
        $this->assert($c['attachments']['images'][11] ?? NULL)->equals('cats.jpg');

    });

    $this->try('trim()', function() {

        $d = $this->d;

        $this->assert(strlen($d[3]))
            ->equals(26);

        $this->assert(strlen(Tell_Arr::trim($d)[3]))
            ->equals(23);

        $this->assert(strlen(explode(', ', 'hello, world, how, are,  you')[4]))
            ->equals(4);

        $this->assert(strlen(Tell_Arr::trim('hello, world, how, are,  you')[4]))
            ->equals(3);

    });

})->vars([
    'a' => [
        'item_name'   => 'Crucio Fero Quam',
        'item_status' => 'Active',
        'category_id' => 7,
        'attachments' => [
            'resume' => 'bob-cv.pdf',
            'images' => [
                0 => 'whales.jpg',
                1 => 'horses.jpg',
                2 => 'flowers.jpg',
                3 => 'dogs.jpg',
                4 => 'dolphins.jpg',
                5 => 'cats.jpg',
            ],
        ],
    ],
    'b' => [
        'images' => [
            'driver'     => 'gd',
            'aa_samples' => 2,
        ],
        'i18n' => [
            'timezone' => 'UTC',
            'currency' => 'USD',
            'locales'  => [
                'en_US',
                'en',
            ],
            'translate' => [
                'domain' => 'app',
                'paths'  => [],
            ],
        ],
    ],
    'c' => [
        'images' => [
            'driver'     => 'gd',
            'aa_samples' => 4,
        ],
        'i18n' => [
            'timezone' => 'UTC',
            'currency' => 'CAD',
            'locales'  => [
                'en_CA',
                'en',
            ],
            'translate' => [
                'domain' => 'api',
                'paths'  => [],
            ],
        ],
    ],
    'd' => [
        ' Vires Solium ',
        ' Super ',
        ' Parens ',
        ' Tutaminis Vorago Relevo  ',
        ' Infeci Munus Renuntio ',
        ' Mores Postpono Illa ',
        ' Defaeco Progressus Recognosco ',
    ],
]);
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

$inspect('Tell_Format', function() {

    $this->try('_()', function() {

        // @todo

    });

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

$inspect('Tell_Schema_Table_Pgsql', function() {

    $this->try('Creating example tables from SQL file', function(Tell_Db $db) {

        if ($db->pdo instanceof Closure) {
            $db->pdo->__invoke();
        }

        $this->assert($db->pdo->exec($this->ddl))->int()->equals(0);

    })->then('Validate the DDL without schema in cache', function(Tell_Schema $schema) {

        $this->assert($schema->incrementColumn('example'))->equals('id');

        $this->assert($schema->primaryKey('example'))->equals('id');

        $this->assert($schema->hasTable('example'))->true();

        $this->assert($schema->hasTable('example2'))->false();

        $this->assert($schema->rename('example', 'example2'))->true();

        $this->assert($schema->hasTable('example'))->false();

        $this->assert($schema->hasTable('example2'))->true();

        $this->assert($schema->hasColumn('example2', 'id'))->true();

        $this->assert($schema->hasColumn('example2', 'id2'))->false();

        $this->assert($schema->hasColumn('example', 'id'))->false();

        $this->assert($schema->hasIndex('example2', 'id'))->false();

        $this->assert($schema->hasIndex('example2', 'bill_first_name, bill_last_name'))->true();

        $this->assert($schema->incrementColumn('example2'))->equals('id');

        $this->assert($schema->primaryKey('example2'))->equals('id');

        $this->assert($schema->dropColumn('example2', 'order_number'))->true();

    })->then('Validate the DDL with schema in cache', function(Tell_Schema $schema) {

        $schema('example2'); // puts schema in cache

        $this->assert($schema->incrementColumn('example2'))->equals('id');

        $this->assert($schema->primaryKey('example2'))->equals('id');

        $this->assert($schema->hasTable('example2'))->true();

        $this->assert($schema->hasTable('example'))->false();

        $this->assert($schema->hasColumn('example2', 'id'))->true();

        $this->assert($schema->hasColumn('example2', 'id2'))->false();

        $this->assert($schema->hasColumn('example', 'id'))->false();

        $this->assert($schema->hasIndex('example2', 'id'))->false();

        $this->assert($schema->hasIndex('example2', 'bill_first_name, bill_last_name'))->true();

        $this->assert($schema->incrementColumn('example2'))->equals('id');

        $this->assert($schema->primaryKey('example2'))->equals('id');

        $this->assert($schema->dropColumn('example2', 'order_number'))->true();

        $this->assert($schema->drop('example2'))->true();

        $this->assert($schema->hasTable('example2'))->false();

    })->then('Create and alter a table', function(Tell_Schema $schema, Tell_Db $db) {

        $this->assert($schema->drop('items'))->true();

        $this->assert($schema->create('items', function(Tell_Schema_Column $column) {
            $column->serial('id')->primary();
            $column->int('category_id');
            $column->enum('listing_status', ['Active', 'Backordered', 'Discontinued', 'Unlisted']);
            $column->varchar('item_name');
            $column->currency('price');
            $column->dateTime('modified');
            $column->dateTime('created');
            $column->index('id, category_id');
            $column->index('listing_status');
        }))->true();

        $this->assert($schema->hasTable('items'))->true();

        $this->assert($schema->hasColumn('items', 'id'))->true();

        $this->assert($schema->hasColumn('items', 'category_id'))->true();

        $this->assert($schema->hasColumn('items', 'listing_status'))->true();

        $this->assert($schema->hasColumn('items', 'item_name'))->true();

        $this->assert($schema->hasColumn('items', 'price'))->true();

        $this->assert($schema->hasColumn('items', 'modified'))->true();

        $this->assert($schema->hasColumn('items', 'created'))->true();

        $this->assert($schema->hasColumn('items', 'something_else'))->false();

        $this->assert($schema->incrementColumn('items'))->equals('id');

        $this->assert($schema->primaryKey('items'))->equals('id');

        $this->assert($schema->hasIndex('items', 'id, category_id'))->true();

        $this->assert($schema->hasIndex('items', 'listing_status'))->true();

        $table = $schema->schema('items', TRUE);

        $this->assert($table)->instanceOf(Tell_Schema_Schema_Bridge::class);

        $columns = $table->columns;

        $this->assert($columns)->arr();

        $this->assert($columns['item_name'] ?? NULL)->instanceOf(Tell_Schema_Column_Bridge::class);

        $this->assert($columns['item_name']->length)->int()->equals(255);

        for ($i = 0; $i < 5; $i++) {
            $this->assert($db("INSERT INTO items")->data(Test_Schema::rowIpsumItem())->run())
                ->int()
                ->equals($i + 1);
        }

        $this->assert($schema->alter('items', function(Tell_Schema_Column $column) {
            $column->rename('id', 'item_id');
            $column->rename('listing_status', 'status');
            $column->enum('status', ['Active', 'Preordered', 'Backordered', 'Discontinued', 'Unlisted']);
            $column->varchar('item_name', 128);
            $column->dropIndex(['id', 'category_id']);
        }))->true();

        $this->assert($schema->incrementColumn('items'))->equals('item_id');

        $this->assert($schema->primaryKey('items'))->equals('item_id');

        $this->assert($schema->hasColumn('items', 'listing_status'))->false();

        $this->assert($schema->hasColumn('items', 'status'))->true();

        $this->assert($schema->hasIndex('items', 'id, category_id'))->false();

        $this->assert($schema->hasIndex('items', 'listing_status'))->false();

        $this->assert($schema->hasIndex('items', 'status'))->true();

        $columns = $schema->schema('items', TRUE)->columns;

        $this->assert($columns['item_name']->length)->int()->equals(128);

        $this->assert($db("INSERT INTO items")->data(Test_Schema::rowItemAfterAltered())->run())
            ->int()
            ->equals($i + 1);

        $this->assert($schema->drop('items'))->true();

        $this->assert($schema->hasTable('items'))->false();

    });

})->swap(function(Tell_Event $event) {
    return [
        'Tell_Db' => new Tell_Db([
            'driver'             => 'pgsql',
            'database'           => 'tell_php_test',
            'host'               => '127.0.0.1',
            'port'               => NULL,
            'socket'             => NULL,
            'username'           => 'postgres',
            'password'           => '',
            'elevate_exceptions' => TRUE,
            'log_failed'         => TRUE,
            'log_success'        => FALSE,
            'log_limit'          => 1000,
        ], $event),
    ];
})->vars([
    'ddl' => file_get_contents(__DIR__ . '/../assets/sql/table.pgsql.sql'),
])->onlyWhen(function() {
    return extension_loaded('pdo_pgsql') && env('TELL_PHP_DEV');
});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

$inspect('Tell_Html', function() {

    $this->try('_()', function() {

        // @todo

    });

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

$inspect('Tell_Locale', function() {

    $this->try('_()', function() {

        // @todo

    });

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

$inspect('Tell_Security', function() {

    $this->try('originRule()')->many($this->rules, function($test) {

        list($desc, $rule, $origin, $expected) = $test;

        return $expected === Tell_Security::originRule($origin, $rule);

    })->then('isOriginAllowed()')->many($this->origins, function($test) {

        list($origin, $expected) = $test;

        return $expected === Tell_Security::isOriginAllowed($origin, $this->whitelist);

    });

    $this->try('fileExistsIn()', function() {

        $unlockedDir = realpath(__DIR__ . '/../bridge');

        $lockedDir = realpath(CORE_PATH . '/classes/auth');

        $this->assert($unlockedDir)->directory();

        $this->assert($lockedDir)->directory();

        $goodUserInput = '/auth/Jwt.php';

        $badUserInput = '/../../../classes/auth/Jwt.php';

        $this->assert($unlockedDir . $goodUserInput)->file();

        $this->assert($unlockedDir . $badUserInput)->file();

        $this->assert(Tell_Security::fileExistsIn($goodUserInput, $unlockedDir))->true();

        $this->assert(Tell_Security::fileExistsIn($badUserInput, $unlockedDir))->false();

    });

    $this->try('random(0)', function() {

        $this->assert(function() {
            Tell_Security::random(0);
        })->exception(Error::class);

    })->then('random(#)')->many(50, function($i) {

        return $i ? strlen(Tell_Security::random($i)) === $i : TRUE; // skip 0

    })->then('csrfToken()', function() {

        $this->validate(Tell_Security::csrfToken())->lengthFixed(40)->assert();

    });

})->vars([
    // description, allowed, origin, expected
    'rules' => [
        ['Allow domain',                    'foo.com',                'foo.com',                 TRUE],
        ['Disallow subdomain on domain',    'foo.com',                'www.foo.com',             FALSE],
        ['Allow subdomain',                 'www.foo.com',            'www.foo.com',             TRUE],
        ['Disallow wrong subdomain',        'www.foo.com',            'ws.foo.com',              FALSE],
        ['Allow All',                       '*',                      'ws.foo.com',              TRUE],
        ['Allow subdomain wildcard',        '*.foo.com',              'ws.foo.com',              TRUE],
        ['Disallow lacks subdomain',        '*.foo.com',              'foo.com',                 FALSE],
        ['Allow double subdomain',          '*.foo.com',              'a.b.foo.com',             TRUE],
        ['Disallow incorrect position',     '*.foo.com',              'a.foo.com.evil.com',      FALSE],
        ['Allow subdomain in the middle',   'a.*.foo.com',            'a.bc.foo.com',            TRUE],
        ['Disallow wrong subdomain',        'a.*.foo.com',            'b.bc.foo.com',            FALSE],
        ['Disallow missing TLD',            'foo.com',                'fooXcom',                 FALSE],
        ['Allow port on domain',            'foo.com:2087',           'foo.com:2087',            TRUE],
        ['Disallow port on domain',         'foo.com',                'foo.com:2087',            FALSE],
        ['Disallow wrong port on domain',   'foo.com:2087',           'foo.com:2088',            FALSE],
        ['Disallow HTTP on HTTPS',          'https://foo.com',        'http://foo.com',          FALSE],
        ['Disallow HTTPS on HTTP',          'http://foo.com',         'https://foo.com',         FALSE],
        ['Disallow HTTPS w/ port',          'https://foo.com:2087',   'https://foo.com',         FALSE],
        ['Allow HTTPS w/ subdomain + port', 'https://*.foo.com:2087', 'https://ws.foo.com:2087', TRUE],
    ],
    // origin, expected
    'origins' => [
        ['http://foo.io', FALSE],
        ['https://foo.io', TRUE],
        ['https://www.foo.io', TRUE],
        ['https://users.foo.io', TRUE],
        ['https://admin.bar.dev', FALSE],
        ['https://admin.bar.dev:8080', TRUE],
        ['http://doe.com', TRUE],
        ['https://doe.com:2087', FALSE],
        ['http://yay.doe.com', TRUE],
        ['http://yay.me.doe.com', TRUE],
        ['https://yay.doe.com:2087', FALSE],
    ],
    'whitelist' => [
        'https://foo.io',
        'https://www.foo.io',
        'https://users.foo.io',
        'https://admin.bar.dev:8080',
        'doe.com',
        '*.doe.com',
    ],
]);
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

// ---------------------------------------------------------------------------------------------

$inspect('Tell_Validate_Card', function() {

    $this->try('luhn(loose)')->many($this->loose, function(bool $expect, $card) {
        return $expect === (new Tell_Validate_Card())->luhn($card, FALSE);
    });

    $this->try('luhn(strict)')->many($this->strict, function(bool $expect, $card) {
        return $expect === (new Tell_Validate_Card())->luhn($card, TRUE);
    });

})->vars([
    'loose' => [ // [value => expect, ...]
        '49927398716'      => TRUE,
        '49927398717'      => TRUE,
        '1234567812345678' => TRUE,
        '1234567812345670' => TRUE,
        '99927398716'      => TRUE,
        '40927398717'      => TRUE,
        '1234567812345778' => TRUE,
        '1234567812345679' => TRUE,
    ],
    'strict' => [ // [value => expect, ...]
        '49927398716'      => TRUE,
        '49927398717'      => FALSE,
        '1234567812345678' => FALSE,
        '1234567812345670' => TRUE,
        '99927398716'      => FALSE,
        '40927398717'      => FALSE,
        '1234567812345778' => FALSE,
        '1234567812345679' => FALSE,
    ],
]);

// ---------------------------------------------------------------------------------------------

$inspect('Tell_Validate_Color', function() {

    $this->try('hex()')->many($this->hex, function(bool $expect, $color) {
        return $expect === (new Tell_Validate_Color())->hex($color);
    });

    $this->try('rgb()')->many($this->rgb, function(bool $expect, $color) {
        return $expect === (new Tell_Validate_Color())->rgb($color);
    });

})->vars([
    'hex' => [ // [value => expect, ...]
        '32C0DF'          => TRUE,
        '32c0df'          => TRUE,
        '#32C0DF'         => TRUE,
        '#32c0df'         => TRUE,
        '#32c0dg'         => FALSE,
        '#2c0df'          => FALSE,
        "# 32c0df"        => FALSE, // ... ...
        "\x0D#32c0df"     => FALSE, // \n...
        "\x0A\x0D#32c0df" => FALSE, // \r\n...
        "#32c0df\x0D"     => FALSE, // ...\n
        "#32c0df\x0A\x0D" => FALSE, // ...\r\n
        "\x0032c0df"      => FALSE, // NULL...
        "#32c0df\x00"     => FALSE, // ...NULL
        "#32\x00c0df"     => FALSE, // ...NULL...
        "\x04#32c0df"     => FALSE, // EOL...
        "#32c0df\x04"     => FALSE, // ...EOL
        "32\x04c0df"      => FALSE, // ...EOL...
    ],
    'rgb' => [ // [value => expect, ...]
        '(0,0,0)'            => TRUE,
        '(255,255,255)'      => TRUE,
        '50, 192, 223'       => TRUE,
        '50,192,223'         => TRUE,
        '(50, 192, 223)'     => TRUE,
        '(50,192,223)'       => TRUE,
        '(50,192223)'        => FALSE,
        '50,192,223)'        => FALSE,
        '(50,192,223'        => FALSE,
        '(5a,192,223)'       => FALSE,
        '256,255,200'        => FALSE,
        '128,128,-1'         => FALSE,
        '128,299,127'        => FALSE,
        "50, 192,223"        => TRUE, // ... ...
        "\x0D50,192,223"     => FALSE, // \n...
        "\x0A\x0D50,192,223" => FALSE, // \r\n...
        "50,192,223\x0D"     => FALSE, // ...\n
        "50,192,223\x0A\x0D" => FALSE, // ...\r\n
        "\x0050,192,223"     => FALSE, // NULL...
        "50,192,223\x00"     => FALSE, // ...NULL
        "50,192\x00,223"     => FALSE, // ...NULL...
        "\x0450,192,223"     => FALSE, // EOL...
        "50,192,223\x04"     => FALSE, // ...EOL
        "50,192\x04,223"     => FALSE, // ...EOL...
    ],
]);

// ---------------------------------------------------------------------------------------------

$inspect('Tell_Validate_Country', function() {

    $this->try('validate()')->many($this->countries, function(bool $expect, $code) {
        return $expect === (new Tell_Validate_Country())->validate($code);
    });

})->vars([
    'countries' => [ // [value => expect, ...]
        0            => FALSE,
        1            => FALSE,
        'US'         => TRUE,
        'CA'         => TRUE,
        'GB'         => TRUE,
        'us'         => FALSE,
        'Us'         => FALSE,
        'uS'         => FALSE,
        'ca'         => FALSE,
        'Ca'         => FALSE,
        'cA'         => FALSE,
        'gb'         => FALSE,
        'Gb'         => FALSE,
        'gB'         => FALSE,
        'USA'        => FALSE,
        'UK'         => FALSE, // UK should be GB (Great Britain), UK not valid
        "U S"        => FALSE, // ... ...
        "\x0DUS"     => FALSE, // \n...
        "\x0A\x0DUS" => FALSE, // \r\n...
        "GB\x0D"     => FALSE, // ...\n
        "GB\x0A\x0D" => FALSE, // ...\r\n
        "\x00CA"     => FALSE, // NULL...
        "CA\x00"     => FALSE, // ...NULL
        "C\x00A"     => FALSE, // ...NULL...
        "\x04FR"     => FALSE, // EOL...
        "FR\x04"     => FALSE, // ...EOL
        "F\x04R"     => FALSE, // ...EOL...
    ],
]);

// ---------------------------------------------------------------------------------------------

$inspect('Tell_Validate_Date', function() {

    $this->try('clock()')->many($this->clocks['*'], function(bool $expect, $clock) {
        return $expect === (new Tell_Validate_Date())->clock($clock);
    })->then('am()')->many($this->clocks['am'], function(bool $expect, $clock) {
        return $expect === (new Tell_Validate_Date())->am($clock);
    })->then('pm()')->many($this->clocks['pm'], function(bool $expect, $clock) {
        return $expect === (new Tell_Validate_Date())->pm($clock);
    })->then('date()')->many($this->dates, function(bool $expect, $date) {
        return $expect === (new Tell_Validate_Date())->date($date);
    })->then('dateTime()')->many($this->dateTimes, function(bool $expect, $stamp) {
        return $expect === (new Tell_Validate_Date())->dateTime($stamp);
    });

    // @todo currently returns erroneous results
    // $this->try('expiration()')->many($this->expirations, function(bool $expect, $expires) {
    //     return $expect === (new Tell_Validate_Date())->expiration($expires)
    //         && $expect === (new Tell_Validate_Date())->expiration($expires, $this->now_a)
    //         && $expect === (new Tell_Validate_Date())->expiration($expires, $this->now_b);
    // });

    $this->try('day()')->many($this->days, function(bool $expect, $day) {
        return $expect === (new Tell_Validate_Date())->day($day);
    });

    $this->try('month()')->many($this->months, function(bool $expect, $day) {
        return $expect === (new Tell_Validate_Date())->month($day);
    });

    $this->try('weekday()')->many($this->weekdays, function(bool $expect, $day) {
        return $expect === (new Tell_Validate_Date())->weekday($day);
    });

    $this->try('format(...)')->many($this->formats, function(array $tests, $fmt) {
        return $this->try("format($fmt)")->many($tests, function(bool $expect, $date) use ($fmt) {
            return $expect === (new Tell_Validate_Date())->format($date, $fmt);
        });
    });

    $this->try('before()')->many($this->before, function(array $range) {
        list($date, $max, $expect) = $range;
        return $expect === (new Tell_Validate_Date())->before($date, $max);
    });

    $this->try('after()')->many($this->after, function(array $range) {
        list($date, $min, $expect) = $range;
        return $expect === (new Tell_Validate_Date())->after($date, $min);
    });

})->vars([
    'now_a'  => '2021-05-31 04:18:20',
    'now_b'  => '2021-06-01 19:59:21',
    'clocks' => [ // [value => expect, ...]
        '*' => [
            0           => FALSE,
            '1'         => FALSE, // Too vague (easily mistaken for boolean instead of 1 AM)
            '6'         => FALSE,
            '01'        => FALSE,
            '1am'       => TRUE,
            '1a.m.'     => TRUE,
            '1 am'      => TRUE,
            '1 a.m.'    => TRUE,
            '1AM'       => TRUE,
            '1 AM'      => TRUE,
            '1 A.M.'    => TRUE,
            '1:32'      => TRUE,
            '1:32am'    => TRUE,
            '1:32a.m.'  => TRUE,
            '1:32 am'   => TRUE,
            '1:32 a.m.' => TRUE,
            '1:33AM'    => TRUE,
            '1:33A.M.'  => TRUE,
            '1:33 AM'   => TRUE,
            '1:33 A.M.' => TRUE,
            '133'       => FALSE,
            '133AM'     => FALSE,
            '15:35'     => TRUE,
            '15:35am'   => FALSE,
            '15:35AM'   => FALSE,
            '15:35 AM'  => FALSE,
            '23:59'     => TRUE,
            '23: 59'    => FALSE,
            '23 : 59'   => FALSE,
            '23 :59'    => FALSE,
            '23:60'     => FALSE,
            '1:13pm'    => TRUE,
            '1:13PM'    => TRUE,
            '1:13p.m.'  => TRUE,
            '7:47 P.M.' => TRUE,
        ],
        'am' => [ // [value => expect, ...]
            0           => FALSE,
            '1'         => FALSE, // Too vague (easily mistaken for boolean instead of 1 AM)
            '1am'       => TRUE,
            '1a.m.'     => TRUE,
            '1 am'      => TRUE,
            '1 a.m.'    => TRUE,
            '1AM'       => TRUE,
            '1 AM'      => TRUE,
            '1 A.M.'    => TRUE,
            '1:32'      => TRUE,
            '1:32am'    => TRUE,
            '1:32a.m.'  => TRUE,
            '1:32 am'   => TRUE,
            '1:32 a.m.' => TRUE,
            '1:33AM'    => TRUE,
            '1:33A.M.'  => TRUE,
            '1:33 AM'   => TRUE,
            '1:33 A.M.' => TRUE,
            '12:59'     => TRUE,
            '15:35'     => FALSE,
            '23:59'     => FALSE,
        ],
        'pm' => [ // [value => expect, ...]
            0           => FALSE,
            '1'         => FALSE, // Too vague (easily mistaken for boolean instead of 1 AM)
            '1pm'       => TRUE,
            '1p.m.'     => TRUE,
            '1 pm'      => TRUE,
            '1 p.m.'    => TRUE,
            '1PM'       => TRUE,
            '1 PM'      => TRUE,
            '1 P.M.'    => TRUE,
            '1:32'      => FALSE,
            '1:32pm'    => TRUE,
            '1:32p.m.'  => TRUE,
            '1:32 pm'   => TRUE,
            '1:32 p.m.' => TRUE,
            '1:33PM'    => TRUE,
            '1:33P.M.'  => TRUE,
            '1:33 PM'   => TRUE,
            '1:33 P.M.' => TRUE,
            '7:47AM'    => FALSE,
            '7:47PM'    => TRUE,
            '7:47 AM'   => FALSE,
            '7:47 PM'   => TRUE,
            '12:59'     => FALSE,
            '15:35'     => TRUE,
            '23:59'     => TRUE,
        ],
    ],
    'dates' => [ // [value => expect, ...]
        0                    => FALSE,
        '1'                  => FALSE,
        '1967-07-14'         => TRUE,
        '07/14/1967'         => TRUE,
        '1967/14/07'         => FALSE,
        '1967-14-07'         => FALSE,
        '1967 14 07'         => FALSE,
        '1969-12-31'         => TRUE,
        '12/31/1969'         => TRUE,
        '1969/31/12'         => FALSE,
        '1969-31-12'         => FALSE,
        '1969 12 31'         => FALSE,
        '1970-01-01'         => TRUE,
        '01/01/1970'         => TRUE,
        '1970/01/01'         => TRUE,
        '1970 01 01'         => FALSE,
        '2000-01-01'         => TRUE,
        '01/01/2000'         => TRUE,
        '2000/01/01'         => TRUE,
        '2000 01 01'         => FALSE,
        '2016-02-01'         => TRUE,
        '02/01/2016'         => TRUE,
        '2016 02 01'         => FALSE,
        '16-03-02'           => FALSE,
        '2016-03-02'         => TRUE,
        '03/02/2016'         => TRUE,
        '2016/02/03'         => TRUE,
        '2016/03/02'         => TRUE,
        '2016-02-03'         => TRUE,
        '2016 02 03'         => FALSE,
        '2016 03 02'         => FALSE,
        '2006-13-07'         => FALSE,
        '2006-07-13'         => TRUE,
        '07/13/2006'         => TRUE,
        '13/07/2006'         => FALSE,
        '2006 13 07'         => FALSE, // ... ...
        '2006 07 13'         => FALSE, // ... ...
        "\x0D12/15/2021"     => FALSE, // \n...
        "\x0A\x0D2021-07-09" => FALSE, // \r\n...
        "03/24/2019\x0D"     => FALSE, // ...\n
        "1994/09/17\x0A\x0D" => FALSE, // ...\r\n
        "\x002021-05-11"     => FALSE, // NULL...
        "07/24/2019\x00"     => FALSE, // ...NULL
        "07/24\x00/2019"     => FALSE, // ...NULL...
        "\x042020-01-20"     => FALSE, // EOL...
        "2020-02-24\x04"     => FALSE, // ...EOL
        "2021-03-\x0415"     => FALSE, // ...EOL...
    ],
    'dateTimes' => [ // [value => expect, ...]
        '2005-08-15T15:52:01+00:00'        => TRUE,
        'Monday, 15-Aug-2005 15:52:01 UTC' => TRUE,
        '2005-08-15T15:52:01+0000'         => TRUE,
        'Mon, 15 Aug 05 15:52:01 +0000'    => TRUE,
        'Monday, 15-Aug-05 15:52:01 UTC'   => TRUE,
        'Mon, 15 Aug 2005 15:52:01 +0000'  => TRUE,
        'Sat, 30 Apr 2016 17:52:13 GMT'    => TRUE,
        '2005-08-15T15:52:01.000+00:00'    => TRUE,
        '1970-01-01 06:46:13'              => TRUE,
        '1970-01-91 06:46:13'              => FALSE,
        '1970-01-91 Z:46:13'               => FALSE,
        '01/01/1970 07:19:23'              => FALSE,
        '01/01-1970 07:19:23'              => FALSE,
        '1970/01/01 12:10:05'              => FALSE,
        '1969-12-31 1:32 AM'               => FALSE,
        '1969-12-31 1:32AM'                => FALSE,
        '1969-12/31 1:32AM'                => FALSE,
        '12/31/1969 1:32'                  => FALSE,
        '12/31/1969 01:32'                 => FALSE,
        '12/31/1969 01'                    => FALSE,
        '2016-02-03 3:14 PM'               => FALSE,
        '2016-02-03 3:14PM'                => FALSE,
        '2016-02-03 3:14'                  => FALSE,
        '2016-02-03 03:14'                 => FALSE,
        '2016-02-03 314'                   => FALSE,
        '2021-05-16 13:27:14'              => TRUE,
        '2021-05-16 00:00:00'              => TRUE,
        '2021-05-16 00:00'                 => FALSE,
        '2021-05-16 08:00'                 => FALSE,
        '2021-05-16 0'                     => FALSE,
        '2021-07-19 12:31:14'              => TRUE,
        "\x0D1970-01-01 06:46:13"          => FALSE, // \n...
        "\x0A\x0D1970-01-01 06:46:13"      => FALSE, // \r\n...
        "1970-01-01 06:46:13\x0D"          => FALSE, // ...\n
        "1970-01-01 06:46:13\x0A\x0D"      => FALSE, // ...\r\n
        "\x002021-05-16 13:28:14"          => FALSE, // NULL...
        "2021-05-16 13:27:14\x00"          => FALSE, // ...NULL
        "2021-05-16 \x0013:38:14"          => FALSE, // ...NULL...
        "\x042021-07-19 12:31:14"          => FALSE, // EOL...
        "2021-07-19 12:31:14\x04"          => FALSE, // ...EOL
        "2021-07-19 \x0412:31:14"          => FALSE, // ...EOL...
    ],
    'formats' => [ // [format => [value => expect, ...], ...]
        'Y-m-d' => [
            '2012-12-31' => TRUE,
            '1967-07-14' => TRUE,
            '2000-01-01' => TRUE,
            '2016-02-03' => TRUE,
            '2012/12/31' => FALSE,
            '2016/02/03' => FALSE,
            '2000-01-00' => FALSE,
            '2012-12-32' => FALSE,
            '2012-31-12' => FALSE,
            '212-12-31'  => FALSE,
            '2012-12'    => FALSE,
            '12-07-13'   => FALSE,
            // ... @todo ...
        ],
        'y-m-d' => [
            '2012-12-31' => TRUE,
            '1967-07-14' => TRUE,
            '2000-01-01' => TRUE,
            '2016-02-03' => TRUE,
            '2012/12/31' => FALSE,
            '2016/02/03' => FALSE,
            '2000-01-00' => FALSE,
            '2012-12-32' => FALSE,
            '2012-31-12' => FALSE,
            '212-12-31'  => FALSE,
            '2012-12'    => FALSE,
            '12-07-13'   => FALSE,
            // ... @todo ...
        ],
        'y/m/d' => [
            '2017/01/10' => TRUE,
            '2017/07/12' => TRUE,
            '2017/12/31' => TRUE,
            '2001/12/32' => FALSE,
            '17/12/31'   => FALSE,
            '2017/13/10' => FALSE,
            '2017/1/10'  => FALSE,
            '2017/10/1'  => FALSE,
            // ... @todo ...
        ],
        'm/d/Y' => [
            '12/31/2016' => TRUE,
            '01/01/2016' => TRUE,
            '07/14/2016' => TRUE,
            '13/12/2016' => FALSE,
            '07/32/2016' => FALSE,
            '1/12/2016'  => FALSE,
            '12/31/16'   => FALSE,
            '07/7/2016'  => FALSE,
            // ... @todo ...
        ],
        'm/d/y' => [
            '12/31/2016' => TRUE,
            '01/01/2016' => TRUE,
            '07/14/2016' => TRUE,
            '13/12/2016' => FALSE,
            '07/32/2016' => FALSE,
            '1/12/2016'  => FALSE,
            '12/31/16'   => FALSE,
            '07/7/2016'  => FALSE,
            // ... @todo ...
        ],
        'm/d' => [
            '07/13' => TRUE,
            '12/31' => TRUE,
            '01/01' => TRUE,
            '13/10' => FALSE,
            '7/13'  => FALSE,
            '07/3'  => FALSE,
            '7/3'   => FALSE,
            // ... @todo ...
        ],
        'm/y' => [
            '12/2012' => TRUE,
            '07/2016' => TRUE,
            '01/2016' => TRUE,
            '01/2000' => TRUE,
            '12/2000' => TRUE,
            '01/12'   => FALSE,
            '00/12'   => FALSE,
            '00/2016' => FALSE,
            '1/2016'  => FALSE,
            '12/12'   => FALSE,
            '12/3'    => FALSE,
            // ... @todo ...
        ],
        'my' => [
            '102013' => TRUE,
            '122013' => TRUE,
            '012013' => TRUE,
            '122000' => TRUE,
            '002013' => FALSE,
            '12013'  => FALSE,
            '1012'   => FALSE,
            '12201'  => FALSE,
            // ... @todo ...
        ],
    ],
    'expirations' => [ // [value => expect, ...]
        date('my', strtotime('-' . date('t') . ' days')) => TRUE, // MMYY
        date('my', strtotime('-32 days'))  => FALSE, // MMYY
        date('my', strtotime('-28 days'))  => TRUE,  // MMYY
        date('my', strtotime('-1 year'))   => FALSE, // MMYY
        date('my', strtotime('+1 year'))   => TRUE,  // MMYY
        date('m/y', strtotime('-1 year'))  => FALSE, // MM/YY
        date('m/y', strtotime('+1 year'))  => TRUE,  // MM/YY
        date('mY', strtotime('-1 year'))   => FALSE, // MMYYYY
        date('mY', strtotime('+1 year'))   => TRUE,  // MMYYYY
        date('m/Y', strtotime('-1 year'))  => FALSE, // MM/YYYY
        date('m/Y', strtotime('+1 year'))  => TRUE,  // MM/YYYY
        date('my', strtotime('-1 month'))  => TRUE,  // MMYY
        date('my', strtotime('+1 month'))  => TRUE,  // MMYY
        date('m/y', strtotime('-1 month')) => TRUE,  // MM/YY
        date('m/y', strtotime('+1 month')) => TRUE,  // MM/YY
        date('mY', strtotime('-1 month'))  => TRUE,  // MMYYYY
        date('mY', strtotime('+1 month'))  => TRUE,  // MMYYYY
        date('m/Y', strtotime('-1 month')) => TRUE,  // MM/YYYY
        date('m/Y', strtotime('+1 month')) => TRUE,  // MM/YYYY
    ],
    'before' => [ // [[value 1, value 2, expect], ...]
        ['2016-03-02', '2016-02-01', FALSE],
        ['2016-03-02', '2016-03-02', TRUE],
        ['2016-02-01', '2016-03-02', TRUE],
        ['07/13/2006', '2006-07-12', FALSE],
        ['07/13/2006', '2006/07/14', TRUE],
        ['07/13/2006', '2006-07-14', TRUE],
        ['2006-07-13', '2006/07/14', TRUE],
        ['2006-07-13', '2005-07-14', FALSE],
    ],
    'after' => [ // [[value 1, value 2, expect], ...]
        ['2016-03-02', '2016-02-01', TRUE],
        ['2016-03-02', '2016-03-02', TRUE],
        ['2016-02-01', '2016-03-02', FALSE],
        ['07/13/2006', '2006-07-12', TRUE],
        ['07/13/2006', '2006/07/14', FALSE],
        ['07/13/2006', '2006-07-14', FALSE],
        ['2006-07-13', '2006/07/14', FALSE],
        ['2006-07-13', '2005-07-14', TRUE],
    ],
    'days' => [ // [value => expect, ...]
        0      => FALSE,
        '1'    => TRUE,
        '1st'  => TRUE,
        '14th' => TRUE,
        28     => TRUE,
        '31'   => TRUE,
        '31st' => TRUE,
        '32'   => FALSE,
        '32nd' => FALSE,
    ],
    'months' => [ // [value => expect, ...]
        'Jan'      => TRUE,
        'JaN'      => TRUE,
        'Feb'      => TRUE,
        'March'    => TRUE,
        'April'    => TRUE,
        'Sep'      => TRUE,
        'SEPT'     => TRUE,
        'NOVEMBER' => TRUE,
        'Decem'    => FALSE,
        'December' => TRUE,
    ],
    'weekdays' => [ // [value => expect, ...]
        'mo'       => TRUE,
        'Mo'       => TRUE,
        'MO'       => TRUE,
        'mon'      => TRUE,
        'Mon'      => TRUE,
        'MON'      => TRUE,
        'monday'   => TRUE,
        'Monday'   => TRUE,
        'MONDAY'   => TRUE,
        'th'       => TRUE,
        'TH'       => TRUE,
        'Thu'      => TRUE,
        'Thur'     => TRUE,
        'THURS'    => TRUE,
        'Thursday' => TRUE,
    ],
]);

// ---------------------------------------------------------------------------------------------

$inspect('Tell_Validate_File', function() {

    $this->assert($this->path_gif)->imageGif();
    $this->assert($this->path_jpeg)->imageJpg();
    $this->assert($this->path_jpg)->imageJpeg();
    $this->assert($this->path_png)->imagePng();

    $this->try('extensions()', function() {

        $this->assert((new Tell_Validate_File())->extensions($this->path_gif, ['bmp', 'exe']))->false();
        $this->assert((new Tell_Validate_File())->extensions($this->path_gif, ['gif', 'bmp', 'exe']))->true();
        $this->assert((new Tell_Validate_File())->extensions($this->path_jpg, ['gif', 'jpg', 'png']))->true();
        $this->assert((new Tell_Validate_File())->extensions($this->path_jpeg, 'gif', 'jpg', 'png'))->true();

    });

    $this->try('mimes()', function() {

        $images = ['image/gif', 'image/jpeg', 'image/png'];

        $this->assert((new Tell_Validate_File())->mimes($this->path_gif, $images))->true();
        $this->assert((new Tell_Validate_File())->mimes($this->path_jpg, $images))->true();
        $this->assert((new Tell_Validate_File())->mimes($this->path_jpeg, $images))->true();
        $this->assert((new Tell_Validate_File())->mimes($this->path_txt, $images))->false();
        $this->assert((new Tell_Validate_File())->mimes($this->path_pdf, $images))->false();

    });

    $this->try('image()', function() {

        if ( ! extension_loaded('gd')) return;

        $this->assert((new Tell_Validate_File())->image($this->path_gif))->true();
        $this->assert((new Tell_Validate_File())->image($this->path_jpg))->true();
        $this->assert((new Tell_Validate_File())->image($this->path_jpeg))->true();
        $this->assert((new Tell_Validate_File())->image($this->path_png))->true();
        $this->assert((new Tell_Validate_File())->image($this->path_txt))->false();
        $this->assert((new Tell_Validate_File())->image($this->path_pdf))->false();

    });

    $this->try('heightBetween()', function() {

        if ( ! extension_loaded('gd')) return;

        $this->assert((new Tell_Validate_File())
            ->heightBetween($this->path_gif, 1000, 1800))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->heightBetween($this->path_gif, 600, 1600))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->heightBetween($this->path_jpg, 3000, 3800))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->heightBetween($this->path_jpg, 3700, 4700))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->heightBetween($this->path_jpeg, 2000, 2300))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->heightBetween($this->path_jpeg, 2300, 2500))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->heightBetween($this->path_png, 800, 1100))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->heightBetween($this->path_png, 800, 900))
            ->false();

    });

    $this->try('heightMin()', function() {

        if ( ! extension_loaded('gd')) return;

        $this->assert((new Tell_Validate_File())
            ->heightMin($this->path_gif, 1500))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->heightMin($this->path_gif, 1800))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->heightMin($this->path_jpg, 3500))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->heightMin($this->path_jpg, 3700))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->heightMin($this->path_jpeg, 2200))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->heightMin($this->path_jpeg, 2300))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->heightMin($this->path_png, 800))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->heightMin($this->path_png, 1000))
            ->false();

    });

    $this->try('heightMax()', function() {

        if ( ! extension_loaded('gd')) return;

        $this->assert((new Tell_Validate_File())
            ->heightMax($this->path_gif, 1500))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->heightMax($this->path_gif, 1800))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->heightMax($this->path_jpg, 3500))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->heightMax($this->path_jpg, 3700))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->heightMax($this->path_jpeg, 2200))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->heightMax($this->path_jpeg, 2300))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->heightMax($this->path_png, 800))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->heightMax($this->path_png, 1000))
            ->true();

    });

    $this->try('widthBetween()', function() {

        if ( ! extension_loaded('gd')) return;

        $this->assert((new Tell_Validate_File())
            ->widthBetween($this->path_gif, 1000, 1200))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->widthBetween($this->path_gif, 600, 1000))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->widthBetween($this->path_jpg, 3000, 6000))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->widthBetween($this->path_jpg, 1000, 3000))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->widthBetween($this->path_jpeg, 1500, 3000))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->widthBetween($this->path_jpeg, 800, 1500))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->widthBetween($this->path_png, 800, 2000))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->widthBetween($this->path_png, 500, 1200))
            ->false();

    });

    $this->try('widthMin()', function() {

        if ( ! extension_loaded('gd')) return;

        $this->assert((new Tell_Validate_File())
            ->widthMin($this->path_gif, 1000))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->widthMin($this->path_gif, 1200))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->widthMin($this->path_jpg, 5000))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->widthMin($this->path_jpg, 6000))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->widthMin($this->path_jpeg, 1500))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->widthMin($this->path_jpeg, 2000))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->widthMin($this->path_png, 1300))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->widthMin($this->path_png, 1600))
            ->false();

    });

    $this->try('widthMax()', function() {

        if ( ! extension_loaded('gd')) return;

        $this->assert((new Tell_Validate_File())
            ->widthMax($this->path_gif, 1000))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->widthMax($this->path_gif, 1200))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->widthMax($this->path_jpg, 5000))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->widthMax($this->path_jpg, 6000))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->widthMax($this->path_jpeg, 1500))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->widthMax($this->path_jpeg, 2000))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->widthMax($this->path_png, 1300))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->widthMax($this->path_png, 1600))
            ->true();

    });

    $this->try('sizeBetween()', function() {

        $this->assert((new Tell_Validate_File())
            ->sizeBetween($this->path_gif, '100kb', '0.5mb'))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->sizeBetween($this->path_gif, '300kb', '2mb'))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->sizeBetween($this->path_jpg, '100kb', '1.5mb'))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->sizeBetween($this->path_jpg, '100kb', '1mb'))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->sizeBetween($this->path_jpeg, '100kb', '300kib'))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->sizeBetween($this->path_jpeg, '50kib', '230kib'))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->sizeBetween($this->path_png, '1.0mb', '2mb'))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->sizeBetween($this->path_png, '10 bytes', '1000kb'))
            ->false();

    });

    $this->try('sizeMin()', function() {

        $this->assert((new Tell_Validate_File())
            ->sizeMin($this->path_jpeg, '225kib'))
            ->true();

        $this->assert((new Tell_Validate_File())
            ->sizeMin($this->path_jpeg, '0.25mb'))
            ->false();

    });

    $this->try('sizeMax()', function() {

        $this->assert((new Tell_Validate_File())
            ->sizeMax($this->path_jpeg, '225kib'))
            ->false();

        $this->assert((new Tell_Validate_File())
            ->sizeMax($this->path_jpeg, '0.25mb'))
            ->true();

    });

})->vars([
    'path_png'  => Tell_Asset::path('images/blossoms.png'),
    'path_gif'  => Tell_Asset::path('images/dolphins.gif'),
    'path_jpeg' => Tell_Asset::path('images/shark.jpeg'),
    'path_jpg'  => Tell_Asset::path('images/whale.jpg'),
    'path_txt'  => Tell_Asset::path('docs/ipsum.txt'),
    'path_pdf'  => Tell_Asset::path('docs/skookum.pdf'),
]);

// ---------------------------------------------------------------------------------------------

$inspect('Tell_Validate_List', function() {

    $this->try('blacklist()', function() {

        $list = ['pooh', 'doo', 69, 666];

        $this->assert((new Tell_Validate_List())->blacklist('red', ...$list))->true();

        $this->assert((new Tell_Validate_List())->blacklist('red', $list))->true();

        $this->assert((new Tell_Validate_List())->blacklist('pooh', ...$list))->false();

        $this->assert((new Tell_Validate_List())->blacklist('pooh', $list))->false();

        $this->assert((new Tell_Validate_List())->blacklist(69, ...$list))->false();

    });

    $this->try('whitelist()', function() {

        $list = ['Visa', 'MasterCard', 'Discover', 'American Express', 'Diners Club', 'JCB'];

        $this->assert((new Tell_Validate_List())->whitelist('Amex', ...$list))->false();

        $this->assert((new Tell_Validate_List())->whitelist('American Express', $list))->true();

        $this->assert((new Tell_Validate_List())->whitelist('visa', $list))->false();

        $this->assert((new Tell_Validate_List())->whitelist('Visa', $list))->true();

        $this->assert((new Tell_Validate_List())->whitelist('Diners Club', ...$list))->true();

        $this->assert((new Tell_Validate_List())->whitelist('Diners Club', $list))->true();

    });

});

$inspect('Tell_Validate_Number', function() {

    $this->try('digit()', function() {

        $this->assert((new Tell_Validate_Number())->digit('a'))->false();

        $this->assert((new Tell_Validate_Number())->digit(1))->true();

        $this->assert((new Tell_Validate_Number())->digit('1'))->true();

        $this->assert((new Tell_Validate_Number())->digit(0))->true();

        $this->assert((new Tell_Validate_Number())->digit('0'))->true();

        $this->assert((new Tell_Validate_Number())->digit(-1))->true();

        $this->assert((new Tell_Validate_Number())->digit('-1'))->true();

        $this->assert((new Tell_Validate_Number())->digit('-1'))->true();

        $this->assert((new Tell_Validate_Number())->digit(-1000))->true();

        $this->assert((new Tell_Validate_Number())->digit('-1000'))->true();

        $this->assert((new Tell_Validate_Number())->digit(1259))->true();

        $this->assert((new Tell_Validate_Number())->digit('1259'))->true();

        $this->assert((new Tell_Validate_Number())->digit('125,900'))->false();

        $this->assert((new Tell_Validate_Number())->digit(TRUE))->false();

        $this->assert((new Tell_Validate_Number())->digit(FALSE))->false();

        $this->assert((new Tell_Validate_Number())->digit(NULL))->false();

        $this->assert((new Tell_Validate_Number())->digit([]))->false();

        $this->assert((new Tell_Validate_Number())->digit(new stdClass))->false();

    });

    $this->try('negative()', function() {

        $this->assert((new Tell_Validate_Number())->negative('0'))->false();

        $this->assert((new Tell_Validate_Number())->negative('0', TRUE))->true();

        $this->assert((new Tell_Validate_Number())->negative('-0'))->false();

        $this->assert((new Tell_Validate_Number())->negative('-1'))->true();

        $this->assert((new Tell_Validate_Number())->negative(-1))->true();

        $this->assert((new Tell_Validate_Number())->negative(-0.01))->true();

        $this->assert((new Tell_Validate_Number())->negative('-0.01'))->true();

        $this->assert((new Tell_Validate_Number())->negative(-0.5))->true();

        $this->assert((new Tell_Validate_Number())->negative(0.01))->false();

        $this->assert((new Tell_Validate_Number())->negative('0.01'))->false();

        $this->assert((new Tell_Validate_Number())->negative(1.00))->false();

        $this->assert((new Tell_Validate_Number())->negative('1.00'))->false();

        $this->assert((new Tell_Validate_Number())->negative('a'))->false();

        $this->assert((new Tell_Validate_Number())->negative('+'))->false();

        $this->assert((new Tell_Validate_Number())->negative('-'))->false();

        $this->assert((new Tell_Validate_Number())->negative('.'))->false();

        $this->assert((new Tell_Validate_Number())->negative(' '))->false();

        $this->assert((new Tell_Validate_Number())->negative(TRUE))->false();

        $this->assert((new Tell_Validate_Number())->negative(FALSE))->false();

        $this->assert((new Tell_Validate_Number())->negative(NULL))->false();

        $this->assert((new Tell_Validate_Number())->negative([]))->false();

        $this->assert((new Tell_Validate_Number())->negative(new stdClass))->false();

    });

    $this->try('numeric()', function() {

        $this->assert((new Tell_Validate_Number())->numeric(1337e0))->true(); // 1337

        $this->assert((new Tell_Validate_Number())->numeric('1337e0'))->false();

        $this->assert((new Tell_Validate_Number())->numeric(' 42'))->false();

        $this->assert((new Tell_Validate_Number())->numeric('42 '))->false();

        $this->assert((new Tell_Validate_Number())->numeric('42'))->true();

        $this->assert((new Tell_Validate_Number())->numeric('0.01'))->true();

        $this->assert((new Tell_Validate_Number())->numeric(0.01))->true();

        $this->assert((new Tell_Validate_Number())->numeric(-0.01))->true();

    });

    $this->try('numericBetween()', function() {

        $this->assert((new Tell_Validate_Number())->numericBetween(-1, -2, -1))->true();

        $this->assert((new Tell_Validate_Number())->numericBetween(1, 1, 2))->true();

        $this->assert((new Tell_Validate_Number())->numericBetween('59.50', '50', 100))->true();

        $this->assert((new Tell_Validate_Number())->numericBetween(10.001, 5, 10))->false();

    });

    $this->try('numericMin()', function() {

        $this->assert((new Tell_Validate_Number())->numericMin('-1.01', -1))->false();

        $this->assert((new Tell_Validate_Number())->numericMin('-0.002', -0.001))->false();

        $this->assert((new Tell_Validate_Number())->numericMin(-0.002, -0.002))->true();

        $this->assert((new Tell_Validate_Number())->numericMin(0, 0))->true();

        $this->assert((new Tell_Validate_Number())->numericMin('0', '0'))->true();

        $this->assert((new Tell_Validate_Number())->numericMin(0, '0'))->true();

        $this->assert((new Tell_Validate_Number())->numericMin(1.001, 1))->true();

        $this->assert((new Tell_Validate_Number())->numericMin(10, '5'))->true();

    });

    $this->try('numericMax()', function() {

        $this->assert((new Tell_Validate_Number())->numericMax('0', -1))->false();

        $this->assert((new Tell_Validate_Number())->numericMax('0', -0.01))->false();

        $this->assert((new Tell_Validate_Number())->numericMax(0, '-0.01'))->false();

        $this->assert((new Tell_Validate_Number())->numericMax(-0.01, '-0.01'))->true();

        $this->assert((new Tell_Validate_Number())->numericMax(-5, -10))->false();

        $this->assert((new Tell_Validate_Number())->numericMax(5, 10))->true();

        $this->assert((new Tell_Validate_Number())->numericMax(5, 3))->false();

        $this->assert((new Tell_Validate_Number())->numericMax('0.01', 0.02))->true();

        $this->assert((new Tell_Validate_Number())->numericMax(1000, 1001))->true();

    });

    $this->try('positive()', function() {

        $this->assert((new Tell_Validate_Number())->positive('0'))->false();

        $this->assert((new Tell_Validate_Number())->positive('0', TRUE))->true();

        $this->assert((new Tell_Validate_Number())->positive('-0'))->false();

        $this->assert((new Tell_Validate_Number())->positive('-1'))->false();

        $this->assert((new Tell_Validate_Number())->positive(-1))->false();

        $this->assert((new Tell_Validate_Number())->positive(-0.01))->false();

        $this->assert((new Tell_Validate_Number())->positive('-0.01'))->false();

        $this->assert((new Tell_Validate_Number())->positive(-0.5))->false();

        $this->assert((new Tell_Validate_Number())->positive(0.01))->true();

        $this->assert((new Tell_Validate_Number())->positive('0.01'))->true();

        $this->assert((new Tell_Validate_Number())->positive(1.00))->true();

        $this->assert((new Tell_Validate_Number())->positive('1.00'))->true();

        $this->assert((new Tell_Validate_Number())->positive('a'))->false();

        $this->assert((new Tell_Validate_Number())->positive('+'))->false();

        $this->assert((new Tell_Validate_Number())->positive('-'))->false();

        $this->assert((new Tell_Validate_Number())->positive('.'))->false();

        $this->assert((new Tell_Validate_Number())->positive(' '))->false();

        $this->assert((new Tell_Validate_Number())->positive(TRUE))->false();

        $this->assert((new Tell_Validate_Number())->positive(FALSE))->false();

        $this->assert((new Tell_Validate_Number())->positive(NULL))->false();

        $this->assert((new Tell_Validate_Number())->positive([]))->false();

        $this->assert((new Tell_Validate_Number())->positive(new stdClass))->false();

    });

});

// ---------------------------------------------------------------------------------------------

$inspect('Tell_Validate_Phone', function() {

    $this->try('format()')->many($this->phones, function(bool $expect, $number) {
        return $expect === (new Tell_Validate_Phone())->format($number);
    });

})->vars([
    'phones' => [ // [value => expect, ...]
        '123-456-7890'                    => TRUE,
        '(0555) 1234 5678'                => TRUE,
        '+86 155 5555 5432'               => TRUE,
        '+86 13888889999 extension 12345' => TRUE,
        '1-123-456-7890 *5214'            => TRUE,
        '(123) 456-7890 x 1234'           => TRUE,
        '+1.123-456-7890/1234'            => TRUE,
        '555-555-5555'                    => TRUE,
        '+123.555123456'                  => TRUE,
        '+1.123.555.1234'                 => TRUE,
        '555-555-5555 or 123-456-7890'    => FALSE,
        'mobile 555-555-5555'             => FALSE,
        'call Bob at 555-555-5555'        => FALSE,
        '1.1234x123'                      => FALSE,
        "I am a nice shark"               => FALSE,
    ],
]);

// ---------------------------------------------------------------------------------------------

$inspect('Tell_Validate_Str', function() {

    $this->try('alpha()')->many($this->alpha, function(array $rules, $str) {
        list($spaces, $expect) = $rules;
        return $expect === (new Tell_Validate_Str())->alpha($str, $spaces);
    });

    $this->try('alphaDash()')->many($this->alpha_dash, function(array $rules, $str) {
        list($spaces, $expect) = $rules;
        return $expect === (new Tell_Validate_Str())->alphaDash($str, $spaces);
    });

    $this->try('alphaNumeric()')->many($this->alpha_numeric, function(array $rules, $str) {
        list($spaces, $expect) = $rules;
        return $expect === (new Tell_Validate_Str())->alphaNumeric($str, $spaces);
    });

    $this->try('ascii()')->many($this->ascii, function(bool $expect, $str) {
        return $expect === (new Tell_Validate_Str())->ascii($str);
    });

    $this->try('length*(ascii)')->many($this->ascii_tests, function(array $rules) {
        list($method, $args, $expect) = $rules;
        return $expect === (new Tell_Validate_Str())->$method($this->ascii_value, ...$args);
    });

    $this->try('length*(utf8)')->many($this->utf8_tests, function(array $rules) {
        list($method, $args, $expect) = $rules;
        return $expect === (new Tell_Validate_Str())->$method($this->utf8_value, ...$args);
    });

    $this->try('noWhiteSpace()')->many($this->no_whitespace, function(bool $expect, $str) {
        return $expect === (new Tell_Validate_Str())->noWhiteSpace($str);
    });

    $this->try('words()')->many($this->words, function(bool $expect, $str) {
        return $expect === (new Tell_Validate_Str())->words($str);
    });

})->vars([
    'ascii_value' => 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
    'ascii_tests' => [ // [[method, [args], expect], ...]
        ['lengthFixed',   [56], TRUE],
        ['lengthFixed',   [55], FALSE],
        ['lengthBetween', [50, 60], TRUE],
        ['lengthBetween', [56, 57], TRUE],
        ['lengthBetween', [57, 60], FALSE],
        ['lengthMin',     [60], FALSE],
        ['lengthMin',     [56], TRUE],
        ['lengthMin',     [50], TRUE],
        ['lengthMin',     [57], FALSE],
        ['lengthMax',     [60], TRUE],
        ['lengthMax',     [56], TRUE],
        ['lengthMax',     [50], FALSE],
        ['lengthMax',     [57], TRUE],
    ],
    'utf8_value' => 'žščř',
    'utf8_tests' => [ // [[method, [args], expect], ...]
        ['lengthFixed',   [4], TRUE],
        ['lengthFixed',   [8], FALSE],
        ['lengthFixed',   [3], FALSE],
        ['lengthFixed',   [5], FALSE],
        ['lengthBetween', [2, 6], TRUE],
        ['lengthBetween', [4, 5], TRUE],
        ['lengthBetween', [4, 8], TRUE],
        ['lengthBetween', [5, 8], FALSE],
        ['lengthBetween', [2, 4], TRUE],
        ['lengthBetween', [2, 3], FALSE],
        ['lengthMin',     [5], FALSE],
        ['lengthMin',     [4], TRUE],
        ['lengthMin',     [2], TRUE],
        ['lengthMin',     [3], TRUE],
        ['lengthMin',     [6], FALSE],
        ['lengthMin',     [8], FALSE],
        ['lengthMax',     [5], TRUE],
        ['lengthMax',     [4], TRUE],
        ['lengthMax',     [2], FALSE],
        ['lengthMax',     [3], FALSE],
        ['lengthMax',     [6], TRUE],
        ['lengthMax',     [8], TRUE],
    ],
    'alpha' => [ // [value => [allow/disallow-spaces, expect], ...]
        'hello-world-123'      => [FALSE, FALSE],
        'hello-world'          => [FALSE, FALSE],
        'helloworld'           => [FALSE, TRUE],
        '123'                  => [FALSE, FALSE],
        'HelloWorldFooBar'     => [FALSE, TRUE],
        'Hello WorldFooBar'    => [FALSE, FALSE],
        'hello-world foo'      => [FALSE, FALSE],
        'Hello World_Foo_123'  => [FALSE, FALSE],
        'ahello-world-123'     => [TRUE, FALSE],
        'ahello-world'         => [TRUE, FALSE],
        'ahelloworld'          => [TRUE, TRUE],
        'a123'                 => [TRUE, FALSE],
        'aHelloWorldFooBar'    => [TRUE, TRUE],
        'aHello WorldFooBar'   => [TRUE, TRUE],
        'ahello-world foo'     => [TRUE, FALSE],
        'aHello World_Foo_123' => [TRUE, FALSE],
    ],
    'alpha_dash' => [ // [value => [allow/disallow spaces, expect], ...]
        'hello-world-123'      => [FALSE, TRUE],
        'hello-world'          => [FALSE, TRUE],
        'helloworld'           => [FALSE, TRUE],
        '123'                  => [FALSE, TRUE],
        'HelloWorldFooBar'     => [FALSE, TRUE],
        'Hello WorldFooBar'    => [FALSE, FALSE],
        'hello-world foo'      => [FALSE, FALSE],
        'Hello World_Foo_123'  => [FALSE, FALSE],
        'ahello-world-123'     => [TRUE, TRUE],
        'ahello-world'         => [TRUE, TRUE],
        'ahelloworld'          => [TRUE, TRUE],
        'a123'                 => [TRUE, TRUE],
        'aHelloWorldFooBar'    => [TRUE, TRUE],
        'aHello WorldFooBar'   => [TRUE, TRUE],
        'ahello-world foo'     => [TRUE, TRUE],
        'aHello World_Foo_123' => [TRUE, FALSE],
    ],
    'alpha_numeric' => [ // [value => [allow/disallow spaces, expect], ...]
        'hello-world-123'      => [FALSE, FALSE],
        'hello-world'          => [FALSE, FALSE],
        'helloworld'           => [FALSE, TRUE],
        '123'                  => [FALSE, TRUE],
        'HelloWorldFooBar'     => [FALSE, TRUE],
        'Hello WorldFooBar'    => [FALSE, FALSE],
        'hello-world foo'      => [FALSE, FALSE],
        'Hello World_Foo_123'  => [FALSE, FALSE],
        'ahello-world-123'     => [TRUE, FALSE],
        'ahello-world'         => [TRUE, FALSE],
        'ahelloworld'          => [TRUE, TRUE],
        'a123'                 => [TRUE, TRUE],
        'aHelloWorldFooBar'    => [TRUE, TRUE],
        'aHello WorldFooBar'   => [TRUE, TRUE],
        'ahello-world foo'     => [TRUE, FALSE],
        'aHello World_Foo_123' => [TRUE, FALSE],
    ],
    'ascii' => [ // [value => expect, ...]
        'žščř'                   => FALSE,
        'hëllo-world-123'        => FALSE,
        'hšello-world'           => FALSE,
        "hëllowo'rld"            => FALSE,
        '(123ľ'                  => FALSE,
        'HelloW.orld FooBar'     => TRUE,
        'Hello$WorldFooBar'      => TRUE,
        "hello-world\tfoo"       => TRUE,
        '$Hello World_Foo_123'   => TRUE,
        "Hello\tWorld\t_Foo_123" => TRUE,
    ],
    'no_whitespace' => [ // [value => expect, ...]
        'hello-world-123'       => TRUE,
        'hello-world'           => TRUE,
        'helloworld'            => TRUE,
        '123'                   => TRUE,
        'HelloWorldFooBar'      => TRUE,
        'Hello WorldFooBar'     => FALSE,
        'hello-world foo'       => FALSE,
        'Hello World_Foo_123'   => FALSE,
        'hello-world-123['      => TRUE,
        'hello-;world'          => TRUE,
        "hellowo'rld"           => TRUE,
        '(123'                  => TRUE,
        'HelloW.orld FooBar'    => FALSE,
        'Hello=WorldFooBar'     => TRUE,
        "hello-world\tfoo"      => FALSE,
        "Hello World_Foo_123\n" => FALSE,
        "Hello World\n_Foo_123" => FALSE,
    ],
    'words' => [ // [value => expect, ...]
        'hello-world-123'       => TRUE,
        'hello-world'           => TRUE,
        'helloworld'            => TRUE,
        '123'                   => TRUE,
        'HelloWorldFooBar'      => TRUE,
        'Hello WorldFooBar'     => TRUE,
        'hello-world foo'       => TRUE,
        'Hello World_Foo_123'   => TRUE,
        'hello-world-123['      => FALSE,
        'hello-;world'          => FALSE,
        "hellowo'rld"           => FALSE,
        '(123'                  => FALSE,
        'HelloW.orldFooBar'     => FALSE,
        'Hello=WorldFooBar'     => FALSE,
        "hello-world\tfoo"      => FALSE,
        "Hello World_Foo_123\n" => FALSE,
        "Hello World\n_Foo_123" => FALSE,
    ],
]);

// ---------------------------------------------------------------------------------------------

$inspect('Tell_Validate_Vin', function() {

    // @todo this validator is not working for some reason (it used to?)
    // $this->try('validate()')->many($this->vins, function($expect, $vin) {
    //     return $expect === (new Tell_Validate_Vin())->validate($vin);
    // });

})->vars([
    'vins' => [ // [value => expect, ...]
        '3MZBM1V73FM144590'  => TRUE,
        'JYA2JU007HA058976'  => TRUE,
        '3VWTL81K39M389565'  => TRUE,
        '1FAHP34N65W153563'  => TRUE,
        '4T1GK12E3SU265462'  => TRUE,
        'JHBFA1536P1S10176'  => TRUE,
        '1N6BA0CA5BN390204'  => TRUE,
        'JHMCP2F36BC075467'  => TRUE,
        '1GDM7T1J7RJ574782'  => TRUE,
        '1GCEK14T05E276540'  => TRUE,
        '1GDM7T1J7ZJ574782'  => TRUE,
        'MZBM1V73FM144590'   => FALSE,
        'JYA2JU007HA0589767' => FALSE,
        '3VXTL81K39M389565'  => FALSE,
        '1FCHP34N65W153563'  => FALSE,
        '4T4GK12E3SU265462'  => FALSE,
        'JHDFA1536P1S10176'  => FALSE,
        '1N6BB0CA5BN390204'  => FALSE,
        'JHMCP5F36BC075467'  => FALSE,
        '1GCEK14T09E276540'  => FALSE,
    ],
]);

// ---------------------------------------------------------------------------------------------

$inspect('Tell_Validate_Web', function() {

    $this->try('domain()')->many($this->domains, function(bool $expect, $domain) {
        return $expect === (new Tell_Validate_Web())->domain($domain);
    });

    $this->try('email()')->many($this->emails, function(bool $expect, $email) {
        return $expect === (new Tell_Validate_Web())->email($email);
    });

    $this->try('ip()')->many($this->ip_addresses, function(bool $expect, $ip) {
        return $expect === (new Tell_Validate_Web())->ip($ip, TRUE, TRUE);
    });

    $this->try('ipv4Range()')->many($this->ipv4_ranges, function($value) {

        list($input, $cidr, $expect) = $value;

        return $expect === (new Tell_Validate_Web())->ipv4Range($input, $cidr);

    });

    $this->try('url()')->many($this->urls, function(bool $expect, $url) {
        return $expect === (new Tell_Validate_Web())->url($url);
    });

})->vars([
    'domains' => [ // [value => expect, ...]
        // 'foo.-google.com'   => FALSE, // needs fixed
        // 'foo.google.-co.uk' => FALSE, // needs fixed
        'google.com'         => TRUE,
        'go--gle.com'        => TRUE,
        'google-.com'        => FALSE,
        'foo.google.com'     => TRUE,
        'foo.google.com/bar' => FALSE,
        'http://google.com'  => FALSE,
        'https://google.com' => FALSE,
        'google.co.uk'       => TRUE,
        'foo.google.co.uk'   => TRUE,
        "\x0Dgoogle.com"     => FALSE, // \n...
        "goo\x0Dgle.com"     => FALSE, // ...\n...
        "google.com\x0D"     => FALSE, // ...\n
        "\x00google.com"     => FALSE, // NULL...
        "goo\x00gle.com"     => FALSE, // ...NULL...
        "google.com\x00"     => FALSE, // ...NULL
        "\x04google.com"     => FALSE, // EOL...
        "goo\x04gle.com"     => FALSE, // ...EOL...
        "google.com\x04"     => FALSE, // ...EOL
    ],
    'emails' => [ // [value => expect, ...]
        0                            => FALSE,
        1                            => FALSE,
        'bartle.doo@foo.com'         => TRUE,
        'bobbie@templeton.co.uk'     => TRUE,
        '@foo.com'                   => FALSE,
        "bartle.doo@foo..com"        => FALSE,
        'bartle.doo@.com'            => FALSE,
        "bartle.doo@foo"             => FALSE,
        'omer asaf.lang@quies.co.il' => FALSE, // ... ...
        "bartle.doo @foo.com"        => FALSE, // ... ...
        "bartle.doo@ foo.com"        => FALSE, // ... ...
        "\x0Dbartle.doo@foo.com"     => FALSE, // \n...
        "\x0A\x0Dbartle.doo@foo.com" => FALSE, // \r\n...
        "bartle.doo@foo.com\x0D"     => FALSE, // ...\n
        "bartle.doo@foo.com\x0A\x0D" => FALSE, // ...\r\n
        "\x00bartle.doo@foo.com"     => FALSE, // NULL...
        "bartle.doo@foo.com\x00"     => FALSE, // ...NULL
        "bartle.doo\x00doo@foo.com"  => FALSE, // ...NULL...
        "\x04bartledoo@foo.com"      => FALSE, // EOL...
        "bartle.doo@foo.com\x04"     => FALSE, // ...EOL
        "bartle.doo\x04@foo.com"     => FALSE, // ...EOL...
    ],
    'ip_addresses' => [ // [value => expect, ...]
        '123.169.23.16'                           => TRUE,
        '44.213.46.223'                           => TRUE,
        '178.208.44.81'                           => TRUE,
        '16.195.168.77'                           => TRUE,
        '206.0.47.20'                             => TRUE,
        '218.117.101.145'                         => TRUE,
        '152.154.100.99'                          => TRUE,
        '30.184.54.80'                            => TRUE,
        '51.228.94.171'                           => TRUE,
        '254.159.129.88'                          => TRUE, // reserved range
        '16.195.168.77 '                          => FALSE,
        '16.195.16877'                            => FALSE,
        "16.195.168\x00.77"                       => FALSE, // NULL
        'a0af:5637:1cbb:e389:a0d9:e226:b2cd:59df' => TRUE,
        'fbc9:1ec2:529b:2790:4fd6:ded8:a5ad:389c' => TRUE,
        '825b:d227:2397:50f2:b4d3:5a21:a183:2034' => TRUE,
        'dcb7:60a7:66c0:b5a6:418b:5b23:e080:3c2c' => TRUE,
        '5e2f:ff97:c3cc:9c6c:9091:debc:4e8b:481f' => TRUE,
        '939d:d5a6:2761:8091:814b:91e2:925b:0261' => TRUE,
        'c712:ba91:6d99:73ba:aec8:e477:0cb2:eb33' => TRUE,
        'da3f:75c7:d6d6:7c88:473a:c00b:61e7:bcbf' => TRUE,
        '1839:30cc:8126:23a0:eb32:aae0:ab73:b7f6' => TRUE,
        'bfb0:15f7:0557:51cd:1e0a:db6a:0281:b630' => TRUE,
        'bfb0:15f7:0557:51cd:1e0a:db6a:0281b630'  => FALSE,
    ],
    // Values from Symfony HTTP Foundation Tests/IPUtilsTest.php
    // (c) Fabien Potencier <fabien@symfony.com>
    'ipv4_ranges' => [ // [[value, cidr notation(s), expect], ...]
        ['192.168.1.1', '192.168.1.1', TRUE],
        ['192.168.1.1', '192.168.1.1/1', TRUE],
        ['192.168.1.1', '192.168.1.0/24', TRUE],
        ['192.168.1.1', '1.2.3.4/1', FALSE],
        ['192.168.1.1', '192.168.1.0/a', FALSE], // Invalid subnet
        ['192.168.1.1', '192.168.1.1/33', FALSE], // Invalid subnet
        ['192.168.1.1', ['1.2.3.4/1', '192.168.1.0/24'], TRUE],
        ['192.168.1.1', ['192.168.1.0/24', '1.2.3.4/1'], TRUE],
        ['192.168.1.1', ['1.2.3.4/1', '4.3.2.1/1'], FALSE],
        ['1.2.3.4', '0.0.0.0/0', TRUE],
        ['1.2.3.4', '192.168.1.0/0', TRUE],
        ['1.2.3.4', '256.256.256/0', FALSE], // Invalid CIDR notation
        ['an_invalid_ip', '192.168.1.0/24', FALSE],
        ['', '1.2.3.4/1', FALSE],
    ],
    'urls' => [ // [value => expect, ...]
        'https://example.com/believe/authority.html'                         => TRUE,
        'example.com/believe/authority.html'                                 => FALSE,
        'http://example.org/'                                                => TRUE,
        'http://apparel.example.com/'                                        => TRUE,
        'https://www.example.com/border?advice=action'                       => TRUE,
        'http://www.example.com/bomb.html'                                   => TRUE,
        'http://army.example.com/birthday/addition'                          => TRUE,
        'https://www.example.com'                                            => TRUE,
        'https://example.com/boundary/anger.php'                             => TRUE,
        'https://www.example.com/afternoon.php'                              => TRUE,
        'https://belief.example.com/aunt/brass.php?balance=boy&board=action' => TRUE,
        'http://pt.wikipedia.org/wiki/Guimarães'                             => TRUE,
        "https://www.example.com/hello\x00world"                             => FALSE, // NULL
        "https://www.example.com/hello\x0Dworld"                             => FALSE, // \n
    ],
]);

// ---------------------------------------------------------------------------------------------
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

$inspect('Tell_Ipsum', function() {

    $this->try('Tell_Ipsum_Card', function() {

        $this->try('number()')->many(50, function(Tell_Ipsum_Card $card) {
            return $this->assert($card->number())->string();
        })->then('factory()', function(Tell_Ipsum_Card $card) {
            return $this->assert($card->factory())->instanceOf(Tell_Bridge_Card::class);
        });

    });

    $this->try('Tell_Ipsum_Contact', function() {

        // @todo

    });

    $this->try('Tell_Ipsum_Record', function() {

        // @todo

    });

    $this->try('Tell_Ipsum_Text', function() {

        // @todo

    });

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

$inspect('Tell_Session', function() {

    $this->try('_()', function() {

        // @todo

    });

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

$inspect('Tell_Str_Obj', function() {

    $tests = [
        'el' => /* Greek   */ "Γαζέες καὶ μυρτιὲς δὲν θὰ βρῶ πιὰ στὸ χρυσαφὶ ξέφωτο",
        'en' => /* English */ "The quick brown fox jumps over the lazy dog",
        'de' => /* German  */ "Falsches Üben von Xylophonmusik quält jeden größeren Zwerg",
        'es' => /* Spanish */ "El pingüino Wenceslao hizo kilómetros bajo exhaustiva lluvia y frío, añoraba a su querido cachorro.",
        'fr' => /* French  */ "Le cœur déçu mais l'âme plutôt naïve, Louÿs rêva de crapaüter en canoë au delà des îles, près du mälström où brûlent les novæ.",
    ];

    $greek   = new Tell_Str_Obj($tests['el']);
    $english = new Tell_Str_Obj($tests['en']);
    $german  = new Tell_Str_Obj($tests['de']);
    $spanish = new Tell_Str_Obj($tests['es']);
    $french  = new Tell_Str_Obj($tests['fr']);

    $this->assert($greek->__toString())->equals($tests['el']);
    $this->assert($greek[0])->equals('Γ');
    $this->assert($greek[26])->equals('β');
    $this->assert($greek[51])->equals('ο');

    $this->assert($english->__toString())->equals($tests['en']);
    $this->assert($english[0])->equals('T');
    $this->assert($english[27])->equals('v');
    $this->assert($english[42])->equals('g');

    $this->assert($german->__toString())->equals($tests['de']);
    $this->assert($german[0])->equals('F');
    $this->assert($german[46])->equals('ö');
    $this->assert($german[57])->equals('g');

    $this->assert($spanish->__toString())->equals($tests['es']);
    $this->assert($spanish[0])->equals('E');
    $this->assert($spanish[7])->equals('ü');
    $this->assert($spanish[98])->equals('.');

    $this->assert($french->__toString())->equals($tests['fr']);
    $this->assert($french[0])->equals('L');
    $this->assert($french[41])->equals('ÿ');
    $this->assert($french[125])->equals('.');

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

$inspect('Tell_Env', function() {

    $this->try('get()', function() {

        // @todo

    });

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

$inspect('Tell_Date_Epoch', function() {

    $props = [
        'years',
        'months',
        'weeks',
        'days',
        'hours',
        'minutes',
        'seconds',
    ];

    $epochs = [
        new Tell_Date_Epoch(
            new DateTime('2000-01-01 00:00:00'),
            new DateTime('2001-01-01 00:00:00')
        ),
        new Tell_Date_Epoch(
            new DateTime('2001-01-01 00:00:00'),
            new DateTime('2002-01-01 00:00:00')
        ),
        new Tell_Date_Epoch(
            new DateTime('2020-01-10 00:00:00'),
            new DateTime('2020-01-20 00:00:00')
        ),
        new Tell_Date_Epoch(
            new DateTime('2020-01-10 09:15:00'),
            new DateTime('2020-01-11 10:15:00')
        ),
        new Tell_Date_Epoch(
            new DateTime('2020-03-10 09:15:25'),
            new DateTime('2021-03-09 09:15:25')
        ),
        new Tell_Date_Epoch(
            new DateTime('1962-03-10 12:30:00'),
            new DateTime('1964-03-11 12:30:00')
        ),
        new Tell_Date_Epoch(
            new DateTime('1970-01-01 00:01:00'),
            new DateTime('1971-01-01 00:01:30')
        ),
    ];

    foreach ($epochs as $epoch) {
        foreach ($props as $prop) {
            $lt = '+' . ($epoch->$prop)     . ' ' . $prop;
            $gt = '+' . ($epoch->$prop + 1) . ' ' . $prop;

            $this->assert((clone $epoch->from)->modify($lt))->lessThanOrEqualTo($epoch->to);
            $this->assert((clone $epoch->from)->modify($gt))->greaterThanOrEqualTo($epoch->to);
        }
    }

});

$inspect('Tell_Date_Unit', function() {

    $base = [
        'isIncremental' => FALSE,
        'isDateTime'    => FALSE,
        'isDate'        => FALSE,
        'isTime'        => FALSE,
        'isYear'        => FALSE,
        'isMonth'       => FALSE,
        'isWeek'        => FALSE,
        'isWeekday'     => FALSE,
        'isDay'         => FALSE,
        'isHour'        => FALSE,
        'isMinute'      => FALSE,
        'isSecond'      => FALSE,
    ];

    $tests = [
        // ------------------------------
        [
            'value'      => '1970-01-01 06:46:13',
            'isDateTime' => TRUE,
        ],
        [
            'value'      => '01/01/1970 07:19:23',
            'isDateTime' => TRUE,
        ],
        [
            'value'      => '2016-02-03 3:14 PM',
            'isDateTime' => TRUE,
        ],
        [
            'value'      => '2016-02-03 3:14PM',
            'isDateTime' => TRUE,
        ],
        [
            'value'      => '2021-05-16 13:27:14',
            'isDateTime' => TRUE,
        ],
        [
            'value'      => '2021-05-16 00:00:00',
            'isDateTime' => TRUE,
        ],
        [
            'value'      => '2021-05-16 00:00',
            'isDateTime' => TRUE,
        ],
        [
            'value'      => '2021-07-19 12:31:14',
            'isDateTime' => TRUE,
        ],
        // ------------------------------
        [
            'value'  => '2020-09-14',
            'isDate' => TRUE,
        ],
        [
            'value'  => '2000-01-01',
            'isDate' => TRUE,
        ],
        [
            'value'  => '2016/02/03',
            'isDate' => TRUE,
        ],
        [
            'value'  => '07/13/2006',
            'isDate' => TRUE,
        ],
        // ------------------------------
        [
            'value'  => '9am',
            'isTime' => TRUE,
        ],
        [
            'value'  => '9 am',
            'isTime' => TRUE,
        ],
        [
            'value'  => '9AM',
            'isTime' => TRUE,
        ],
        [
            'value'  => '9 AM',
            'isTime' => TRUE,
        ],
        [
            'value'  => '9:00',
            'isTime' => TRUE,
        ],
        [
            'value'  => '09:00',
            'isTime' => TRUE,
        ],
        [
            'value'  => '9:15am',
            'isTime' => TRUE,
        ],
        [
            'value'  => '9:15 am',
            'isTime' => TRUE,
        ],
        [
            'value'  => '9:15AM',
            'isTime' => TRUE,
        ],
        [
            'value'  => '9:15 AM',
            'isTime' => TRUE,
        ],
        [
            'value'  => '09:15 AM',
            'isTime' => TRUE,
        ],
        [
            'value'  => '1230',
            'isTime' => TRUE,
        ],
        [
            'value'  => '1800',
            'isTime' => TRUE,
        ],
        [
            'value'  => '2359',
            'isTime' => TRUE,
        ],
        // ------------------------------
        [
            'value'   => 'sep',
            'isMonth' => TRUE,
        ],
        [
            'value'   => 'sept',
            'isMonth' => TRUE,
        ],
        [
            'value'   => 'September',
            'isMonth' => TRUE,
        ],
        [
            'value'   => 'SEPTEMBER',
            'isMonth' => TRUE,
        ],
        [
            'value'   => 'Jul',
            'isMonth' => TRUE,
        ],
        [
            'value'   => 'JUL',
            'isMonth' => TRUE,
        ],
        [
            'value'   => 'July',
            'isMonth' => TRUE,
        ],
        // ------------------------------
        [
            'value'         => 'Month',
            'isIncremental' => TRUE,
            'isMonth'       => TRUE,
        ],
        [
            'value'         => '1 month',
            'isIncremental' => TRUE,
            'isMonth'       => TRUE,
        ],
        [
            'value'         => '2 mths',
            'isIncremental' => TRUE,
            'isMonth'       => TRUE,
        ],
        [
            'value'         => '6 months',
            'isIncremental' => TRUE,
            'isMonth'       => TRUE,
        ],
        [
            'value'         => '2 MONTHS',
            'isIncremental' => TRUE,
            'isMonth'       => TRUE,
        ],
        [
            'value'         => '48 MONTHS',
            'isIncremental' => TRUE,
            'isMonth'       => TRUE,
        ],
        // ------------------------------
        [
            'value'     => 'we',
            'isWeekday' => TRUE,
        ],
        [
            'value'     => 'wed',
            'isWeekday' => TRUE,
        ],
        [
            'value'     => 'WED',
            'isWeekday' => TRUE,
        ],
        [
            'value'     => 'wednesday',
            'isWeekday' => TRUE,
        ],
        [
            'value'     => 'Friday',
            'isWeekday' => TRUE,
        ],
        [
            'value'     => 'FRIDAY',
            'isWeekday' => TRUE,
        ],
        // ------------------------------
        [
            'value'         => 'day',
            'isIncremental' => TRUE,
            'isDay'         => TRUE,
        ],
        [
            'value'         => '1 day',
            'isIncremental' => TRUE,
            'isDay'         => TRUE,
        ],
        [
            'value'         => '3 DAYS',
            'isIncremental' => TRUE,
            'isDay'         => TRUE,
        ],
        [
            'value'         => '60 DAYS',
            'isIncremental' => TRUE,
            'isDay'         => TRUE,
        ],
        // ------------------------------
        [
            'value'         => 'hr',
            'isIncremental' => TRUE,
            'isHour'        => TRUE,
        ],
        [
            'value'         => 'hour',
            'isIncremental' => TRUE,
            'isHour'        => TRUE,
        ],
        [
            'value'         => '1 hour',
            'isIncremental' => TRUE,
            'isHour'        => TRUE,
        ],
        [
            'value'         => '3 HOURS',
            'isIncremental' => TRUE,
            'isHour'        => TRUE,
        ],
        [
            'value'         => '4 hr',
            'isIncremental' => TRUE,
            'isHour'        => TRUE,
        ],
        [
            'value'         => '4 hrs',
            'isIncremental' => TRUE,
            'isHour'        => TRUE,
        ],
        // ------------------------------
        [
            'value'         => '15 minutes',
            'isIncremental' => TRUE,
            'isMinute'      => TRUE,
        ],
        [
            'value'         => '15 MINUTES',
            'isIncremental' => TRUE,
            'isMinute'      => TRUE,
        ],
        [
            'value'         => '15 mins',
            'isIncremental' => TRUE,
            'isMinute'      => TRUE,
        ],
        [
            'value'         => '15 min',
            'isIncremental' => TRUE,
            'isMinute'      => TRUE,
        ],
        [
            'value'         => '60 min',
            'isIncremental' => TRUE,
            'isMinute'      => TRUE,
        ],
        // ------------------------------
        [
            'value'         => '30 sec',
            'isIncremental' => TRUE,
            'isSecond'      => TRUE,
        ],
        [
            'value'         => '35 secs',
            'isIncremental' => TRUE,
            'isSecond'      => TRUE,
        ],
        [
            'value'         => '45 seconds',
            'isIncremental' => TRUE,
            'isSecond'      => TRUE,
        ],
        [
            'value'         => '120 SECONDS',
            'isIncremental' => TRUE,
            'isSecond'      => TRUE,
        ],
        // ------------------------------
    ];

    foreach ($tests as $k => $test) {
        $tests[$k] = array_merge($base, $test);
    }

    foreach (array_keys($base) as $method) {
        $this->try($method . '()')->many($tests, function($t) use ($method) {
            return $t[$method] === (new Tell_Date_Unit($t['value']))->$method();
        });
    }

    // $this->assert(function() { new Tell_Date_Unit('Dec 32nd'); })
    //     ->exception(Tell_Exception_Type::class);

    // $this->assert(function() { new Tell_Date_Unit('Dec abc'); })
    //     ->exception(Tell_Exception_Type::class);

    // $this->assert(function() { new Tell_Date_Unit('1 March'); })
    //     ->exception(Tell_Exception_Type::class);

    // $this->assert(function() { logger(new Tell_Date_Unit('32 Monday')); })
    //     ->exception(Tell_Exception_Type::class);

    // $this->assert(function() { new Tell_Date_Unit('March 32'); })
    //     ->exception(Tell_Exception_Type::class);

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

class Inspect_Cron_Job extends Tell_Cron_Job
{
    public function __call($name, $arguments)
    {
        return $this->$name(...$arguments);
    }
}

class GoodCronJob
{
    public function __invoke()
    {
        return 'OK';
    }
}

class BadCronJob {}

$inspect('Tell_Cron_Job', function(Tell_Container $ioc) {

    $event = $ioc->pull(Tell_Event::class);

    $evFailure = $evSuccess = [];

    $event->register(Tell::EVENT_JOB_FAILURE, function($ev) use (&$evFailure) {
        $evFailure[] = $ev;
    });

    $event->register(Tell::EVENT_JOB_SUCCESS, function($ev) use (&$evSuccess) {
        $evSuccess[] = $ev;
    });

    $reset = function() use (&$evFailure, &$evSuccess) {
        $evFailure = $evSuccess = [];
    };

    $cron = function(string $schedule, $now) use ($ioc) {
        return new Inspect_Cron_Job($schedule, $ioc, __DIR__ . '/../job', $now);
    };

    $assertEvent = function($event) {
        $this->assert($event)->array()
            ->hasKey('schedule')
            ->hasKey('type')
            ->hasKey('input')
            ->hasKey('output')
            ->hasKey('result');
    };

    $this->try('runCommand() - failure event', function() use ($cron, &$evFailure, $reset, $assertEvent) {

        $job = $cron('*/5 * * * *', '2023-03-06 10:10:53');

        $this->assert($job->shouldRun())->true();

        ob_start();

        $job->runCommand('cd /foo/bar/not/exist');

        $this->assert(strlen(trim(ob_get_clean())))->greaterThan(3);

        $assertEvent($evFailure[0] ?? NULL);

        $this->assert($evFailure[0]['input'])->equals('cd /foo/bar/not/exist');

        $this->assert($evFailure[0]['result'])->array()
            ->hasKey('stdout')
            ->hasKey('stderr')
            ->hasKey('return');

        $reset();

    })->then('runCommand() - success event', function() use ($cron, &$evSuccess, $reset, $assertEvent) {

        $job = $cron('*/5 * * * *', '2023-03-06 10:10:53');

        $this->assert($job->shouldRun())->true();

        ob_start();

        $job->runCommand('echo OK');

        $this->assert(trim(ob_get_clean()))->equals('OK');

        $assertEvent($evSuccess[0] ?? NULL);

        $this->assert($evSuccess[0]['input'])->equals('echo OK');

        $this->assert($evSuccess[0]['result'])->array()
            ->hasKey('stdout')
            ->hasKey('stderr')
            ->hasKey('return');

        $this->assert(trim($evSuccess[0]['result']['stdout']))->equals('OK');

        $this->assert($evSuccess[0]['result']['return'])->equals(0);

        $reset();

    })->then('runJob() - failure event', function() use ($cron, &$evFailure, $reset, $assertEvent) {

        $job = $cron('*/5 * * * *', '2023-03-06 10:10:53');

        $this->assert($job->shouldRun())->true();

        ob_start();

        $job->runJob('invalid.job');

        $this->assert(trim(ob_get_clean()))->equals('"invalid.job" does not resolve to a valid job script.');

        $assertEvent($evFailure[0] ?? NULL);

        $this->assert($evFailure[0]['input'])->equals('invalid.job');

        $this->assert($evFailure[0]['output'])->equals('"invalid.job" does not resolve to a valid job script.');

        $reset();

    })->then('runJob() - success event', function() use ($cron, &$evSuccess, $reset, $assertEvent) {

        $job = $cron('*/5 * * * *', '2023-03-06 10:10:53');

        $this->assert($job->shouldRun())->true();

        ob_start();

        $job->runJob('test');

        $this->assert(trim(ob_get_clean()))->equals('OK');

        $assertEvent($evSuccess[0] ?? NULL);

        $this->assert($evSuccess[0]['input'])->equals('test');

        $this->assert($evSuccess[0]['output'])->equals('OK');

        $reset();

    })->then('runResolve() - failure event', function() use ($cron, &$evFailure, $reset, $assertEvent) {

        $job = $cron('*/5 * * * *', '2023-03-06 10:10:53');

        $this->assert($job->shouldRun())->true();

        ob_start();

        $job->runResolve(BadCronJob::class);

        $this->assert(trim(ob_get_clean()))->equals('BadCronJob must have an __invoke() method to be used as a cron job.');

        $assertEvent($evFailure[0] ?? NULL);

        $this->assert($evFailure[0]['input'])->equals(BadCronJob::class);

        $reset();

    })->then('runResolve() - success event', function() use ($cron, &$evSuccess, $reset, $assertEvent) {

        $job = $cron('*/5 * * * *', '2023-03-06 10:10:53');

        $this->assert($job->shouldRun())->true();

        ob_start();

        $job->runResolve(GoodCronJob::class);

        $this->assert(trim(ob_get_clean()))->equals('OK');

        $assertEvent($evSuccess[0] ?? NULL);

        $this->assert($evSuccess[0]['input'])->equals(GoodCronJob::class);

        $this->assert($evSuccess[0]['output'])->equals('OK');

        $reset();

    })->then('__invoke()', function() use ($cron, &$evSuccess, $reset, $assertEvent) {

        $job = $cron('*/5 * * * *', '2023-03-06 10:10:53')
            ->command('echo OK')
            ->job('test')
            ->resolve(GoodCronJob::class);

        $this->assert($job->shouldRun())->true();

        ob_start();

        $job->__invoke();

        $this->assert(trim(ob_get_clean()))->equals("OK\nOK\nOK");

        $assertEvent($evSuccess[0] ?? NULL);

        $assertEvent($evSuccess[1] ?? NULL);

        $assertEvent($evSuccess[2] ?? NULL);

        $reset();

    });

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

$inspect('Tell_Reflect', function() {

    $this->try('_()', function() {

        // @todo

    });

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

$inspect('Tell_Email', function() {

    $this->try('__()', function() {

        // @todo

    });

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

$inspect('Tell_Request', function() {

    $this->try('_()', function() {

        // @todo

    });

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

$inspect('Tell_Json', function() {

    $this->try('_()', function() {

        // @todo

    });

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

$inspect('Tell_Schema_Table_Mysql', function() {

    $this->try('Creating example tables from SQL file', function(Tell_Db $db) {

        if ($db->pdo instanceof Closure) {
            $db->pdo->__invoke();
        }

        $this->assert($db->pdo->exec($this->ddl))->int()->equals(0);

    })->then('Validate the DDL without schema in cache', function(Tell_Schema $schema) {

        $this->assert($schema->incrementColumn('example'))->equals('id');

        $this->assert($schema->primaryKey('example'))->equals('id');

        $this->assert($schema->hasTable('example'))->true();

        $this->assert($schema->hasTable('example2'))->false();

        $this->assert($schema->rename('example', 'example2'))->true();

        $this->assert($schema->hasTable('example'))->false();

        $this->assert($schema->hasTable('example2'))->true();

        $this->assert($schema->hasColumn('example2', 'id'))->true();

        $this->assert($schema->hasColumn('example2', 'id2'))->false();

        $this->assert($schema->hasColumn('example', 'id'))->false();

        $this->assert($schema->hasIndex('example2', 'id'))->false();

        $this->assert($schema->hasIndex('example2', 'bill_first_name, bill_last_name'))->true();

        $this->assert($schema->incrementColumn('example2'))->equals('id');

        $this->assert($schema->primaryKey('example2'))->equals('id');

        $this->assert($schema->dropColumn('example2', 'order_number'))->true();

    })->then('Validate the DDL with schema in cache', function(Tell_Schema $schema) {

        $schema('example2'); // puts schema in cache

        $this->assert($schema->incrementColumn('example2'))->equals('id');

        $this->assert($schema->primaryKey('example2'))->equals('id');

        $this->assert($schema->hasTable('example2'))->true();

        $this->assert($schema->hasTable('example'))->false();

        $this->assert($schema->hasColumn('example2', 'id'))->true();

        $this->assert($schema->hasColumn('example2', 'id2'))->false();

        $this->assert($schema->hasColumn('example', 'id'))->false();

        $this->assert($schema->hasIndex('example2', 'id'))->false();

        $this->assert($schema->hasIndex('example2', 'bill_first_name, bill_last_name'))->true();

        $this->assert($schema->incrementColumn('example2'))->equals('id');

        $this->assert($schema->primaryKey('example2'))->equals('id');

        $this->assert($schema->dropColumn('example2', 'order_number'))->true();

        $this->assert($schema->drop('example2'))->true();

        $this->assert($schema->hasTable('example2'))->false();

    })->then('Create and alter a table', function(Tell_Schema $schema, Tell_Db $db) {

        $this->assert($schema->drop('items'))->true();

        $this->assert($schema->create('items', function(Tell_Schema_Column $column) {
            $column->serial('id')->primary();
            $column->int('category_id');
            $column->enum('listing_status', ['Active', 'Backordered', 'Discontinued', 'Unlisted']);
            $column->varchar('item_name');
            $column->currency('price');
            $column->dateTime('modified');
            $column->dateTime('created');
            $column->index('id, category_id');
            $column->index('listing_status');
        }))->true();

        $this->assert($schema->hasTable('items'))->true();

        $this->assert($schema->hasColumn('items', 'id'))->true();

        $this->assert($schema->hasColumn('items', 'category_id'))->true();

        $this->assert($schema->hasColumn('items', 'listing_status'))->true();

        $this->assert($schema->hasColumn('items', 'item_name'))->true();

        $this->assert($schema->hasColumn('items', 'price'))->true();

        $this->assert($schema->hasColumn('items', 'modified'))->true();

        $this->assert($schema->hasColumn('items', 'created'))->true();

        $this->assert($schema->hasColumn('items', 'something_else'))->false();

        $this->assert($schema->incrementColumn('items'))->equals('id');

        $this->assert($schema->primaryKey('items'))->equals('id');

        $this->assert($schema->hasIndex('items', 'id, category_id'))->true();

        $this->assert($schema->hasIndex('items', 'listing_status'))->true();

        $table = $schema->schema('items', TRUE);

        $this->assert($table)->instanceOf(Tell_Schema_Schema_Bridge::class);

        $columns = $table->columns;

        $this->assert($columns)->arr();

        $this->assert($columns['item_name'] ?? NULL)->instanceOf(Tell_Schema_Column_Bridge::class);

        $this->assert($columns['item_name']->length)->int()->equals(255);

        for ($i = 0; $i < 5; $i++) {
            $this->assert($db("INSERT INTO items")->data(Test_Schema::rowIpsumItem())->run())
                ->int()
                ->equals($i + 1);
        }

        $this->assert($schema->alter('items', function(Tell_Schema_Column $column) {
            $column->rename('id', 'item_id');
            $column->rename('listing_status', 'status');
            $column->enum('status', ['Active', 'Preordered', 'Backordered', 'Discontinued', 'Unlisted']);
            $column->varchar('item_name', 128);
            $column->dropIndex(['id', 'category_id']);
        }))->true();

        $this->assert($schema->incrementColumn('items'))->equals('item_id');

        $this->assert($schema->primaryKey('items'))->equals('item_id');

        $this->assert($schema->hasColumn('items', 'listing_status'))->false();

        $this->assert($schema->hasColumn('items', 'status'))->true();

        $this->assert($schema->hasIndex('items', 'id, category_id'))->false();

        $this->assert($schema->hasIndex('items', 'listing_status'))->false();

        $this->assert($schema->hasIndex('items', 'status'))->true();

        $columns = $schema->schema('items', TRUE)->columns;

        $this->assert($columns['item_name']->length)->int()->equals(128);

        $this->assert($db("INSERT INTO items")->data(Test_Schema::rowItemAfterAltered())->run())
            ->int()
            ->equals($i + 1);

        $this->assert($schema->drop('items'))->true();

        $this->assert($schema->hasTable('items'))->false();

    });

})->swap(function(Tell_Event $event) {
    return [
        'Tell_Db' => new Tell_Db([
            'driver'             => 'mysql',
            'database'           => 'tell_php_test',
            'host'               => '127.0.0.1',
            'port'               => NULL,
            'socket'             => NULL,
            'username'           => 'tell_php_test',
            'password'           => 'testing',
            'elevate_exceptions' => TRUE,
            'log_failed'         => TRUE,
            'log_success'        => FALSE,
            'log_limit'          => 1000,
        ], $event),
    ];
})->vars([
    'ddl' => file_get_contents(__DIR__ . '/../assets/sql/table.mysql.sql'),
])->onlyWhen(function() {
    return extension_loaded('pdo_mysql') && env('TELL_PHP_DEV');
});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

$inspect('Tell_Exception_File', function() {

    $this->try('Granular Methods', function() {

        $this->try('__construct()', function() {

            // @todo

        });

        $this->try('toArr()', function() {

            // @todo

        });

        $this->try('operation()', function() {

            // @todo

        });

        $this->try('path()', function() {

            // @todo

        });

        $this->try('reason()', function() {

            // @todo

        });

    })->then('Shorthand Methods', function() {

        $this->try('notEmpty()', function() {

            // @todo

        });

        $this->try('notExecutable()', function() {

            // @todo

        });

        $this->try('notDirectory()', function() {

            // @todo

        });

        $this->try('notEmptyDirectory()', function() {

            // @todo

        });

        $this->try('notExecutable()', function() {

            // @todo

        });

        $this->try('notFile()', function() {

            // @todo

        });

        $this->try('notFound()', function() {

            // @todo

        });


        $this->try('notReadable()', function() {

            // @todo

        });

        $this->try('notWritable()', function() {

            // @todo

        });

        $this->try('failedDeleteDirectory()', function() {

            // @todo

        });

        $this->try('failedDeleteFile()', function() {

            // @todo

        });

        $this->try('failedCreateDirectory()', function() {

            // @todo

        });

        $this->try('failedCreateFile()', function() {

            // @todo

        });

        $this->try('failedUpdateFile()', function() {

            // @todo

        });

    });

});

$inspect('Tell_Exception_Path', function() {

    $this->try('_()', function() {

        // @todo

    });

});

$inspect('Tell_Exception_Type', function() {

    $this->try('_()', function() {

        // @todo

    });

});
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

/**
 * This inspection is still under construction.
 * ---
 * @todo
 */

// ---------------------------------------------------------------------------------------------

$inspect('Tell_Inflect_Pluralize', function() {

    $this->try('isPlural()', function() {

        // @todo

    })->then('plural()', function() {

        // @todo

    });

    $this->try('isSingular()', function() {

        // @todo

    })->then('singular()', function() {

        // @todo

    });

});

// ---------------------------------------------------------------------------------------------

$inspect('Tell_Inflect_Variadic - Lists', function() {


    $this->try('isListable()', function() {

        // @todo

    })->then('toList()', function() {

        // @todo

    });

})->vars([
    'args' => [
        0 => new stdClass(),
        1 => [new stdClass()],
        2 => 'dory',
        3 => ['dory'],
        4 => ['foo', 'I', 'am', 'a', 'nice', 'shark'],
        5 => ['foo', 'I', 'am', 'a', new stdClass(), 'shark'],
    ],
]);

// ---------------------------------------------------------------------------------------------

$inspect('Tell_Inflect_Variadic - Parameters', function() {

    $this->try('isParamable()', function() {

        // @todo

    })->then('toParams()', function() {

        // @todo

    });

})->vars([
    'good' => [
        0 => ['first_name', 'Bob'],
        1 => [
            'first_name' => 'Bob',
            'last_name'  => 'Smith',
        ],
    ],
    'bad' => [
        0 => 'Bob',
        1 => ['Bob'],
        2 => ['first_name', 'Bob', 'last_name', 'Smith'],
    ],
]);

// ---------------------------------------------------------------------------------------------

$inspect('Tell_Inflect_Variadic - Rows', function() {

    $this->try('isRowable()', function() {

        foreach ($this->good as $val) {

            $inflect = new Tell_Inflect_Variadic([], $val);

            $this->assert($inflect->isRowable())->true();

        }

        foreach ($this->bad as $val) {

            $inflect = new Tell_Inflect_Variadic([], $val);

            $this->assert($inflect->isRowable())->false();

        }

    })->then('toRows()', function() {

        foreach ($this->good as $val) {

            $inflect = new Tell_Inflect_Variadic([], $val);

            $this->assert($inflect->toRows())->arr();

        }

        foreach ($this->bad as $val) {

            $inflect = new Tell_Inflect_Variadic([], $val);

            $this->assert($inflect->toRows())->null();

        }

    });

})->vars([
    'good' => [
        0 => [[
            'first_name' => 'Johnny',
            'last_name'  => 'Raincloud',
            'billing_id' => NULL,
        ]],
        1 => [[
            [
                'first_name' => 'Johnny',
                'last_name'  => 'Raincloud',
            ],
            [
                'first_name' => 'Negative',
                'last_name'  => 'Nancy'
            ],
            [
                'first_name' => 'Starvin',
                'last_name'  => 'Marvin',
            ],
        ]],
    ],
    'bad' => [
        0 => [['Positive', 'Pedro']],
        1 => [[
            [
                'first_name' => 'Johnny',
                'last_name'  => 'Raincloud',
            ],
            'Negative Nancy',
            [
                'first_name' => 'Starvin',
                'last_name'  => 'Marvin',
            ],
        ]],
    ],
]);

// ---------------------------------------------------------------------------------------------

$inspect('Tell_Inflect', function() {

    $this->try('plural()', function() {

        // @todo

    });

    $this->try('singular()', function() {

        // @todo

    });

    $this->try('bytes()', function() {

        // @todo

    });

    $this->try('kilobytes()', function() {

        // @todo

    });

    $this->try('megabytes()', function() {

        // @todo

    });

    $this->try('gigabytes()', function() {

        // @todo

    });

    $this->try('terabytes()', function() {

        // @todo

    });

    $this->try('filesize()', function() {

        // @todo

    });

    $this->try('unitMultiplier()', function() {

        // @todo

    });

    $this->try('bool()', function() {

        // @todo

    });

    $this->try('enum()', function() {

        // @todo

    });

    $this->try('humanize()', function() {

        // @todo

    });

    $this->try('int()', function() {

        // @todo

    });

    $this->try('ordinal()', function() {

        // @todo

    });

    $this->try('slugify()', function() {

        // @todo

    });

    $this->try('time()', function() {

        // @todo

    });

    $this->try('unaccent()', function() {

        // @todo

    });

});

// ---------------------------------------------------------------------------------------------
Open in a new window Source Run
            {JSON results of applicable inspection}
        
<?php

$inspect('Action', function() {

    $db = Mockery::mock(Tell_Db::class);
    $db->shouldReceive('query->data->run')->andReturn(1);

    $action = new Test_Action_UserCreate();
    $action::inject('ioc', $this->ioc);
    $action::inject('db', $db);

    $this->try('Creating user')->action(Test_Action_UserCreate::class)->assert();

})->request(function(Tell_Ipsum_Contact $ipsum) {

    $contact = $ipsum->contact();

    $password = $ipsum->password();

    return [
        'method' => 'POST',
        'post'   => [
            'first_name'       => $contact->first_name,
            'last_name'        => $contact->last_name,
            'address1'         => $contact->address1,
            'address2'         => $contact->address2,
            'city'             => $contact->city,
            'province'         => $contact->province,
            'postal'           => $contact->postal,
            'country'          => $contact->country,
            'email'            => $contact->email,
            'password'         => $password,
            'password_confirm' => $password,
            'status'           => $ipsum->random(['Active', 'Disabled']),
        ],
    ];

});