diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..657ca2b
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,9 @@
+language: php
+
+php:
+ - 5.3
+ - 5.4
+ - 5.5
+ - 5.6
+
+script: phpunit tests
diff --git a/TwoFactorAuth.phpproj b/TwoFactorAuth.phpproj
index b91193c..c17df88 100644
--- a/TwoFactorAuth.phpproj
+++ b/TwoFactorAuth.phpproj
@@ -39,6 +39,7 @@
+
@@ -46,8 +47,10 @@
+
+
diff --git a/tests/TwoFactorAuthTest.php b/tests/TwoFactorAuthTest.php
new file mode 100644
index 0000000..36d409e
--- /dev/null
+++ b/tests/TwoFactorAuthTest.php
@@ -0,0 +1,180 @@
+assertEquals('543160', $tfa->getCode('VMR466AB62ZBOKHE', 1426847216));
+ $this->assertEquals('538532', $tfa->getCode('VMR466AB62ZBOKHE', 0));
+ }
+
+ /**
+ * @expectedException \RobThree\Auth\TwoFactorAuthException
+ */
+ public function testCreateSecretThrowsOnInsecureRNGProvider() {
+ $rng = new TestRNGProvider();
+
+ $tfa = new \RobThree\Auth\TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng);
+ $tfa->createSecret();
+ }
+
+ public function testCreateSecretOverrideSecureDoesNotThrowOnInsecureRNG() {
+ $rng = new TestRNGProvider();
+
+ $tfa = new \RobThree\Auth\TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng);
+ $this->assertEquals('ABCDEFGHIJKLMNOP', $tfa->createSecret(80, false));
+ }
+
+ public function testCreateSecretDoesNotThrowOnSecureRNGProvider() {
+ $rng = new TestRNGProvider(true);
+
+ $tfa = new \RobThree\Auth\TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng);
+ $this->assertEquals('ABCDEFGHIJKLMNOP', $tfa->createSecret());
+ }
+
+ public function testCreateSecretGeneratesDesiredAmountOfEntropy() {
+ $rng = new TestRNGProvider(true);
+
+ $tfa = new \RobThree\Auth\TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng);
+ $this->assertEquals('A', $tfa->createSecret(5));
+ $this->assertEquals('AB', $tfa->createSecret(6));
+ $this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ', $tfa->createSecret(128));
+ $this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', $tfa->createSecret(160));
+ $this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', $tfa->createSecret(320));
+ $this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567A', $tfa->createSecret(321));
+ }
+
+
+ public function testVerifyCodeWorksCorrectly() {
+
+ $tfa = new \RobThree\Auth\TwoFactorAuth('Test', 6, 30);
+ $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847190));
+ $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 29)); //Test discrepancy
+ $this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 30)); //Test discrepancy
+ $this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 - 1)); //Test discrepancy
+
+ $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 0)); //Test discrepancy
+ $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 35)); //Test discrepancy
+ $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 - 35)); //Test discrepancy
+
+ $this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 65)); //Test discrepancy
+ $this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 - 65)); //Test discrepancy
+
+ $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 + 65)); //Test discrepancy
+ $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 - 65)); //Test discrepancy
+ }
+
+ public function testTotpUriIsCorrect() {
+ $qr = new TestQrProvider();
+
+ $tfa = new \RobThree\Auth\TwoFactorAuth('Test&Issuer', 6, 30, 'sha1', $qr);
+ $data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE'));
+ $this->assertEquals('test/test', $data['mimetype']);
+ $this->assertEquals('base64', $data['encoding']);
+ $this->assertEquals('otpauth://totp/Test%26Label?secret=VMR466AB62ZBOKHE&issuer=Test%26Issuer&period=30&algorithm=SHA1&digits=6@200', $data['data']);
+ }
+
+ /**
+ * @expectedException \RobThree\Auth\TwoFactorAuthException
+ */
+ public function testGetQRCodeImageAsDataUriThrowsOnInvalidSize() {
+ $qr = new TestQrProvider();
+
+ $tfa = new \RobThree\Auth\TwoFactorAuth('Test', 6, 30, 'sha1', $qr);
+ $tfa->getQRCodeImageAsDataUri('Test', 'VMR466AB62ZBOKHE', 0);
+ }
+
+ private function DecodeDataUri($datauri) {
+ if (preg_match('/data:(?P[\w\.\-\/]+);(?P\w+),(?P.*)/', $datauri, $m) === 1) {
+ return array(
+ 'mimetype' => $m['mimetype'],
+ 'encoding' => $m['encoding'],
+ 'data' => base64_decode($m['data'])
+ );
+ }
+ return null;
+ }
+}
+
+class TestRNGProvider implements \RobThree\Auth\Providers\Rng\IRNGProvider {
+ private $isSecure;
+
+ function __construct($isSecure = false) {
+ $this->isSecure = $isSecure;
+ }
+
+ public function getRandomBytes($bytecount) {
+ $result = '';
+ for ($i=0; $i<$bytecount; $i++)
+ $result.=chr($i);
+ return $result;
+
+ }
+
+ public function isCryptographicallySecure() {
+ return $this->isSecure;
+ }
+}
+
+class TestQrProvider implements \RobThree\Auth\Providers\Qr\IQRCodeProvider {
+ public function getQRCodeImage($qrtext, $size) {
+ return $qrtext . '@' . $size;
+ }
+
+ public function getMimeType() {
+ return 'test/test';
+ }
+}
\ No newline at end of file