The path to an automagical multisite switch 02

Doping a WordPress test installation.

Foreword

This is a work in progress to give semi-automagical powers to the WPDb module part of wp-browser, a WordPress specific set of modules for the Codeception testing suite.
As such I might babble incoherently and waste my time; I’m fine with it and hope anyone reading this is as well.

A failing test

Resuming the struggle outlined in the previous post I will work in a TDD fashion and start from a failing test, the one below.

class WPDbSubdomainMultisiteCest {

    public function _before( AcceptanceTester $I ) {
    }

    public function _after( AcceptanceTester $I ) {
    }

    /**
     * @test
     * it should allow seeing posts from different blogs
     */
    public function it_should_allow_seing_posts_from_different_blogs( AcceptanceTester $I ) {
        $I->haveMultisiteInDatabase();
        $ids = $I->haveManyBlogsInDatabase( 3, [ 'domain' => 'test{{n}}' ] );

        for ( $i = 0; $i < 3; $i++ ) {
            $I->seeBlogInDatabase( [ 'domain' => 'test' . $i ] );
        }

        $firstBlogId = reset( $ids );
        $I->useBlog( $firstBlogId );
        $I->haveManyPostsInDatabase( 3, [
            'post_title'    => 'Blog {{blog}} - Post {{n}}',
            'template_data' => [ 'blog' => $firstBlogId ]
        ] );

        $I->amOnSubdomain( 'test0' );
        $I->amOnPage( '/' );
        $I->see( "Blog $firstBlogId - Post 0" );
        $I->see( "Blog $firstBlogId - Post 1" );
        $I->see( "Blog $firstBlogId - Post 2" );

        $I->amOnSubdomain( 'test1' );
        $I->amOnPage( '/' );
        $I->dontSee( "Blog {$ids[1]} - Post 0" );

        $I->amOnSubdomain( 'test2' );
        $I->amOnPage( '/' );
        $I->dontSee( "Blog {$ids[2]} - Post 0" );
    }
}

Failing ambitious test This test is currently failing on the assertion

$I->see( "Blog $firstBlogId - Post 0" );

As the site is not running as multisite and trying to reach it through a subdomain will simply redirect to the main site.
The underlying database structure might be there but WordPress is not using it.

wp-config injection

Referring to the previous article points 1 and 2 of the multisite recipe are not covered (constants and .htaccess file).
To cover the missing constant definitions I’ve modified the haveMultisiteInDatabase method of the WPDb module to insert an entry in the database in its last lines.

public function haveMultisiteInDatabase( $subdomainInstall = true ) {
    $this->isSubdomainMultisiteInstall = $subdomainInstall;
    $dbh                               = $this->driver->getDbh();
    foreach ( $this->tables->multisiteTables() as $table ) {
        $operation         = 'create';
        $prefixedTableName = $this->grabPrefixedTableNameFor( $table );
        if ( $this->_seeTableInDatabase( $prefixedTableName ) ) {
            $query     = $this->tables->getAlterTableQuery( $table, $this->config['tablePrefix'] );
            $operation = 'alter';
        } else {
            $query = $this->tables->getCreateTableQuery( $table, $this->config['tablePrefix'] );
        }

        if ( !empty ( $query ) ) {
            $sth = $dbh->prepare( $query );
            $this->debugSection( 'Query', $sth->queryString );
            $out[$table] = [ 'operation' => $operation, 'exit' => $sth->execute( [ ] ) ];
        } else {
            $out[$table] = [ 'operation' => $operation, 'exit' => false ];
        }
    }

    $domain = $this->getSiteDomain();
    if ( !$this->countInDatabase( $this->grabSiteTableName(), [ 'domain' => $domain ] ) ) {
        $this->haveInDatabase( $this->grabSiteTableName(), [ 'domain' => $domain, 'path' => '/' ] );
    }
    if ( !$this->countInDatabase( $this->grabBlogsTableName(), [ 'blog_id' => 1 ] ) ) {
        $this->query( "ALTER TABLE {$this->grabBlogsTableName()} AUTO_INCREMENT=1" );
        $mainBlogData = [
            'site_id'      => 1,
            'domain'       => $domain,
            'path'         => '/',
            'registered'   => Date::now(),
            'last_updated' => Date::now(),
            'public'       => 1
        ];
        $this->haveInDatabase( $this->grabBlogsTableName(), $mainBlogData );
    }

    $this->haveOptionInDatabase( '_wpbrowser', [
        'isMultisite'      => true,
        'subdomainInstall' => $subdomainInstall,
        'siteDomain'       => $domain,
        'pathCurrentSite'  => '/'
    ], 'yes' );

    return $out;
}

This is covering the values WordPress multisite constants will require.
The need for a line in the database is due to how acceptance testing, the worst case scenario for Codeception modules, work.
In an acceptance use case the relations will be like the one below Acceptance testing relations Anything the WPDb module can do will have to do in the database.
But WordPress code base is part of the equation too and with it the wp-config.php file; leveraging it I will try to make WordPress run in multisite mode when and if required.
This will require modifying the last lines of the typical wp-config.php file checking for a local WPBrowser config file, something like this

// the usual stuff...

/* That's all, stop editing! Happy blogging. */

/** WPBrwoser configuration */
if (file_exists(dirname(__FILE__) . '/wpbrowser-config.php')) {
    include dirname(__FILE__) . '/wpbrowser-config.php';
}

/** Absolute path to the WordPress directory. */
if ( !defined('ABSPATH') )
    define('ABSPATH', dirname(__FILE__) . '/');

/** Sets up WordPress vars and included files. */
require_once(ABSPATH . 'wp-settings.php');

What this wpbrowser-config.php file needs to do is to set up WordPress to run as multisite if so required.

// Open a connection to WordPress database
try {
    $dsn = 'mysql:dbname=' . DB_NAME . ';host=' . DB_HOST;
    $dbh = new PDO( $dsn, DB_USER, DB_PASSWORD );
} catch ( PDOException $e ) {
    echo "WPBrowser Config: there was an issue connecting to the database";

    return;
}

// read the line the WPDb module might have inserted in the database
$sth     = $dbh->query( "SELECT option_value FROM {$table_prefix}options WHERE option_name = '_wpbrowser'" );
$options = $sth->fetchColumn();
unset( $dbh );
if ( empty( $options ) ) {
    return;
}

// depending upon the options define some constants 
$options = @unserialize( $options );
if ( ! empty( $options ) ) {
    if ( ! empty( $options['isMultisite'] ) ) {
        $multisiteConstants = [
            'WP_ALLOW_MULTISITE'   => true,
            'MULTISITE'            => true,
            'SUBDOMAIN_INSTALL'    => $options['subdomainInstall'],
            'DOMAIN_CURRENT_SITE'  => $options['siteDomain'],
            'PATH_CURRENT_SITE'    => $options['pathCurrentSite'],
            'SITE_ID_CURRENT_SITE' => 1,
            'BLOG_ID_CURRENT_SITE' => 1
        ];

        foreach ( $multisiteConstants as $multisiteConstant => $value ) {
            if ( ! defined( $multisiteConstant ) ) {
                error_log( 'Defining ' . $multisiteConstant . ' to ' . $value );
                define( $multisiteConstant, $value );
            }
        }
    }
}

Since WordPress will not be up and running when this piece of code executes a traditional database access is required but after that it’s nothing fancy.
Now, how to test this first modification?

The multisite test theme

I’ve scaffolded a basic theme bearing the slug of multisite and the relevant part of it is its minimal index.php markup

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Multisite Test</title>
</head>
<body>
<h1><?php echo is_multisite() ? 'Multisite is active' : 'Multisite is not active' ?></h1>   
</body>
</html>

Very humble. Multisite test theme index page It’s time for a new and less ambitious test targeted at this test theme specifically

/**
 * @test
 * it should not activate multisite by default
 */
public function it_should_not_activate_multisite_by_default( AcceptanceTester $I ) {
    // Set the theme to the multisite test one
    $I->haveOptionInDatabase( 'stylesheet', 'multisite', 'yes' );
    $I->haveOptionInDatabase( 'template', 'multisite', 'yes' );

    $I->amOnPage( '/' );
    $I->see( 'Multisite is not active' );
}

/**
 * @test
 * it should be able to activate multisite
 */
public function it_should_be_able_to_activate_multisite( AcceptanceTester $I ) {
    // Set the theme to the multisite test one
    $I->haveOptionInDatabase( 'stylesheet', 'multisite', 'yes' );
    $I->haveOptionInDatabase( 'template', 'multisite', 'yes' );

    $I->haveMultisiteInDatabase();

    $I->amOnPage( '/' );
    $I->see( 'Multisite is active' );
}

and while the third and ambitious test keeps failing the first two will pass. 2 tests on 3 passingDoping a WordPress test installation.

Next

I will tackle the missing piece: the .htaccess file.