My computer has been running a Linux-based OS for several years now. Updating the OS & software generally takes about a minute. Unfortunately, I have some software that only run on Windows. So I use Windows 7 via VirtualBox for that stuff. The main reason I say "unfortunately" is updating Windows 7 is such a freaking hastle. It takes a long time to run and multiple reboots are commonly necessary to get all of the everything installed.
This annoyance came to a head the other day. I hadn't used the VM since May, so logged into my admin account and started Windows Update. The "Checking for updates" process ran, and ran, and ran. I let it keep going, out of curiosity. But after 5 freakin' hours, I figured I should kill the process before some cat in the neighborhood got killed by my curiosity.
A bit of web searching brought up a message board thread page from June 2016. The folks there mentioned some KB articles/downloads that solved the situation for them at the time. But I already had those updates installed. Then I noticed the thread had several more pages. Jumping to the latest post in the thread indicated KB 3172605: July 2016 update rollup for Windows 7 SP1 and Windows Server 2008 R2 SP1 did the trick.
Sidebar Gripe: The kids at Microsoft have drank the "Look, Ma! No HTML!" Flavor Aid. Their KB pages use JavaScript to render main content. I've been meaning to write a blog post about this long increasing trend, so all I'll say about that here is "Stop It!"
Anyway, I downloded that rollup, stopped the Windows Update service (just in case) and double clicked on the package. It ran quickly and I rebooted.
Then using Windows Update went okay. Found about 40 updates relatively quickly, though 30 minutes had passed by the time the reboot came around.
Once up and running, starting Windows Update revealed 1 more important update awaits. I decided to check for updates again, which found another 3 important updates. Ten more minutes and I'm rebooting.
Again, the obligitory update check. Nope, not done yet. KB3177467 wants to be installed. Sigh. Install that, reboot, check for updates. Voila!
Who'da thunk this whole mishegas could have started 7 hours ago.
While installing Universal Ctags from source on a new machine I ran into the following error during the autogen.sh step:
error: possibly undefined macro: AC_DEFINE
Fortunately, the solution was as simple as installing pkg-config:
apt-get install pkg-config
There's a new trend in brute force attacks. Most IP addresses are being used a single time. This renders IP address blocking useless.
Traditionally, criminals used a few (compromised) computers to wage brute force attacks against a given site. So security professionals and web masters have monitored IP addresses and set up rules to deny requests from bad actors.
But the increasing proliferation of root kits and sophistication of command and control software has meant attackers can consider each bot (kind of) disposable. They'll use a given computer for one shot, then move on to another computer for the next shot.
Here's an example. One of my sites received a steady stream of 3,496 bogus login requests over a 56 hour period (about 60 per hour) in early January, 2016. The attack came from 2,019 different IP addresses. 1,260 (36%) of the IP's were used only once. Just 3 addresses were used more than 10 times. The most used address only made 15 requests!
Filtering by the first two or three octets of the IP space doesn't get you anything either.
Item | Quantity | Used 1x | Used > 10x | Maximum Used |
---|---|---|---|---|
Unique IPs | 2,019 | 1,260 | 3 | 15 |
Unique First 3 Octets | 1,651 | 871 | 9 | 17 |
Unique First 2 Octets | 744 | 191 | 71 | 80 |
Passwords | 1,191 | n/a | 0 | 4 |
User Names | 3 | n/a | n/a | n/a |
The attacker's control server picks three likely user names ("administrator", "admin", and the blog's name) and one password then tells three bots to try one combination. Then the control server picks another password and has three other bots try those combinations. Rinse and repeat.
I noticed this trend because I'm the author, and user, of the Login Security Solution WordPress plugin. Fortunately, LSS is set up to catch these kinds of attacks by monitoring any combination of IP address (including IPv6), user name, or password. All of the other brute force plugins I've looked at only watch for IP addresses.
Hmm.... Insert your favorite closing quip here by sending it to me on Twitter. :)
Here's a fun one... I recently received emails from two tech firms in London. They had a suspicion that the "Daniel Convissor" seeking employment from them was an imposter. The guy put some of my work on his resume. So, yes, they're a fraud.
The fool is using "danielconvissor@gmail.com" to email folks. On January 12th, I filled out Google's Abuse Form. Ten days later, I got another report, so filled out the form with that information. As of Noon on February 1st, the email account is still active.
I sent an email to someone I know who works for The Borg. They found a help page which says "Gmail is unable to participate in mediations involving third parties regarding impersonation." So much for security.
The fraudster is also using the "danny.conv" Skype account. I sent a GPG signed email to Skype detailing the problem. That got an autoreply saying signature.asc doesn't have an allowed file name extension. Sigh. Then they replied that I should use the website to report abuse. So I did. To which they replied with steps on how to report about a hijacked account. Double sigh.
I finally got someone to say that people should use one's Skype client as follows. View the contacts list (or search for a new contact) select the problematic account and use the "block" option. In the resulting popup, check the "report abuse" checkbox and then submit the form. "If the person is also reported by other Skype users too, his account will be automatically blocked once the number of reports reaches the limit."
So Skype deflects responsibility, but at least they have something.
For the record, I don't have a Gmail account and never will. My email is "danielc@analysisandsolutions.com". My Skype user name is "danielconvissor". If you're ever in doubt, my GPG key is 8FFE1FFC.
Exuberant Ctags is a lovely, obscure tool developers use to index source code. Unfortunately, the project went dormant after 2009. Thankfully, Masatake Yamato resuscitated the project as Universal Ctags in 2014.
Since then there have been well over 1,000 commits from several significant contributors. There's even a pull request open at the moment to bring in support for PHP 7.
Before installing the shiny new ctags, one needs to expunge the existing installation. If you installed it from source, as discussed in an earlier blog post, "walk this way:"
cd <path to where you keep source code>/ctags sudo make uninstall
But if you have it installed via a packge, here's one example of how to "put the candle back:"
sudo apt-get remove exuberant-ctags
"Give my creature liiiife!"
cd <path to where you keep source code> git clone https://github.com/universal-ctags/ctags.git universal-ctags cd universal-ctags ./autogen.sh ./configure make sudo make install
"Oh, sweet mystery of life at last I've found you..." From my quick examination, things are working perfectly with Effortless Ctags with Git and Ctags for SVN.
I've been using StartSSL for low level, free TLS certificates for a few years. Their interface and processes are a bit clunky (though I see they're coming out with a new website this weekend). More problematically, they wouldn't renew a free cert for my community garden's domain because the site has a link for making donations via PayPal.
I recently heard of Let's Encrypt, a new, open source, free certificate authority. It's got big name backers like Mozilla, EFF, Automatic and many more. The API system allows users to easilty automoate the process. They went into public beta mode earlier in the month.
One of my TLS certificates was coming up for renewal, so I figured it was worth a shot. I'm glad I did.
I ran into a few roadblocks using the "apache" plugin, so I used the "webroot" plugin which is slightly less automagical. After using it, I realized a little shell script would help simplify the process. Also, Let's Encrypt is pretty lax with file permissions (I was able to read the private keys from my regular user account!) so my instructions and shell script lock things down.
All shell commands in this tutorial assume you're running as root. So start off by making that so, then installing my shell script.
sudo -i mkdir -p -m 755 /usr/local/src cd /usr/local/src git clone https://github.com/convissor/call_letsencrypt cd call_letsencrypt chmod 744 call_letsencrypt.sh # Edit the "email" variable in the script. # Use whatever editor you want. As you see, I use vim. vim call_letsencrypt.sh git commit -am 'My settings' cd /usr/local/sbin ln -s /usr/local/src/call_letsencrypt/call_letsencrypt.sh call_letsencrypt.sh
Next, ensure regular users can't get at the data, then install the Let's Encrypt scripts in /root
mkdir -m 700 /etc/letsencrypt cd git clone https://github.com/letsencrypt/letsencrypt
Execute my script for the core domain names and tighten up permissions some more.
call_letsencrypt.sh www.analysisandsolutions.com analysisandsolutions.com find /etc/letsencrypt -type d -exec chmod 700 {} \;
SSLCertificateFile /etc/letsencrypt/live/www.analysisandsolutions.com/cert.pem SSLCertificateChainFile /etc/letsencrypt/live/www.analysisandsolutions.com/chain.pem SSLCertificateKeyFile /etc/letsencrypt/live/www.analysisandsolutions.com/privkey.pem
smtpd_tls_cert_file = /etc/letsencrypt/live/www.analysisandsolutions.com/cert.pem smtpd_tls_key_file = /etc/letsencrypt/live/www.analysisandsolutions.com/privkey.pem
ssl_cert = </etc/letsencrypt/live/www.analysisandsolutions.com/cert.pem ssl_key = </etc/letsencrypt/live/www.analysisandsolutions.com/privkey.pem
service apache2 reload service dovecot reload service postfix reload
Certificates from Let's Encrypt expire in 90 days. Renewing them is as easy as adding a cron job that gets called every other month. So call crontab -e and put the following in there.
5 4 3 */2 * /usr/local/sbin/call_letsencrypt.sh -adp www.analysisandsolutions.com analysisandsolutions.com
As you may know, I'm between jobs at the moment. When the last gig ended, COBRA coverage for health insurance was offered. I considered getting insurance via New York's health insurance exhange, New York State of Health. But a cursory look at their website and the plethora of insurance plans available was daunting. I had bigger fish to fry at that moment, so I bit the bullet and went with the COBRA plan, despite it costing $1,300.
In late November, I got the open enrollement / renewal information for the COBRA coverage. It's going up to $1,500 / month -- this for a plan with high deductables and out of pocket maximums. Uhhh, thanks, but no thanks. So off to the exchange website I go.
As a fellow web developer, you know how things should work. And you know that feeling you have upon using a site that sucks. NY State of Health gave me sinking, frustrating, and WTF?! feelings over and over again. I got to the point of saying "there's got to be a better way."
Fortunately, the site uses jQuery, which I used extensively in my last job. So I whipped up a bookmarklet to drastically simplify downloading the insurance plan data. Throw in a couple of little shell scripts to extract the data into CSV files. As icing, I added some tips about deadlines, etc. Then I made it all available on Github. (All hail Github.)
https://github.com/convissor/new_york_state_of_health_spreadsheet
Enjoy.
A recent On the Media segment was a good reminder to think skeptically about what you're presented1 in the news and when interacting with websites.
The story covered a techincal analysis of the Ashley Madison disclosures. Annalee Newitz, then Editor in Chief of Gizmodo, (plus her colleagues and collaborators) examined Ashley Madison's documents, database, and source code.
Turns out that only 5% of the users are women. To fill the void (well, their coffers, really), the folks at Avid Life Media used fake female accounts, made chat bots and paid for "affiliates" to chat and even go out with the men. "As documents from company e-mails now reveal, 80 percent of first purchases on Ashley Madison were a result of a man trying to contact a bot, or reading a message from one."
The interview concluded with some solid insights on programming, UX, and human behavior.
Annalee Newitz: I was not interested in looking for specific names in the database. I wanted to look at it wholistically, as opposed to leaping to conclusions about larger issues of men and women and how they behave. You can't really reach those conclusions until you understand how the site works.
There's a logic underlying the code that tries to encourage certain certain kinds of behavior. You're being led by code, and you're being boxed in by code. And so, without understanding that you don't know why people are pushing which buttons.
Brooke Gladstone, Host: So it doesn't really reveal anything about how many men have affairs and why.
Annalee Newitz: All it reveals is that if you target straight men who want to have an affair, as a market, you can find them. It was really aimed at male fantasies. So it doesn't even tell us anything about real behavior. It just tells us about a fantasy that we already knew existed, and has existed for as long as we've had marriage.
1. Well, that can be said of pretty much every OTM story. :)
My first grader's teacher gives students the option of doing spelling homework on a computer once per week. When my daughter does so, it's on my computer, where I'm teaching her to write using Vim.
I'm curious which she'll be self-sufficient on first, Vim or a bicycle. Either way, she'll end up with two skills that will last her a lifetime.
The F1-F12 function keys and the ESC key are essential parts of a balanced diet for folks, like myself, who keyboard navigate their computers. When I started using my Lenovo X250 for the first time, it gave me a serious bout of indigestion.
Traditionally, Lenovo laptop keyboards have some keys serve a dual purpose. The secondary role is accessed by holding down the "Fn" key then pressing the key in question. Efficient. Tasteful. Well, it seems Lenovo let some kids into the kitchen. The primary purpose of the top row of keys has been converted to "multimedia function keys" and the actual F1-F12 function keys were demoted to the secondary behavior. Sigh. Kids these days...
Okay. They've added a lock mode to the "Fn" key. Let's try that. Wait, what!? Turning that on makes the ESC key become the "FnLk" key and turns the "End" key into "Insert"! Grrr... Pass me the calcium carbonate!
Fortunately, there's a BIOS setting that restores natural order. Here's how to get there.
Now if only the "Trackpad Disabled" BIOS setting worked... :/
Here's a funny one. I ran top the other day on a lightly used server and found MySQL pushing the CPU between 50% and 100%. "Am I under attack?" was my first thought. Oddly, SHOW PROCESSLIST was empty. "High CPU usage but no queries were running? WTF!"
Turns out MySQL's has a bug handling leap seconds. Clearing the condition was as simple as running date -s "`date`"
The workflow tools at my prior job were built around Subversion. When I started working there, I was sad to give up "Effortless Ctags with Git" and Fugitive. Porting those concepts was necessary to keep my sanity. Itchy, meet Scratchy: Ctags for SVN. A small tweak to our workflow tools got it call ctags_for_svn.sh when appropriate.
I also made a symlink called cs in my bin directory pointing to the ctags_for_svn.sh script. So updating a checkout's ctags is as simple as typing cs.
The concepts in Tim's Effortless Ctags blog post use Git's init.templatedir setting. Git added the feature in the 1.7.1 release. When I first read the post, my box was running an earlier version. If you're in this boat, the simplest workaround is to putting the hooks into Git's system wide template dir, /usr/share/git-core/templates.
Other alternatives, in case you need them, are either setting the $GIT_TEMPLATE_DIR environment variable or manually specifying the template dir on the command line: git init --template ~/.git_template
By default, the ctags parser tracks where variables are declared. So if your code follows the standard of naming object variables for the class they're instantiated from, hitting CTRL-] on a class name means you'll get brought to the tag-menulist instead of going directly to the desired class. And if it's a popular name, that means wading through pages of chafe. Plus it bloats the tags file.
Disabling this sillyness is as simple as passing the --php-kinds=-v argument to the ctags command.
A bug in Exuberant Ctags 5.8 causes comments in PHP files to be parsed. When comments contains the word function, the subsequent word is parsed as the name of a function. This can wind up filling the Tag List view with meaningless entries. Below is a demonstration of the problem and instructions on how to get the fix by installing ctags from source.
First, let's see the problem. Copy this first code block into a script named example.php:
<?php /** * ctags 5.8 parses this function incorrectly by not * ignoring the word function inside comments */ function real_function() {}
Install the default ctags package available at the moment, 5.8:
sudo apt-get install exuberant-ctags
And parse the file:
ctags -f ./tags example.php
This screen shot shows incorrectly and inside in the tag list on the left:
Fortunately, one day after 5.8 was released, jafl committed revision 729 to fix the problem. Here's how to obtain that on your system:
UPDATE: I just learned of the Universal Ctags project, an actively maintained fork of the Exuberant Ctags code base. Use these instructions to install ctags. Skip the following code block, which is only here for historical purposes.
# See UPDATE note, above. # DON'T EXECUTE THE COMMANDS IN THIS BLOCK # Get rid of 5.8. sudo apt-get remove exuberant-ctags # autoconf is needed to assemble the configure files sudo apt-get install autoconf autoconf-doc svn checkout svn://svn.code.sf.net/p/ctags/code/trunk/ ctags cd ctags autoconf ./configure make sudo make install
Time to re-parse the file using the repaired version of ctags:
ctags -f ./tags example.php
Voila! The only thing in the tag list is real_function.
A few years ago, I bit the bullet and started using Vim, cold turkey. It's been rewarding. My first step was forking Andrei Zmievski's vim-settings repository.
One very handy thing in Andrei's setup is Yegappan Lakshmanan's Tag List plugin. To that I added Tim Pope's Fugitive plugin. Combining those together with installing Darren Hiebert's Exuberant Ctags and the Git hook concepts in Señor Pope's "Effortless Ctags with Git" moves Vim from a good source code editor to an excellent one.
You can jump from where a function is used to where it is declared by hitting CTRL-]. Hitting F5 produces a pane on the left listing functions in the current file. Tab completion is available. Tag lists are automatically updated when git checkouts, pulls or commits are done.
But, there were a handful of annoying shortcomings. I'll address the bigger issues in separate blog posts. Below are resolutions for the small stuff...
The default process of jumping to a tag opens the file in a new buffer in the current window. I have yet to learn to use The Force, so find tabs easier to use. Therefore, I adjust the tag jump to open a new tab:
nnoremap <C-]> :tab tjump <C-r><C-w><CR>
It seems the syntax highlighter is inefficient or buggy. Large PHP files make vim very slow to respond. An easy fix is to disable syntax highlighting in big files:
au BufReadPost * if getfsize(bufname("%")) > 102400 | set syntax= | endif
Maybe it's just me, but the default colors of MatchParen are very simliar to some colors used by syntax highlighting. This made it difficult to figure out what's what. Tweaking the colors is pretty simple:
highlight MatchParen ctermbg=Blue ctermfg=White
I tend to write more PHP and text than HTML. So having matchpairs add a > everytime I type a < got annoying pretty fast. Placing the following to .vimrc increased my glee:
autocmd BufRead * set matchpairs=(:),{:},[:]
Part of what my Ubuntu Laptop Installation script does is put all of these components in place.
I'll be one of the presenters at WordCamp NYC this year. My talk is "Unit Testing WordPress Plugins with PHPUnit," taking place on Saturday, August 2 at 3:30.
Another talk I want to see is "Accessibility Beyond Coding: It’s Everyone’s Responsibility," an issue near and dear to my heart. Alas, it's at the same time as my session.
The whole conference runs from Friday, 8/1 through Sunday, 8/3, so come on down to Brooklyn.
This fourth posting in my WordPress plugin / PHPUnit testing series explains how I verify information inserted into database records with auto-increment IDs in my Object Oriented Plugin Template Solution.
In order for this post to be a be a stand-alone entity, I need to re-explain two terms from the earlier posts. First is "parent class." It holds helper properties and methods for use by multiple test classes.
abstract class TestCase extends PHPUnit_Framework_TestCase {}
The second term is "test class." It extends the "parent class" and contain the test methods that PHPUnit will execute.
class LoginTest extends TestCase {}
Naturally, if we're testing database records, we need a table to put them
in. Below is the table definition from the activate()
method
in the plugin's admin class.
CREATE TABLE `$this->table_login` ( login_id BIGINT(20) NOT NULL AUTO_INCREMENT, user_login VARCHAR(60) NOT NULL DEFAULT '', date_login TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (login_id), KEY user_login (user_login(5)) )
The real work happens in a method in my "parent class." It uses
WordPress' $wpdb->insert_id in the WHERE
clause of a
query against my table. I then make sure the data matches expectations.
Checking the character data is straight ahead via
assertEquals()
.
Verifying the time stamp is a bit trickier. Since the database server may
be using a different time zone than the computer running the tests, it is
necessary to get the current time from the database, done here via
SYSDATE()
. I then compare the inserted time against the current
time using PHP's DateTime::diff()
.
protected function check_login_record($user_name) { global $wpdb; $this->assertInternalType('integer', $wpdb->insert_id, 'This should be an insert id.'); $sql = 'SELECT *, SYSDATE() AS sysdate FROM `' . self::$o->table_login . '` WHERE login_id = %d'; $actual = $wpdb->get_row($wpdb->prepare($sql, $wpdb->insert_id)); if (!$actual) { $this->fail('Could not find the record in the "login" table.'); } $this->assertEquals($user_name, $actual->user_login, "'user_name' field mismatch."); $date_login = new DateTime($actual->date_login); // Keep tests from going fatal under PHP 5.2. if (method_exists($date_login, 'diff')) { $sysdate = new DateTime($actual->sysdate); $interval = $date_login->diff($sysdate); $this->assertLessThanOrEqual('00000000000001', $interval->format('%Y%M%D%H%I%S'), "'date_login' field off by over 1 second: $actual->date_login."); } }
Now let's utilize that stuff in a test in the "test class."
public function test_insert_login() { self::$o->insert_login($this->user_name); $this->check_login_record($this->user_name); }
That's All Folks. I'm curious what people think of this series. Feel free to submit comments via the form on the post's permalinked page. The full framework, can be found in the Object Oriented Plugin Template Solution.
This third post in my series on testing
WordPress plugins with
PHPUnit
will cover the real world dilemma I ran into combining wp_logout()
,
wp_redirect()
, and continuing a given test's execution after PHP
produces expected errors.
When a PHP error/warning/whatever is encountered inside a test, PHPUnit
throws an exception, immediately stopping execution of that test. It shows the
problem in errors section of the results once all tests have completed. But
what if you want to test the fact
that PHP is supposed to produce an error? PHPUnit lets you do that with
the setExpectedException('PHPUnit_Framework_Error', 'some message')
method or with docblock annotations. It's great stuff.
But there's one big limitation. What I said about PHPUnit "immediately stopping execution of that test" still applies. Any code after the PHP error will not be run (and it's easy to never even realize it). Here's an example.
/** * @expectedException PHPUnit_Framework_Error * @expectedExceptionMessage Notice: Undefined variable: nada */ public function test_error() { echo "$nada\n"; // Execution stops above. The rest of this stuff will not be run. $actual = some_procedure(); $this->assertFalse($actual); }
That test will pass, even if there's a problem with the return from
some_procedure()
. Don't despair! This post explains how to deal
with the problem.
Some of the following concepts are similar to those discussed for
testing wp_mail()
.
Please bear any slight repetition.
First, a bit of background. In this post, when I say "parent class," I mean the class that holds helper properties and methods that all test classes can reference:
abstract class TestCase extends PHPUnit_Framework_TestCase {}
And when I say "test class," I mean the class containing the test methods that PHPUnit will execute:
class LoginTest extends TestCase {}
Monitoring wp_redirect()
itself is pretty easy. WordPress lets
users override the one built into the core. All users have to do is declare
it before WP does. My version calls a static method in the
"parent class."
function wp_redirect($location, $status = 302) { TestCase::wp_redirect($location, $status); }
The redirect method in the "parent class" writes the location to a
property for later comparison. But before doing that, it makes sure that
wp_redirect()
was called at an expected time by checking that the
$location_expected property has been set.
public static function wp_redirect($location, $status) { if (!self::$location_expected) { throw new Exception('wp_redirect() called at unexpected time' . ' ($location_expected was not set).'); } self::$location_actual = $location; }
Here's the plugin's method we'll be testing. It logs the user out and redirects them to the password reset page.
protected function force_retrieve_pw() { wp_logout(); wp_redirect(wp_login_url() . '?action=retrievepassword'); }
The hitch is wp_logout()
calls setcookie()
, which
sends headers, but PHPUnit has already generated some output, meaning (you
guessed it) PHP will produce a warning: Cannot modify header information
- headers already sent by (output started at
PHPUnit/Util/Printer.php:172). That means I won't be able to make sure
the redirect worked because PHPUnit will stop execution when that warning is
thrown.
Thought bubble... I just realized
wp_logout()
andwp_redirect()
are "pluggable" too. So I could override them with methods that don't set cookies and even makes sure the function gets called when I want it to. But that would obviate the need for the cool part of this tutorial. So, uh, let's imagine I never said anything. (Eventually I'll modify the unit tests in the Login Security Solution.)
So, let's set up the error handlers we'll need in the "parent class." The first method is used by the tests to say an error will be coming. It stores the expected error messages in a property and sets an error handler for PHP.
protected function expected_errors($error_messages) { $this->expected_error_list = (array) $error_messages; set_error_handler(array(&$this, 'expected_errors_handler')); }
When PHP produces an error, my error handler makes sure the message is one we anticipated. If so, the $expected_errors_found property is set to true so we can check it later.
public function expected_errors_handler($errno, $errstr) { foreach ($this->expected_error_list as $expect) { if (strpos($errstr, $expect) !== false) { $this->expected_errors_found = true; return true; } } return false; }
Then there is the method that tests can call to make sure the expected PHP error actually happened and to pull my error handler off of the stack.
protected function were_expected_errors_found() { restore_error_handler(); return $this->expected_errors_found; }
Finally! Here's the test for PHPUnit to execute.
public function test_redirect() { // Establish the error handler with the expected message. $expected_error = 'Cannot modify header information'; $this->expected_errors($expected_error); // Let the customized wp_redirect() know it's okay to be called now. self::$location_expected = wp_login_url() . '?action=retrievepassword'; // Call the plugin method we want to test. self::$o->force_retrieve_pw(); // Make sure the error happened. $this->assertTrue($this->were_expected_errors_found(), "Expected error not found: '$expected_error'"); // Check that the redirect URI matches the expected one. $this->assertEquals(self::$location_expected, self::$location_actual, 'wp_redirect() produced unexpected location header.'); }
Well, we made it through. If this was helpful, buy me a beer some time. After figuring this out in the first place and pulling this post together, I can sure use one.
PHPUnit is
a great way to improve and maintain application quality. This post
summarizes how I use PHPUnit to test the behavior of
WordPress' wp_mail()
function in the
Object Oriented Plugin Template Solution.
Before getting into the nitty gritty, let me explain two terms I'll be using. First is "parent class." It holds helper properties and methods for use by multiple test classes.
abstract class TestCase extends PHPUnit_Framework_TestCase {}
The second term is "test class." It extends the "parent class" and contain the test methods that PHPUnit will execute.
class LoginTest extends TestCase {}
Okay, now on with the show...
The wp_mail()
function is "pluggable." Site owners
can override such functions (because WordPress declares their version of a
given function only if it hasn't been implemented yet). So my test framework
takes advantage of this by declaring one first. All my version does is call a
static method in the "parent class."
function wp_mail($to, $subject, $message) { TestCase::mail_to_file($to, $subject, $message); }
My "mail" method in the "parent class" writes the message contents to a temporary file for later comparison. But before doing that, it makes sure that the mail function is actually being called at an expected time by checking that the $mail_file_basename propery has been set.
public static function mail_to_file($to, $subject, $message) { if (!self::$mail_file_basename) { throw new Exception('wp_mail() called at unexpected time' . ' (mail_file_basename was not set).'); } if (!self::$temp_dir) { self::$temp_dir = sys_get_temp_dir(); } // Keep Windows happy by replacing :: in method names with --. $basename = str_replace('::', '--', self::$mail_file_basename); self::$mail_file = self::$temp_dir . '/' . $basename; $contents = 'To: ' . implode(', ', (array) $to) . "\n" . "Subject: $subject\n\n$message"; return file_put_contents(self::$mail_file, $contents, FILE_APPEND); }
Now we need to create the file containing the expected message. The files
are generally named for the test method where the mail is being triggered from,
using the format produced by the __METHOD__
magic constant but with
::
replaced with --
to keep Windows from freaking
out. For this example it's LoginTest--test_notify_login
.
Since the text of the actual messages can be composed with translated
strings, we need to provide separate expected message files for each supported
translation. The files for a given translation go into a subdirectory of
tests/expected
named for the
WPLANG
to be tested. The default directory is tests/expected/en_US
.
Here's the expected file for this example, using format placeholders for strings that can vary in different environments:
To: %a Subject: howdy %s just logged in to %s.
The actual test in the "test class" is pretty straight forward. The comments explain what's going on.
public function test_notify_login() { // Set the name of the file holding the expected output. self::$mail_file_basename = __METHOD__; // Call the method that generates the mail. (See declaration, below.) self::$o->notify_login($this->user_name); // Compare the generated message to the expected one. (See below.) $this->check_mail_file(); }
The plugin's method that generates the email:
protected function notify_login($user_name) { $to = get_site_option('admin_email'); $subject = 'howdy'; $blog = get_option('blogname'); // __() passes the string though the translation process. $message = sprintf(__("%s just logged in to %s.", self::ID), $user_name, $blog) . "\n"; return wp_mail($to, $subject, $message); }
Back in the "parent class," there is a method for comparing the
files containing the expected and actual messages. The first thing it does
is check that wp_mail()
was really executed.
protected function check_mail_file() { // Ensure the actual output was generated. if (!self::$mail_file) { $this->fail('wp_mail() has not been called.'); } $basedir = dirname(__FILE__) . '/expected/'; $locale = get_locale(); if (!file_exists("$basedir/$locale")) { $locale = 'en_US'; } // Placate Microsoft. $basename = str_replace('::', '--', self::$mail_file_basename); // Use PHPUnit's file diff method. $this->assertStringMatchesFormatFile( "$basedir/$locale/$basename", file_get_contents(self::$mail_file) ); }
If everything worked as planned, assertStringMatchesFormatFile()
will tell PHPUnit that the test passed. If not, it'll produce a diff between
the expected and actual output. Do note, there's
a bug in
PHPUnit 3.6 that shows all lines as having a diff, not just the
ones that actually have a diff. This is fixed in PHPUnit 3.7.
Well, that's it. Let me know if this was helpful. To examine the full framework, download the Object Oriented Plugin Template Solution from WordPress' plugin directory.
Creating the Login Security Solution taught me a lot about designing clean, solid WordPress plugins. It seemed like a good idea to distill those concepts into a skeleton that other developers can use for creating or augmenting their own plugins. So please raise a toast for the Object Oriented Plugin Template Solution. Now I'll throw some more logs under the boiler to extract a few essences into some quick tutorials.
The initial posts will cover unit testing WordPress plugins with PHPUnit. This first article shows how to work around the conflict between WordPress' heavy reliance on globally scoped variables (groan) and PHPUnit's squashing of global variables. This mayhem results in several errors:
Place this at the top of the script that sets up the environment:
global $current_blog, $current_site, $wp_rewrite, $wpdb; $_SERVER['HTTP_HOST'] = 'localhost';
Add this property to your parent test case class that extends
PHPUnit_Framework_TestCase
:
protected $backupGlobals = false;
Now require_once
the wp-load.php
file and you're good to go.
To examine the full framework, download the Object Oriented Plugin Template Solution from WordPress' plugin directory.
Back in 2008, Chris Shiflett said I should should start a blog. He was right. But I'm more of a doer than a talker and I didn't feel I had anything compelling to say.
During past several months of "doing" I've come across and solved some interesting, common problems. Explaining the solutions can help other developers avoid the conundrums I faced. Now I feel compelled to talk.