Page 2 of 3

Posted: Wed Aug 16, 2006 6:42 pm
by Weirdan
Chris, I have another idea :)

Basically, the essence of your troubles is that your border classes (Swift/Connection/*) are not encapsulated enough. They are exposing writeHook/readHook (which are native PHP stream resources) to outside world. If you could abstract the access to these streams, you would be able to mock them (and set expectations on them) easily.
Which leads us to another idea: abstract bidirectional stream. After all, all of your communications with MTA are done via some sort of bidi stream (be it socket or pair of process pipes). In the scheme I envision the connection plugins would be no longer responsible for actual communication with a MTA. They would get more like a factories which know how to create appropriate stream. On the other side, it's possible to keep all the transport related functionality inside the plugin and make the stream object to act as a proxy to it (Alger calls it 'facet of functionality').

Testing authenticators is somewhat simplier. For the most (except popB4smtp) it's as easy as to mock $this->base->command() (and set expectation on it, of course). With wrapped streams testing popB4smtp shouldn't be that hard either: you would need two mocks ($this->base->command() and stream mock that mimicks the access to POP server).

Just some random ideas I had while fixing toilet tank Image

Posted: Wed Aug 16, 2006 6:59 pm
by Chris Corbyn
~Weirdan, I like that idea :)

I'm moving along nicely with the end-to-end tests over real smtp at the moment and once I have those done I'll play with this for sure.

BTW... I was thinking of actually creating the resources myself for testing... I could swear for the life of me I've seen PHP functions to do this (not socket_create... something which only exists as a variable in the PHP script but it can be fwrite() and fread() from/to).

EDIT | viewtopic.php?p=298323#298323

EDIT 2 | Yes, I can make a testConnection class type thing with these stream functions (namely stream_wrapper_register() )

Posted: Wed Aug 16, 2006 7:35 pm
by Weirdan
while you could go with PHP stream wrappers that's not the idea I had ;) I was thinking along the lines of:

Code: Select all

class BiDiStream {
   private $in;
   private $out;
   public function __construct($in, $out) {
       $this->in = $in;
       $this->out = $out;
   }
   public function send($data) {
       fwrite($this->out, $data);
   } 
   public function recv($size = 4096) {
       return fread($this->in, $size);
   }
}
class popb4smtp {
   private $stream;
   public function __construct($stream) {
       $this->stream = $stream;
   }
   public function doTheWork() {
       $this->stream->send('datastring');
       return $this->stream->recv() == 'OK';
   }
}
require 'SimpleTest/unit_tester.php';
require 'SimpleTest/reporter.php';
require 'SimpleTest/mock_objects.php';
Mock::generate('BiDiStream');
class TestOfSend extends UnitTestCase {
   function testOKResponse() {
      // mock setup
      $stream = new MockBiDiStream($this);
      $stream->setReturnValue('send', true);
      $stream->setReturnValue('recv', 'OK');
      $stream->expectOnce('send', array('datastring'));
      $stream->expectOnce('recv', array());
      
      // authenticator setup
      $auth = new popb4smtp($stream);

      // test
      $this->assertTrue($auth->doTheWork());

      // tally
      $stream->tally();
   }
}

Posted: Wed Aug 16, 2006 7:43 pm
by Weirdan
The idea here is to create border class as simple as possible (to be able to not test it at all). If all of your communication with the outside world is performed via this class you may mock the 'world' by mocking this class.

Posted: Thu Aug 17, 2006 5:23 am
by Chris Corbyn
Oh oh.... Looking at your code and I think the whole concept of mocks is starting to make sense.

Posted: Thu Aug 17, 2006 7:07 am
by sweatje
Just a quick note. Passing $this to the mock constructor, and the $mock->tally() methods are both unnescesary with recent versions of simpletest.

Code: Select all

function testOKResponse() {
      // mock setup
      $stream = new MockBiDiStream;
      $stream->setReturnValue('send', true);
      $stream->setReturnValue('recv', 'OK');
      $stream->expectOnce('send', array('datastring'));
      $stream->expectOnce('recv', array());
     
      // authenticator setup
      $auth = new popb4smtp($stream);

      // test
      $this->assertTrue($auth->doTheWork());
  }

Posted: Thu Aug 17, 2006 7:33 am
by Chris Corbyn
Looking further at the mock example you gave, it would require some re-writing of the connection classes and of swift itself to works since swift does the writing and reading. I should really pull out the actual calls to fwrite() etc and move them into the connections to provide a little more flexibility but I'm going nowhere near a change like that until Swift makes a majot version jump again, and I'm reluctant to do that just yet.

So... I think what I'll do is I'll create a really basic stream wrapper, use that to handle the abstraction you're using here and then mock it to make it do what it should under certain scenarios. Mocks seem cool... can't believe I never got the point of them before.

I'll still need a true end-to-end test however, which can also double as smoke test for the user to compare with some images on my server.

Here's what I'm going to test:

* The SMTP connection class itself (just the basis such as making sure *can* write, and it *does* get a repsonse)
* The sendmail connection class (same as above)
* Swift, various fine-grained aspects using a mock connection
* Full swift broad-scoped end-to-end tests (doubling as smoke tests)

I already have the smoke tests working for sendmail and/or smtp depending upon what the user is running but I need to set up some images to compare against. Characters sets could cause a problem since I have a few smoke tests which ensure the encoding is correctly detected but they use Russain/Greek fonts which the user may not have - tough... I'll stick it in a README.

Posted: Thu Aug 17, 2006 7:35 am
by Ambush Commander
but I'm going nowhere near a change like that until Swift makes a majot version jump again, and I'm reluctant to do that just yet.
You can always branch the trunk, do the major changes there, then merge them back in.
but I'm going nowhere near a change like that until Swift makes a majot version jump again, and I'm reluctant to do that just yet.
Well, some of the W3C compatibility tests require some weird font that almost nobody has. It's not that unreasonable a requirement.

Posted: Thu Aug 17, 2006 10:36 am
by Chris Corbyn
Here's how I'm abstracting the streams for now (unfinished). It's a bit like what Weirdan demonstrated. I could Mock the stream and then register the mock but that seems a bit pointless when the stream was already written to be manipulated in this way.

Code: Select all

<?php

require_once '../config.php';
require_once SIMPLETEST_BASE . '/unit_tester.php';
require_once SIMPLETEST_BASE . '/reporter.php';

class FakeStreamCommandProcessor
{
	static private $instance = null;
	
	public $command;
	public $response = "";
	
	public $isOpen = false;
	
	private function __construct() {}
	
	public function getGreeting()
	{
		return "220 fakestream.spoofed ESMTP FakeStream 0.0.0 ".date('r');
	}
	
	public function getBadCommand()
	{
		return "500 bad command";
	}
	
	public function getEHLO()
	{
		return "250-fakestream.spoofed Hello localhost.localdomain [127.0.0.1]".
			"\r\n250-PIPELINING".
			"\r\n250 AUTH LOGIN PLAIN CRAM-MD5";
	}
	
	public function getOK()
	{
		return "250 OK";
	}
	
	public function getDataGoAhead()
	{
		return "354 Go Ahead";
	}
	
	static public function getInstance()
	{
		if (self::$instance === null) self::$instance = new FakeStreamCommandProcessor();
		return self::$instance;
	}
	
	public function setCommand($command)
	{
		$this->command = $command;
	}
	
	public function setResponse($response)
	{
		$this->response .= $response."\r\n";
	}
	
	public function getResponse($size)
	{
		$ret = substr($this->response, 0, $size);
		
		$this->response = substr($this->response, $size);
		
		//Fake SMTP's behaviour of hanging past EOF
		if (!strlen($ret)) $this->hanging = true;
		else $this->hanging = false;
		
		return $ret;
	}
	
	//SMTP hangs if you try to read past EOF... we don't want that in our tests
	// but we do want to test *for* it
	public function isHanging()
	{
		return $this->hanging;
	}
}

class FakeStream
{
	private $processor;
	
	public function stream_open($path, $mode, $options, &$opened_path)
	{
		$this->processor = FakeStreamCommandProcessor::getInstance();
		$this->processor->isOpen = true;
		return $this->processor->isOpen;
	}
	
	public function stream_read($size)
	{
		return $this->processor->getResponse($size);
	}
	
	public function stream_write($string)
	{
		if ($this->processor->isOpen)
		{
			$this->processor->setCommand($string);
			return strlen($string);
		}
		else return 0;
	}
	
	public function stream_eof()
	{
		//Does nothing... SMTP doesn't implement it
		// It's vital we can work this out ourselves
	}
}

stream_wrapper_register('fake', 'FakeStream');

class TestOfFakeStream extends UnitTestCase
{
	private $stream;
	private $processor;
	
	public function setup()
	{
		$this->processor = FakeStreamCommandProcessor::getInstance();
	}
	
	public function testCreatingStream()
	{
		$this->assertFalse($this->processor->isOpen);
		$this->stream = fopen('fake://esmtp', 'w+');
		$this->assertTrue($this->processor->isOpen);
		$this->assertTrue(is_resource($this->stream));
	}
	
	public function testReading()
	{
		$this->processor->setResponse("250 TESTING");
		$response = fgets($this->stream);
		$this->assertEqual($response, "250 TESTING\r\n");
	}
	
	public function testExceedEofHang()
	{
		$this->assertFalse($this->processor->isHanging());
		$response = fgets($this->stream);
		$this->assertTrue($this->processor->isHanging());
	}
	
	public function testWriting()
	{
		$written = fwrite($this->stream, "TEST WRITING\r\n");
		$this->assertTrue($written);
		$this->assertEqual($this->processor->command, "TEST WRITING\r\n");
	}
	
	public function testWritingResponse()
	{
		$written = fwrite($this->stream, $this->processor->getOK());
		$this->assertTrue($written);
		$this->assertEqual($this->processor->command, $this->processor->getOK());
	}
}

$test = new TestOfFakeStream();
$test->run(new HtmlReporter());

?>

Posted: Thu Aug 17, 2006 10:41 am
by Chris Corbyn
Ambush Commander wrote:
but I'm going nowhere near a change like that until Swift makes a majot version jump again, and I'm reluctant to do that just yet.
You can always branch the trunk, do the major changes there, then merge them back in.
but I'm going nowhere near a change like that until Swift makes a majot version jump again, and I'm reluctant to do that just yet.
Well, some of the W3C compatibility tests require some weird font that almost nobody has. It's not that unreasonable a requirement.
I will indeed do this sort of development under a new branch. There's no big urge to get it out there quickly though :)

Posted: Thu Aug 17, 2006 10:51 am
by Chris Corbyn
Just thinking what I'll do on top of that... since that only handles setting responses to commands etc, yet I need to work out what response to give based upon the input sent. I think I'll add an observer which deals with this. It'll just have a default set of responses to certain commands, that's all. Sound reasonable? I'm pretty much winging it here :?

EDIT | Reason I can't set the response in the test is that several commands may be run at certain times, and several responses issued. You wouldn't get time to set that up between method calls.

Posted: Thu Aug 17, 2006 12:12 pm
by Weirdan
You wouldn't get time to set that up between method calls.
I believe in SimpleTest you could set the sequence of return values (like, for the first call return 'asdf', for the second - 'zxcv'). Moreover, it should be possible to set return values based on inputs to functions... just read through the docs, it should definitely be there ;) I'm not quite sure if it's possible to expect certain sequence of calls though.

Posted: Thu Aug 17, 2006 12:45 pm
by Ambush Commander
Well, if the Mock Objects are deficient, you can always extend them and add the functionality you need.

Posted: Thu Aug 17, 2006 2:02 pm
by Chris Corbyn
Weirdan wrote:I'm not quite sure if it's possible to expect certain sequence of calls though.
This is the thing I'd need. It's sort of vital that things happen in the right order here and I also want to test that everything behaves correctly when things *do not* happen in the right order (i.e. it fails). The FakeConnection does not try to be clever and implement everything perfectly, it does just act as a stub but it is aware of the ordering of commands so tests *can* fail in swift. If it always sent the same sequence of commands it would be very difficult to make the tests fail even when swift is sending completely wrong stuff.

I'm testing all the components that are used in the tests though.

I've tidied things up a bit since my last snippet (moved those commands out of the I/O processor for a start!) but I'll get to a more complete stage and post what I have and see if you guys think it's a logical way to do things.
Ambush Commander wrote:Well, if the Mock Objects are deficient, you can always extend them and add the functionality you need.
True... I'm just struggling to find a clean way to make Mocks which are clver enough to detect invalid commands being sent. The FakeConnection uses observers (which can provide responses to the stream wrapper) so it's easy enough to change the tracking behaviour.

Posted: Thu Aug 17, 2006 4:23 pm
by Chris Corbyn
Update: (Sorry, this thread is beginning to turn into my little diary of events)

The stuff to fake a TCP socket connection:

Code: Select all

<?php

class FakeStream
{
	private $processor;
	
	public function stream_open($path, $mode, $options, &$opened_path)
	{
		$this->processor = FakeStream_CommandProcessor::getInstance();
		$this->processor->isOpen = true;
		return $this->processor->isOpen;
	}
	
	public function stream_read($size)
	{
		return $this->processor->getResponse($size);
	}
	
	public function stream_write($string)
	{
		if ($this->processor->isOpen)
		{
			$this->processor->setCommand($string);
			return strlen($string);
		}
		else return 0;
	}
	
	public function stream_eof()
	{
		//Does nothing... SMTP doesn't implement it
		// It's vital we can work this out ourselves
	}
	
	public function stream_close()
	{
		$this->processor->destroy();
	}
}

stream_wrapper_register('fake', 'FakeStream');

?>

Code: Select all

<?php

class FakeStream_CommandProcessor
{
	static private $instance = null;
	
	public $command;
	public $response = "";
	private $observers = array();
	
	public $isOpen = false;
	public $hanging = false;
	
	private function __construct() {}
	
	public function addObserver($observer)
	{
		$this->observers[] = $observer;
	}
	
	static public function getInstance()
	{
		if (self::$instance === null) self::$instance = new FakeStream_CommandProcessor();
		return self::$instance;
	}
	
	public function setCommand($command)
	{
		$this->command .= $command;
		foreach ($this->observers as $i => $o) $this->observers[$i]->command($this->command);
	}
	
	public function setResponse($response)
	{
		$this->response .= $response."\r\n";
	}
	
	public function getResponse($size)
	{
		$ret = substr($this->response, 0, $size);
		
		$this->response = substr($this->response, $size);
		
		//Fake SMTP's behaviour of hanging past EOF
		if (!strlen($ret)) $this->hanging = true;
		else $this->hanging = false;
		
		return $ret;
	}
	
	public function isHanging()
	{
		return $this->hanging;
	}
	
	public function destroy()
	{
		self::$instance = null;
		$this->isOpen = false;
	}
}

?>
The test for that Fake connection stuff

Code: Select all

<?php

require_once '../config.php';
require_once SIMPLETEST_BASE . '/unit_tester.php';
require_once SIMPLETEST_BASE . '/reporter.php';
require_once 'FakeStream.php';
require_once 'FakeStream/CommandProcessor.php';

class TestOfFakeStream extends UnitTestCase
{
	private $stream;
	private $processor;
	
	public function setup()
	{
		$this->processor = FakeStream_CommandProcessor::getInstance();
	}
	
	public function testCreatingStream()
	{
		$this->assertFalse($this->processor->isOpen);
		$this->stream = fopen('fake://esmtp', 'w+');
		$this->assertTrue($this->processor->isOpen);
		$this->assertTrue(is_resource($this->stream));
	}
	
	public function testReading()
	{
		$this->processor->setResponse("250 TESTING");
		$response = fgets($this->stream);
		$this->assertEqual($response, "250 TESTING\r\n");
	}
	
	public function testExceedEofHang()
	{
		$this->assertFalse($this->processor->isHanging());
		$response = fgets($this->stream);
		$this->assertTrue($this->processor->isHanging());
	}
	
	public function testWriting()
	{
		$written = fwrite($this->stream, "TEST WRITING\r\n");
		$this->assertTrue($written);
		$this->assertEqual($this->processor->command, "TEST WRITING\r\n");
	}
	
	public function testClosingStream()
	{
		$this->assertTrue(is_resource($this->stream));
		$this->assertTrue($this->processor->isOpen);
		fclose($this->stream);
		$this->assertFalse($this->processor->isOpen);
		$this->assertFalse(is_resource($this->stream));
	}
}

$test = new TestOfFakeStream();
$test->run(new HtmlReporter());

?>

The observer that makes it SMTP-like

Code: Select all

<?php

class SmtpMsg
{
	static public function greeting()
	{
		return "220 fakestream.spoofed ESMTP FakeStream 0.0.0 ".date('r', strtotime('20060817'));
	}
	
	static public function badCommand()
	{
		return "500 bad command";
	}
	
	static public function EHLO()
	{
		return "250-fakestream.spoofed Hello localhost.localdomain [127.0.0.1]".
			"\r\n250-PIPELINING".
			"\r\n250 AUTH LOGIN PLAIN CRAM-MD5";
	}
	
	static public function OK()
	{
		return "250 OK";
	}
	
	static public function dataGoAhead()
	{
		return "354 Go Ahead";
	}
}

class FakeStream_SmtpStub
{
	private $stream;
	private $ending = "\r\n";
	private $validCommandSent = false;
	private $rcptSent = false;
	private $mailSent = false;
	private $dataSent = false;
	
	public function __construct($stream)
	{
		$this->stream = $stream;
		$this->stream->setResponse($this->getGreeting());
	}
	
	public function getGreeting()
	{
		$this->validCommandSent = false;
		return SmtpMsg::greeting();
	}
	
	public function getBadCommand()
	{
		return SmtpMsg::badCommand();
	}
	
	public function getEHLO()
	{
		if (!$this->validCommandSent)
		{
			$this->validCommandSent = true;
			return SmtpMsg::EHLO();
		}
		else return $this->getBadCommand();
	}
	
	public function getOK()
	{
		$this->validCommandSent = true;
		return SmtpMsg::OK();
	}
	
	public function getMAIL()
	{
		if ($this->validCommandSent && !$this->rcptSent && !$this->dataSent)
		{
			$dot_atom_re = '[-!#\$%&\'\*\+\/=\?\^_`{}\|~0-9A-Z]+(?:\.[-!#\$%&\'\*\+\/=\?\^_`{}\|~0-9A-Z]+)*';
			$implemented_domain_re = '[-0-9A-Z]+(?:\.[-0-9A-Z]+)*';
			$full_pattern = $dot_atom_re.'(?:@'.$implemented_domain_re.')?'; 
			
			if (preg_match("/^\s*mail\ +from:\s*(?:(<".$full_pattern.">)|(".$full_pattern.")|(<\ *>))\s*\r\n$/i",
			$this->stream->command))
			{
				$this->mailSent = true;
				return $this->getOK();
			}
			else return $this->getBadCommand();
		}
		else return $this->getBadCommand();
	}
	
	public function getRCPT()
	{
		if ($this->validCommandSent && $this->mailSent && !$this->dataSent)
		{
			$dot_atom_re = '[-!#\$%&\'\*\+\/=\?\^_`{}\|~0-9A-Z]+(?:\.[-!#\$%&\'\*\+\/=\?\^_`{}\|~0-9A-Z]+)*';
			$implemented_domain_re = '[-0-9A-Z]+(?:\.[-0-9A-Z]+)*';
			$full_pattern = $dot_atom_re.'(?:@'.$implemented_domain_re.')?'; 
			
			if (preg_match("/^\s*rcpt\ +to:\s*(?:(<".$full_pattern.">)|(".$full_pattern.")|(<\ *>))\s*\r\n$/i",
			$this->stream->command))
			{
				$this->rcptSent = true;
				return $this->getOK();
			}
			else return $this->getBadCommand();
		}
		else return $this->getBadCommand();
	}
	
	public function getQUIT()
	{
		$this->validCommandSent = false;
		$this->dataSent = false;
		$this->mailSent = false;
		$this->rcptSent = false;
		return SmtpMsg::OK();
	}
	
	public function getDataGoAhead()
	{
		if ($this->validCommandSent && $this->rcptSent && !$this->dataSent)
		{
			if (preg_match("/^\s*data\s*\r\n/i", $this->stream->command))
			{
				$this->validCommandSent = true;
				$this->dataSent = true;
				$this->mailSent = false;
				$this->rcptSent = false;
				$this->ending = "\r\n.\r\n";
				return SmtpMsg::dataGoAhead();
			}
			else return $this->getBadCommand();
		}
		else return $this->getBadCommand();
	}
	
	public function getMessageSent()
	{
		$this->ending = "\r\n";
		$this->dataSent = false;
		return SmtpMsg::OK();
	}
	
	public function command(&$string)
	{
		$this->stream->hanging = true;
		if (substr($string, -(strlen($this->ending))) != $this->ending) return;
		
		$keyword = @strtolower(preg_replace('/^\s*([A-Z]+)\s*.*\r\n$/i', '$1', $string));
		switch ($keyword)
		{
			case 'ehlo':
			$this->stream->setResponse($this->getEHLO());
			break;
			case 'mail':
			$this->stream->setResponse($this->getMAIL());
			break;
			case 'rcpt':
			$this->stream->setResponse($this->getRCPT());
			break;
			case 'data':
			$this->stream->setResponse($this->getDataGoAhead());
			break;
			case 'quit':
			$this->stream->setResponse($this->getQUIT());
			break;
			default:
			if ($this->dataSent) $this->stream->setResponse($this->getMessageSent());
			else $this->stream->setResponse($this->getBadCommand());
			break;
		}
		
		$string = "";
		$this->stream->hanging = false;
	}
}

?>
And the test for the observer:

Code: Select all

<?php

require_once '../config.php';
require_once SIMPLETEST_BASE . '/unit_tester.php';
require_once SIMPLETEST_BASE . '/reporter.php';
require_once 'FakeStream.php';
require_once 'FakeStream/CommandProcessor.php';
require_once 'FakeStream/SmtpStub.php';

class TestOfSmtpStub extends UnitTestCase
{
	private function readFullResponse($stream)
	{
		$ret = "";
		do
		{
			$line = fgets($stream);
			if ($line == "") return;
			$ret .= $line;
		} while (substr($line, 3, 1) != " ");
		return $ret;
	}
	
	public function testWaitingForEOL()
	{
		$stream = fopen('fake://esmtp', 'w+');
		$processor = FakeStream_CommandProcessor::getInstance();
		$processor->addObserver(new FakeStream_SmtpStub($processor));
		
		$this->assertFalse($processor->isHanging());
		fwrite($stream, "Command has no EOL");
		$this->assertTrue($processor->isHanging());
		$this->assertEqual($processor->command, "Command has no EOL");
		fwrite($stream, " yet\r\n");
		$this->assertFalse($processor->isHanging());
		//The command should have been flushed
		$this->assertEqual($processor->command, "");
		
		fclose($stream);
	}
	
	public function testValidSendSequence()
	{
		$stream = fopen('fake://esmtp', 'w+');
		$processor = FakeStream_CommandProcessor::getInstance();
		$processor->addObserver(new FakeStream_SmtpStub($processor));
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::greeting()."\r\n");
		
		//We can only say this once
		fwrite($stream, "EHLO localhost\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::EHLO()."\r\n");
		
		//We can get right down to DATA fine now
		fwrite($stream, "MAIL FROM: <foo@bar>\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::OK()."\r\n");
		
		//We can send these many times in succession
		fwrite($stream, "RCPT TO: <foo@bar>\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::OK()."\r\n");
		
		fwrite($stream, "RCPT TO: <foo@bar>\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::OK()."\r\n");
		
		//We can only send a string ending with \r\n.\r\n now
		fwrite($stream, "DATA\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::dataGoAhead()."\r\n");
		
		//Note! The correct behaviour is to keep waiting for the CRLF.CRLF
		fwrite($stream, "Test message line 1\r\n");
		$this->assertTrue($processor->isHanging());
		
		fwrite($stream, "Test message line 2\r\n.\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::OK()."\r\n");
		
		//We can send another message
		fwrite($stream, "MAIL FROM: <foo@bar>\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::OK()."\r\n");
		
		fwrite($stream, "RCPT TO: <foo@bar>\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::OK()."\r\n");
		
		fwrite($stream, "DATA\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::dataGoAhead()."\r\n");
		
		fwrite($stream, "Test message 2\r\n.\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::OK()."\r\n");
		
		fwrite($stream, "QUIT\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::OK()."\r\n");
		
		fclose($stream);
	}
	
	public function testInvalidSequence()
	{
		$stream = fopen('fake://esmtp', 'w+');
		$processor = FakeStream_CommandProcessor::getInstance();
		$processor->addObserver(new FakeStream_SmtpStub($processor));
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::greeting()."\r\n");
		
		//We can only say this once
		fwrite($stream, "EHLO localhost\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::EHLO()."\r\n");
		
		//This shouldn't work because we need MAIL FROM
		fwrite($stream, "RCPT TO: <foo@bar>\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::badCommand()."\r\n");
		
		//Even though it screamed at us before, it should forgive us now
		fwrite($stream, "MAIL FROM: <foo@bar>\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::OK()."\r\n");
		
		//We shouldn't be able to do this yet!
		fwrite($stream, "DATA\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::badCommand()."\r\n");
		
		//And the RCPT TO should work ok
		fwrite($stream, "RCPT TO: <foo@bar>\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::OK()."\r\n");
		
		//NOW, and only now should DATA work
		fwrite($stream, "DATA\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::dataGoAhead()."\r\n");
		
		fwrite($stream, "Test message 2\r\n.\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::OK()."\r\n");
		
		fwrite($stream, "QUIT\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::OK()."\r\n");
		
		//The connection would technically be closed so this should fail
		fwrite($stream, "MAIL FROM: <foo@bar>\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::badCommand()."\r\n");
		
		fclose($stream);
	}
	
	public function testInvalidSyntax()
	{
		$stream = fopen('fake://esmtp', 'w+');
		$processor = FakeStream_CommandProcessor::getInstance();
		$processor->addObserver(new FakeStream_SmtpStub($processor));
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::greeting()."\r\n");
		
		//We can only say this once
		fwrite($stream, "EHLO localhost\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::EHLO()."\r\n");
		
		//Too many >>>'s, should bail out!
		fwrite($stream, "MAIL FROM: <foo@bar>>>>\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::badCommand()."\r\n");
		
		//But it'll forgive us
		fwrite($stream, "MAIL FROM: <foo@bar>\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::OK()."\r\n");
		
		//And the RCPT TO should work ok
		fwrite($stream, "RCPT TO: <foo@bar>\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::OK()."\r\n");
		
		//This envelope will be rejected, but we can still send cos the first is OK
		fwrite($stream, "RCPT TO: <foo@&&&^bar>\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::badCommand()."\r\n");
		
		//This should work fine
		fwrite($stream, "DATA\r\n");
		$this->assertFalse($processor->isHanging());
		
		$response = $this->readFullResponse($stream);
		$this->assertEqual($response, SmtpMsg::dataGoAhead()."\r\n");
		
		//This shouldn't do anything
		fwrite($stream, "QUIT\r\n");
		$this->assertTrue($processor->isHanging());
		
		fclose($stream);
	}
}

$test = new TestOfSmtpStub();
$test->run(new HtmlReporter());

?>
All works well thank god, and despite it's length it's actually an extremely basic system. The only thing I haven't added (yet) is authentication.