diff --git a/client/djrandom_client/test/__init__.py b/client/djrandom_client/test/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/client/djrandom_client/test/test_client.py b/client/djrandom_client/test/test_client.py
new file mode 100644
index 0000000000000000000000000000000000000000..07789d1e0d521ac21b4b6e583eef998e5fd3a730
--- /dev/null
+++ b/client/djrandom_client/test/test_client.py
@@ -0,0 +1,179 @@
+import mox
+import unittest
+import sys
+import time
+import Queue
+from djrandom_client import client
+from djrandom_client import daemonize
+from djrandom_client import upload
+from djrandom_client import throttle
+from djrandom_client import utils
+from djrandom_client import filescan
+
+
+class EndTest(Exception):
+    pass
+
+
+class CompareTuple(mox.Comparator):
+
+    def __init__(self, *ref):
+        self.ref = ref
+
+    def equals(self, rhs):
+        for comparator, value in zip(self.ref, rhs):
+            if comparator != value:
+                return False
+        return True
+
+    def __repr__(self):
+        return '(%s)' % ', '.join(str(x) for x in self.ref)
+
+
+class FullScanTest(mox.MoxTestBase):
+
+    def setUp(self):
+        mox.MoxTestBase.setUp(self)
+        self.queue = self.mox.CreateMock(Queue.Queue)
+        self.mox.StubOutWithMock(filescan, 'recursive_scan')
+
+    def test_run_once(self):
+        filescan.recursive_scan('basedir', self.queue)
+        self.queue.put(None)
+        self.mox.ReplayAll()
+
+        fs = client.FullScan('basedir', self.queue, 3600, True)
+        fs.run()
+
+    def test_run_loop(self):
+        self.mox.StubOutWithMock(time, 'sleep')
+        filescan.recursive_scan('basedir', self.queue)
+        time.sleep(3600).AndRaise(EndTest)
+        self.mox.ReplayAll()
+
+        fs = client.FullScan('basedir', self.queue, 3600)
+        self.assertRaises(EndTest, fs.run)
+
+    def test_run_filescan_error(self):
+        self.mox.StubOutWithMock(time, 'sleep')
+        filescan.recursive_scan('basedir', self.queue).AndRaise(
+            Exception('ahi ahi'))
+        time.sleep(1800).AndRaise(EndTest)
+        self.mox.ReplayAll()
+
+        fs = client.FullScan('basedir', self.queue, 3600)
+        self.assertRaises(EndTest, fs.run)
+
+
+class ClientOptionsTest(mox.MoxTestBase):
+
+    def setUp(self):
+        mox.MoxTestBase.setUp(self)
+        self.mox.StubOutWithMock(utils, 'read_config_defaults')
+        utils.read_config_defaults(mox.IgnoreArg(), mox.IsA(str))
+        self.mox.StubOutWithMock(utils, 'check_version')
+        self.mox.StubOutWithMock(daemonize, 'daemonize')
+
+    def _Run(self, args, expect_success=True):
+        try:
+            sys.argv = ['client'] + args
+            status = client.main()
+            success = not status
+        except SystemExit, e:
+            success = False
+        self.assertEquals(
+            expect_success, success,
+            'execution with args: "%s" failed (status %s, expected %s)' % (
+                ' '.join(args), success, expect_success))
+
+    def test_client_needs_api_key(self):
+        self.mox.ReplayAll()
+        self._Run([], False)
+
+    def test_too_many_arguments(self):
+        self.mox.ReplayAll()
+        self._Run(['--api_key=KEY', 'arg'], False)
+
+    def test_run_default_options(self):
+        utils.check_version().AndReturn(False)
+        daemonize.daemonize(mox.IgnoreArg(),
+                            client.run_client,
+                            CompareTuple(mox.IsA(str),
+                                         mox.IsA(str),
+                                         'KEY',
+                                         None, None, True))
+
+        self.mox.ReplayAll()
+        self._Run(['--api_key=KEY'])
+
+    def test_run_with_options(self):
+        utils.check_version().AndReturn(False)
+        daemonize.daemonize(mox.IgnoreArg(),
+                            client.run_client,
+                            CompareTuple('http://server/receiver',
+                                         '/my/music',
+                                         'KEY',
+                                         True, 10, False))
+
+        self.mox.ReplayAll()
+        self._Run(['--api_key=KEY', '--server_url=http://server/receiver',
+                   '--music_dir=/my/music', '--bwlimit=10',
+                   '--once', '--no_realtime_watch'])
+
+    def test_music_path_expands_tilde(self):
+        utils.check_version().AndReturn(False)
+
+        def music_dir_does_not_start_with_tilde(music_dir):
+            return not music_dir.startswith('~')
+
+        daemonize.daemonize(mox.IgnoreArg(),
+                            client.run_client,
+                            CompareTuple('http://server/receiver',
+                                         mox.Func(music_dir_does_not_start_with_tilde),
+                                         'KEY',
+                                         True, 10, False))
+
+        self.mox.ReplayAll()
+        self._Run(['--api_key=KEY', '--server_url=http://server/receiver',
+                   '--music_dir=~/music', '--bwlimit=10',
+                   '--once', '--no_realtime_watch'])
+
+
+class RunClientTest(mox.MoxTestBase):
+
+    def setUp(self):
+        mox.MoxTestBase.setUp(self)
+
+    def test_run_once(self):
+        self.mox.StubOutWithMock(throttle, 'set_rate_limit')
+        throttle.set_rate_limit(150)
+
+        uploader = self.mox.CreateMock(upload.Uploader)
+        self.mox.StubOutWithMock(upload, 'Uploader', use_mock_anything=True)
+        upload.Uploader('http://server/receiver', 'KEY').AndReturn(uploader)
+        uploader.setDaemon(True)
+        uploader.queue = 'queue!'
+
+        fs = self.mox.CreateMock(client.FullScan)
+        self.mox.StubOutWithMock(client, 'FullScan', use_mock_anything=True)
+        client.FullScan('/my/music', uploader.queue, mox.IsA(int), True
+                        ).AndReturn(fs)
+        fs.setDaemon(True)
+
+        uploader.start()
+        fs.start()
+        uploader.join()
+
+        self.mox.ReplayAll()
+        client.run_client(
+            'http://server/receiver',
+            '/my/music',
+            'KEY',
+            True,
+            150,
+            False)
+
+
+if __name__ == '__main__':
+    unittest.main()
+
diff --git a/client/djrandom_client/test/test_filescan.py b/client/djrandom_client/test/test_filescan.py
new file mode 100644
index 0000000000000000000000000000000000000000..2e44e38535141c47eba4ce1ada09ed93332d8c01
--- /dev/null
+++ b/client/djrandom_client/test/test_filescan.py
@@ -0,0 +1,70 @@
+import os
+import unittest
+import shutil
+import tempfile
+from djrandom_client import filescan
+
+DIR = 0
+FILE = 1
+
+
+class FakeQueue(object):
+
+    def __init__(self):
+        self.data = []
+
+    def put(self, obj):
+        self.data.append(obj)
+
+
+class FilescanTest(unittest.TestCase):
+
+    def setUp(self):
+        self.dir = tempfile.mkdtemp()
+        self._createtree([
+                (DIR, 'dir1'),
+                (FILE, 'dir1/file1.mp3'),
+                (FILE, 'dir1/file2.txt'),
+                (DIR, 'dir2'),
+                (FILE, 'dir2/file3.mp3'),
+                ])
+
+    def tearDown(self):
+        shutil.rmtree(self.dir)
+
+    def _createtree(self, treedata):
+        for dtype, dname in treedata:
+            path = os.path.join(self.dir, dname)
+            if dtype == DIR:
+                os.mkdir(path)
+            else:
+                with open(path, 'w') as fd:
+                    fd.write('data\n')
+
+    def test_recursive_scan(self):
+        queue = FakeQueue()
+        n = filescan.recursive_scan(self.dir, queue)
+        self.assertEquals(2, n)
+
+        expected_files = [
+            os.path.join(self.dir, 'dir1/file1.mp3'),
+            os.path.join(self.dir, 'dir2/file3.mp3'),
+            ]
+        self.assertEquals(expected_files, queue.data)
+
+    def test_directory_scan(self):
+        queue = FakeQueue()
+        n = filescan.directory_scan(self.dir, queue)
+        self.assertEquals(0, n)
+        self.assertEquals([], queue.data)
+
+        queue = FakeQueue()
+        n = filescan.directory_scan(self.dir + '/dir1', queue)
+        self.assertEquals(1, n)
+        self.assertEquals([
+            os.path.join(self.dir, 'dir1/file1.mp3'),
+            ], queue.data)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/client/djrandom_client/test/test_throttle.py b/client/djrandom_client/test/test_throttle.py
new file mode 100644
index 0000000000000000000000000000000000000000..50e0d75340a57b38b871d83fce07f832cf7d9ce9
--- /dev/null
+++ b/client/djrandom_client/test/test_throttle.py
@@ -0,0 +1,113 @@
+import sys
+import time
+import threading
+import unittest
+import urllib2
+import BaseHTTPServer
+from djrandom_client import throttle
+
+
+class TokenBucketTest(unittest.TestCase):
+
+    def test_token_bucket(self):
+        tb = throttle.TokenBucket(1024, 512)
+        # A few tokens should be readily available.
+        self.assertEquals(0, tb.consume(1024))
+        # Another 2N tokens should take two seconds.
+        self.assertAlmostEquals(2, tb.consume(1024), 3)
+
+        # Now we expect tokens to be available at the
+        # normal rate.
+        self.assertAlmostEquals(1, tb.consume(512), 3)
+        self.assertAlmostEquals(1, tb.consume(512), 3)
+        self.assertAlmostEquals(1, tb.consume(512), 3)
+        self.assertAlmostEquals(1, tb.consume(512), 3)
+
+
+class HttpSinkHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+
+    def do_POST(self):
+        clen = int(self.headers.get('Content-Length', 0))
+        data = self.rfile.read(clen)
+        print 'received %d bytes on %s' % (len(data), self.path)
+        self.send_response(200)
+        self.send_header('Content-type', 'text/html')
+        self.end_headers()
+        self.wfile.write('ok')
+
+    do_GET = do_POST
+
+
+class HttpSink(threading.Thread):
+
+    def __init__(self, stop):
+        threading.Thread.__init__(self)
+        addr = ('127.0.0.1', 0)
+        self.httpd = BaseHTTPServer.HTTPServer(addr, HttpSinkHandler)
+        self.url = 'http://127.0.0.1:%d' % self.httpd.server_port
+        self.stop = stop
+
+    def run(self):
+        while not self.stop.is_set():
+            self.httpd.handle_request()
+
+
+class ThrottledHttpTest(unittest.TestCase):
+
+    def setUp(self):
+        self.http_stop = threading.Event()
+        self.http_server = HttpSink(self.http_stop)
+        self.http_server.start()
+
+    def tearDown(self):
+        self.http_stop.set()
+        urllib2.urlopen(self.http_server.url + '/quit').read()
+        self.http_server.join()
+
+    def test_rate_limit(self):
+        throttle.set_rate_limit(10)
+
+        opener = urllib2.build_opener(throttle.ThrottledHTTPHandler)
+        testdata = "x" * (50 * 1024)
+
+        req = urllib2.Request(
+            self.http_server.url + '/speedtest',
+            data=testdata,
+            headers={'Content-Length': str(len(testdata))})
+        start = time.time()
+        result = opener.open(req).read()
+        end = time.time()
+
+        self.assertEquals('ok', result)
+
+        elapsed = end - start
+        print >>sys.stderr, 'elapsed: %g secs' % elapsed
+        self.assertTrue(elapsed > 4.5 and elapsed < 5.5,
+                        'elapsed time out of range: %g' % elapsed)
+
+    def test_rate_limit_across_many_requests(self):
+        throttle.set_rate_limit(10)
+
+        opener = urllib2.build_opener(throttle.ThrottledHTTPHandler)
+        testdata = "x" * (5 * 1024)
+        n_reqs = 10
+
+        start = time.time()
+        for i in xrange(n_reqs):
+            req = urllib2.Request(
+                self.http_server.url + '/speedtest',
+                data=testdata,
+                headers={'Content-Length': str(len(testdata))})
+            result = opener.open(req).read()
+        end = time.time()
+
+        self.assertEquals('ok', result)
+
+        elapsed = end - start
+        print >>sys.stderr, 'elapsed: %g secs' % elapsed
+        self.assertTrue(elapsed > 4.5 and elapsed < 5.5,
+                        'elapsed time out of range: %g' % elapsed)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/client/djrandom_client/test/test_upload.py b/client/djrandom_client/test/test_upload.py
new file mode 100644
index 0000000000000000000000000000000000000000..64f1c40df8ea85ec9305b4c3fde383450748829f
--- /dev/null
+++ b/client/djrandom_client/test/test_upload.py
@@ -0,0 +1,191 @@
+import json
+import os
+import mox
+import unittest
+import shutil
+import tempfile
+import urllib2
+from djrandom_client import stats
+from djrandom_client import throttle
+from djrandom_client import upload
+from djrandom_client import utils
+
+
+class FileDatabaseTest(mox.MoxTestBase):
+
+    def setUp(self):
+        mox.MoxTestBase.setUp(self)
+        self.tmpdir = tempfile.mkdtemp()
+        self.dbpath = os.path.join(self.tmpdir, 'test.db')
+
+    def tearDown(self):
+        shutil.rmtree(self.tmpdir)
+        mox.MoxTestBase.tearDown(self)
+
+    def test_create_db(self):
+        db = upload.FileDatabase(self.dbpath)
+        db.close()
+        self.assertTrue(os.path.exists(self.dbpath))
+
+    def test_add_key(self):
+        db = upload.FileDatabase(self.dbpath)
+        db.add('/test/key')
+        self.assertTrue(db.has('/test/key'))
+
+    def test_add_key_twice(self):
+        db = upload.FileDatabase(self.dbpath)
+        db.add('/test/key')
+        db.add('/test/key')
+        self.assertTrue(db.has('/test/key'))
+
+    def test_no_such_key(self):
+        db = upload.FileDatabase(self.dbpath)
+        self.assertFalse(db.has('/test/key'))
+
+
+class UploadTest(mox.MoxTestBase):
+
+    def setUp(self):
+        mox.MoxTestBase.setUp(self)
+        self.tmpdir = tempfile.mkdtemp()
+        self.dbpath = os.path.join(self.tmpdir, 'test.db')
+        self.statspath = os.path.join(self.tmpdir, 'stats')
+
+        self.opener = self.mox.CreateMockAnything()
+        self.mox.StubOutWithMock(urllib2, 'build_opener')
+        urllib2.build_opener(throttle.ThrottledHTTPHandler
+                             ).AndReturn(self.opener)
+
+        self.test_file = os.path.join(self.tmpdir, 'testfile')
+        with open(self.test_file, 'w') as fd:
+            fd.write('data')
+        self.test_sha1 = utils.sha1_of_file(self.test_file)
+
+    def tearDown(self):
+        shutil.rmtree(self.tmpdir)
+        mox.MoxTestBase.tearDown(self)
+
+    def _create_uploader(self):
+        return upload.Uploader('http://server', 'api_key',
+                               db_path=self.dbpath,
+                               state_path=self.statspath)
+
+    def test_get(self):
+        resp = self.mox.CreateMockAnything()
+        self.opener.open(mox.IsA(urllib2.Request)).AndReturn(resp)
+        resp.read().AndReturn(json.dumps({'status': True}))
+
+        self.mox.ReplayAll()
+        up = self._create_uploader()
+        result = up._get('/url')
+        self.assertEqual(True, result)
+
+    def test_put(self):
+
+        def check_request(req):
+            self.assertTrue(isinstance(req, urllib2.Request))
+            self.assertEquals('data', str(req.get_data()[:]))
+            return True
+
+        resp = self.mox.CreateMockAnything()
+        self.opener.open(mox.Func(check_request)).AndReturn(resp)
+        resp.read().AndReturn(json.dumps({'status': True}))
+
+        self.mox.ReplayAll()
+
+        up = self._create_uploader()
+        result = up._put('/url', self.test_file)
+        self.assertEqual(True, result)
+
+    def test_upload_already_on_server(self):
+        self.mox.StubOutWithMock(upload.Uploader, '_get')
+        upload.Uploader._get('/check/%s' % self.test_sha1
+                             ).AndReturn(True)
+
+        self.mox.ReplayAll()
+
+        up = self._create_uploader()
+        up.upload(self.test_file)
+
+        self.assertEquals(0, up.stats._data.get('uploaded_files', 0))
+
+    def test_upload(self):
+        self.mox.StubOutWithMock(upload.Uploader, '_get')
+        upload.Uploader._get('/check/%s' % self.test_sha1
+                             ).AndReturn(False)
+        self.mox.StubOutWithMock(upload.Uploader, '_put')
+        upload.Uploader._put('/upload/%s' % self.test_sha1,
+                             self.test_file
+                             ).AndReturn(True)
+
+        self.mox.ReplayAll()
+
+        up = self._create_uploader()
+        up.upload(self.test_file)
+
+        self.assertEquals(1, up.stats._data['uploaded_files'])
+
+    def test_run(self):
+        self.mox.StubOutWithMock(upload.Uploader, 'upload')
+        upload.Uploader.upload(self.test_file)
+
+        self.mox.ReplayAll()
+
+        up = self._create_uploader()
+        up.queue.put(self.test_file)
+        up.queue.put(None)
+        up.run()
+
+        db = upload.FileDatabase(self.dbpath)
+        self.assertTrue(db.has(self.test_file))
+
+    def test_run_seen_file(self):
+        db = upload.FileDatabase(self.dbpath)
+        db.add(self.test_file)
+        db.close()
+
+        self.mox.StubOutWithMock(upload.Uploader, 'upload')
+        self.mox.ReplayAll()
+
+        up = self._create_uploader()
+        up.queue.put(self.test_file)
+        up.queue.put(None)
+        up.run()
+
+    def test_run_with_upload_error(self):
+        self.mox.StubOutWithMock(upload.Uploader, 'upload')
+        upload.Uploader.upload(self.test_file).AndRaise(Exception('argh!'))
+        self.mox.ReplayAll()
+
+        up = self._create_uploader()
+        up.queue.put(self.test_file)
+        up.queue.put(None)
+        up.run()
+
+        self.assertEquals(1, up.stats._data['errors'])
+
+    def test_run_and_check_stats(self):
+        st = self.mox.CreateMock(stats.Stats)
+        self.mox.StubOutWithMock(stats, 'Stats', use_mock_anything=True)
+        stats.Stats(self.statspath).AndReturn(st)
+
+        st.set('uploading', None)
+        st.set('uploading', self.test_file)
+
+        self.mox.StubOutWithMock(upload.Uploader, 'upload')
+        upload.Uploader.upload(self.test_file)
+
+        st.set('uploading', None)
+        st._save()
+
+        self.mox.ReplayAll()
+
+        up = self._create_uploader()
+        up.stats = st
+        up.queue.put(self.test_file)
+        up.queue.put(None)
+        up.run()
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/client/djrandom_client/test/test_utils.py b/client/djrandom_client/test/test_utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..1f4abcb5cfed19799d335d3f59ba418c41182793
--- /dev/null
+++ b/client/djrandom_client/test/test_utils.py
@@ -0,0 +1,76 @@
+import mox
+import unittest
+import os
+import shutil
+import tempfile
+import urllib2
+from djrandom_client import utils
+
+
+class UtilsTest(mox.MoxTestBase):
+
+    def setUp(self):
+        mox.MoxTestBase.setUp(self)
+        self.tmpdir = tempfile.mkdtemp()
+
+    def tearDown(self):
+        shutil.rmtree(self.tmpdir)
+        mox.MoxTestBase.tearDown(self)
+
+    def test_sha1_of_file(self):
+        test_file = os.path.join(self.tmpdir, 'testfile')
+        with open(test_file, 'w') as fd:
+            fd.write('test\n')
+        sha1 = utils.sha1_of_file(test_file)
+        self.assertEquals('4e1243bd22c66e76c2ba9eddc1f91394e57f9f83', sha1)
+
+    def test_read_config_defaults(self):
+        cfg_file = os.path.join(self.tmpdir, 'config')
+        with open(cfg_file, 'w') as fd:
+            fd.write('''
+# Test config file
+var_a=a
+var_b = b
+
+  var_c  = 42
+''')
+
+        parser = self.mox.CreateMockAnything()
+        parser.set_default('var_a', 'a')
+        parser.set_default('var_b', 'b')
+        parser.set_default('var_c', '42')
+        self.mox.ReplayAll()
+
+        utils.read_config_defaults(parser, cfg_file)
+
+    def test_read_config_file_with_error(self):
+        cfg_file = os.path.join(self.tmpdir, 'config')
+        with open(cfg_file, 'w') as fd:
+            fd.write('this is not a config\n')
+
+        self.assertRaises(utils.SyntaxError,
+                          utils.read_config_defaults,
+                          None, cfg_file)
+
+    def test_read_config_file_missing(self):
+        utils.read_config_defaults(
+            None, os.path.join(self.tmpdir, 'nosuchfile'))
+    
+
+    def test_check_version_ok(self):
+        resp = self.mox.CreateMockAnything()
+        self.mox.StubOutWithMock(urllib2, 'urlopen')
+        urllib2.urlopen(mox.IsA(str)).AndReturn(resp)
+        resp.read().AndReturn("VERSION = '0.1'\n")
+
+        self.mox.ReplayAll()
+        self.assertFalse(utils.check_version())
+
+    def test_check_version_should_upgrade(self):
+        resp = self.mox.CreateMockAnything()
+        self.mox.StubOutWithMock(urllib2, 'urlopen')
+        urllib2.urlopen(mox.IsA(str)).AndReturn(resp)
+        resp.read().AndReturn("VERSION = '9999'\n")
+
+        self.mox.ReplayAll()
+        self.assertTrue(utils.check_version())