WordPress

  • Building Automated Tests with WordPress Playground

    A big advantage of WordPress Playground is that it can start a new WordPress site quickly and be configured during boot.

    Both of these features are important for automated testing, so Playground seems like a good fit for testing WordPress projects.

    Previously Jan and I worked on the Playground tester which automatically tests Playground compatibility of all WordPress.org themes and plugins using GitHub actions (for free).

    The original approach using Bash was a bit complicated, but after Playground created a way to start the Playground CLI from JavaScript by calling runCLI, using Playground for automated tests became easy.

    In this post I will demonstrate how to get started with using the Playground CLI for testing WordPress projects.

    If you want to skip the tutorial and see the final code, it’s available on GitHub.

    Or if you prefer video, you can checkout this WCEU workshop.

    Setup

    To follow this tutorial checkout the playground-testing-demo repository and follow the setup instructions in the readme.

    The demo project includes a simple WordPress plugin, Vitest for integration testing and Playwright for end-to-end testing.

    Playground CLI

    Before we can start building tests, we need to get familiar with the Playground CLI.

    The Playground CLI enables us creates a local server for our WordPress projects.

    Mount a project into Playground

    To start using the CLI make sure you have at least Node 20 installed and after that run npx @wp-playground/cli server --mount=.:/wordpress/wp-content/plugins/playground-testing-demo to start a server and mount your current directory as a WordPress plugin.

    Configure Playground using a Blueprint

    To automatically activate the plugin we can create a Blueprint with an activatePlugin step.

    Here’s a Blueprint that will activate this sample plugin:

    {
        "steps": [
            {
                "step": "activatePlugin",
                "pluginPath": "/wordpress/wp-content/plugins/playground-testing-demo/playground-testing-demo.php"
            }
        ]
    }

    Now we can start a server and activate the plugin during boot using npx @wp-playground/cli server --mount=.:/wordpress/wp-content/plugins/ --blueprint=./blueprint.json

    Notes

    You can replace . with a path to your project and /wordpress/wp-content/plugins/ with a path where you would like your project to be loaded.

    /wordpress/ is the site root folder, similar to /var/www/ in some other severs.

    If you are mounting an entire WordPress site use a combination of --mountBeforeInstall and --skipWordPressSetup.

    End-to-end tests

    In the sample plugin you can run end-to-end tests using npm run test:e2e.

    For the sample plugin we want to test the form and confirm that the data is persisted after a page reload.

    Before starting, you can copy all the necessary imports from here.

    First we need to start Playground just like we did with the CLI.

    const blueprint = JSON.parse(
          readFileSync(resolve("./blueprint.json"), "utf8")
    );
    const cliServer = await runCLI({
          command: "server",
          mount: [".:/wordpress/wp-content/plugins/playground-testing-demo"],
          blueprint,
          quiet: true,
    });
    const handler = cliServer.requestHandler;
    const php = await handler.getPrimaryPhp();

    After that we need to login, so we get access to the admin page.

    await login(php, {
          username: "admin",
    });

    Now we can generate the admin page URL by using the PHPRequestHandler from Playground.

    const wpAdminUrl = new URL(handler.absoluteUrl);
        wpAdminUrl.pathname = "/wp-admin/admin.php";
        wpAdminUrl.searchParams.set("page", "workshop-tests");

    And finally we can run our tests using Playwright.

    await page.goto(wpAdminUrl.toString());
    
    await expect(page).toHaveTitle(/Workshop Tests/);
    
    await page.getByPlaceholder("Enter a message").fill("Hello, world!");
    await page.getByRole("button", { name: "Send" }).click();
    await expect(page.getByText("User says: Hello, world!")).toBeVisible();
    
    await page.reload();
    await expect(page.getByText("User says: Hello, world!")).toBeVisible();

    Also, let’s not forget to stop the server in the end.

    await cliServer.server.close();

    Here’s the full end-to-end test for the sample plugin.

    Integration tests

    In the sample plugin you can run integration tests using npm run test:integration.

    For this tutorial we will cover how to test a Rest API endpoint, more test examples are available here.

    Before starting, you can copy all the necessary imports from here.

    First we need to start Playground just like we did with the CLI and in end-to-end tests.

    const blueprint = JSON.parse(
          readFileSync(resolve("./blueprint.json"), "utf8")
    );
    const cliServer = await runCLI({
          command: "server",
          mount: [".:/wordpress/wp-content/plugins/playground-testing-demo"],
          blueprint,
          quiet: true,
    });
    const handler = cliServer.requestHandler;
    const php = await handler.getPrimaryPhp();

    Now we can generate the API endpoint url by using the PHPRequestHandler from Playground.

    const apiUrl = new URL("/wp-json/PTD/v1/message", handler.absoluteUrl);

    Before we can send a request to the API, we need to generate a Rest API nonce.

    Nonces are tied to cookies, but they aren’t stored in Node, so we need to extend fetch with a Cookie store.

    const fetchCookie = makeFetchCookie(fetch);

    Once we can store cookies, we can create a PHP script using Playground’s file system API and call that script using fetchCookie to obtain the nonce.

    
    
    const getAuthHeaders = async (handler: PHPRequestHandler) => {
      const php = await handler.getPrimaryPhp();
      if (!(await php.fileExists("/wordpress/get_rest_auth_data.php"))) {
        await php.writeFile(
          "/wordpress/get_rest_auth_data.php",
          `<?php
                require_once '/wordpress/wp-load.php';
                echo json_encode(
                    array(
                        'X-WP-Nonce' => wp_create_nonce('wp_rest'),
                    )
                );
                `
        );
      }
    
      await login(php, {
        username: "admin",
      });
      const response = await fetchCookie(handler.absoluteUrl + "/get_rest_auth_data.php");
      return await response.json();
    };

    Now we can run out integration test and check if the API returns a valid response.

    In the test we first obtain the nonce, so that we can send it in the request.
    Because we use fetchCookie, the cookie that was sent in the nonce response will now be sent back together with the request.

    const authHeaders = await getAuthHeaders(handler);
    const formData = new FormData();
    formData.append("message", "John Doe");
    const apiResponse = await fetchCookie(apiUrl, {
          method: "POST",
          headers: authHeaders,
          body: formData,
    });
    const responseJson = await apiResponse.json();
    expect(apiResponse.status).toBe(200);
    expect(responseJson).toMatchObject({
          success: true,
          message: "User says: John Doe",
    });

    And in the end, let’s not forget to stop the server.

    await cliServer.server.close();

    Have fun and build something awesome!

  • Someone finished installing WordPress for me

    When I first started building this site, I took it slow and did a bit every day. One day I would update DNS records, the next day I would set up Nginx, and so on.

    When it was time to install WordPress I uploaded the files and stopped for the day, leaving an unfinished WordPress installation publicly available for anyone to finish. At the time I thought that it would be funny to come back and see the site was installed.

    I didn’t care about the server and could rebuild it at any time, so I left it unfinished.

    The next day guess what happened. My site installation was finished. When I saw it I was excited.

    I’m a huge Darknet Diaries fan but don’t have a lot of security experience, so this was a perfect opportunity for me to practice what I’ve learned.

    Cleaning up

    First I turned off the server to get some time to think. I considered leaving it running to see what the hacker would do next but decided to don’t do it because I had very limited time to play with it. Plus, I now know how to get hacked, so I can easily recreate it in the future.

    The next day I made a clone of the server and restored my server from a backup.

    After that, I changed a couple of passwords and SSH keys the server had. With that done, I was back up and running and ensured nobody could come back. I don’t think that they could come back, but this was an exercise for me and I wanted to do everything the right way.

    Analysis

    Now it was time to see what happened. I turned on the cloned machine and connected to it.

    Remote database

    First I took a look at the WordPress installation because I assume that there are database credentials in wp-config.php and I was right.

    I was also able to find the exact time when the installation happened by checking when wp-config.php was created.

    Of course, I had to connect to the remote database to see what I could find there.

    The database contained a couple of WordPress installations and they all looked standard as if someone just went through the installation process.

    There was an admin email in the options table. It looks like a throwaway Gmail account, but the format is FirstnameLastnameNumbers.

    In the end, I removed my database tables from the remote database.

    WordPress installation

    To my surprise, there weren’t any plugins or themes installed, and the files weren’t changed by the hacker. There wasn’t much I could find there.

    Server

    I assumed that the hacker didn’t get server access, but to be sure I went to check what files were changed and I couldn’t find anything unusual.

    Also, there were no new users added.

    Next, I went through the logs to see if I could find where the attack came from.

    I quickly checked Nginx and PHP logs, but there wasn’t anything useful there.

    This is when I decided to stop and actually launch the blog.

    Next steps

    Because I have the domain names of other hacked sites, I will try to get in touch with site owners and let them know what happened.

    Things I learned

    It can be fun to get hacked as long as you don’t have anything to lose.

    From now on I will always put my unfinished sites behind basic auth.

    I should disable remote database connections because they aren’t necessary and would have prevented this from happening.