SEBA-570 additional unit test for backup/restore

Change-Id: Iaa10c7c9c1ca7e11531c62fd56a6dd83c63a527e
diff --git a/VERSION b/VERSION
index 351227f..fb93c04 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-3.2.4
+3.2.5-dev
diff --git a/xos/coreapi/backupprocessor.py b/xos/coreapi/backupprocessor.py
index 0f50bca..e02bfa0 100644
--- a/xos/coreapi/backupprocessor.py
+++ b/xos/coreapi/backupprocessor.py
@@ -39,6 +39,9 @@
         self.backup_file_dir = "/var/run/xos/backup/local"
         self.log = create_logger(Config().get("logging"))
 
+    def get_backuphandler(self):
+        return BackupHandler()
+
     def instrument_fail(self, req, where):
         """ Check to see if the request indicates that a failure should be instrumented for testing
             purposes. This is done by inserting special strings ("fail_before_restore", etc) into
@@ -64,7 +67,7 @@
 
     def emergency_rollback(self, emergency_rollback_fn):
         self.log.warning("Performing emergency rollback")
-        BackupHandler().restore(emergency_rollback_fn)
+        self.get_backuphandler().restore(emergency_rollback_fn)
 
     def finalize_response(self, request, response, status, checksum=None, error_msg=None, exception=False):
         """ Build a response dictionary, incorporating informaiton from the request, as well as information
@@ -74,10 +77,9 @@
             the reponse.
         """
         if error_msg:
-            # TODO(smbaker): Consider also including exception information?
             response["error_msg"] = error_msg
-            response[""] = traceback.format_exc()
             if exception:
+                response["exception"] = traceback.format_exc()
                 self.log.exception(error_msg)
             else:
                 self.log.error(error_msg)
@@ -95,7 +97,8 @@
             response["file_details"]["checksum"] = checksum
 
         fn = os.path.join(self.backup_response_dir, request["request_fn"] + "_response")
-        open(fn, "w").write(json.dumps(response))
+        with open(fn, "w") as resp_f:
+            resp_f.write(json.dumps(response))
         os.remove(request["request_pathname"])
         return status
 
@@ -116,7 +119,7 @@
 
         try:
             self.instrument_fail(req, "fail_before_backup")
-            BackupHandler().backup(backend_filename)
+            self.get_backuphandler().backup(backend_filename)
         except Exception:
             return self.finalize_response(req, resp, "failed",
                                           error_msg="Backup failed",
@@ -160,7 +163,7 @@
 
         try:
             self.instrument_fail(req, "fail_before_rollback")
-            BackupHandler().backup(emergency_rollback_fn)
+            self.get_backuphandler().backup(emergency_rollback_fn)
         except Exception:
             return self.finalize_response(req, resp, "failed",
                                           error_msg="Exception during create emergency rollback",
@@ -170,7 +173,7 @@
 
         try:
             self.instrument_fail(req, "fail_before_restore")
-            BackupHandler().restore(backend_filename)
+            self.get_backuphandler().restore(backend_filename)
         except Exception:
             self.emergency_rollback(emergency_rollback_fn)
             return self.finalize_response(req, resp, "failed",
@@ -182,8 +185,7 @@
         if (not self.try_models()) or ("fail_try_models" in req["file_details"]["backend_filename"]):
             self.emergency_rollback(emergency_rollback_fn)
             return self.finalize_response(req, resp, "failed",
-                                          error_msg="Try_models failed, emergency rollback performed",
-                                          exception=True)
+                                          error_msg="Try_models failed, emergency rollback performed")
 
         return self.finalize_response(req, resp, "restored")
 
diff --git a/xos/coreapi/backupsetwatcher.py b/xos/coreapi/backupsetwatcher.py
index 704988f..8b16639 100644
--- a/xos/coreapi/backupsetwatcher.py
+++ b/xos/coreapi/backupsetwatcher.py
@@ -222,7 +222,8 @@
             "backend_filename": backupop.file.backend_filename}
 
         request_fn = os.path.join(self.backup_request_dir, "request")
-        open(request_fn, "w").write(json.dumps(request))
+        with open(request_fn, "w") as f:
+            f.write(json.dumps(request))
 
     def run_once(self):
         # If we're exiting due to a backup request being saved, then return
diff --git a/xos/coreapi/test_backuphandler.py b/xos/coreapi/test_backuphandler.py
new file mode 100644
index 0000000..e621a97
--- /dev/null
+++ b/xos/coreapi/test_backuphandler.py
@@ -0,0 +1,89 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import pdb
+import unittest
+from mock import patch
+
+from xosconfig import Config
+
+
+class TestBackupHandler(unittest.TestCase):
+    def setUp(self):
+        config = os.path.abspath(
+            os.path.dirname(os.path.realpath(__file__)) + "/test_config.yaml"
+        )
+        Config.clear()  # in case left unclean by a previous test case
+        Config.init(config)
+
+        import backuphandler
+
+        self.handler_postgres = backuphandler.BackupHandler_postgres()
+
+    def tearDown(self):
+        pass
+
+    def test_postgres_init(self):
+        self.assertEqual(self.handler_postgres.db_name, "xos")
+        self.assertEqual(self.handler_postgres.db_username, "postgres")
+        self.assertEqual(self.handler_postgres.db_password, "password")
+        self.assertEqual(self.handler_postgres.db_host, "xos-db")
+        self.assertEqual(self.handler_postgres.db_port, "5432")
+
+    def test_postgres_backup(self):
+        filename = "/tmp/foo"
+        with patch("os.system") as os_system:
+            os_system.return_value = 0
+            self.handler_postgres.backup(filename)
+
+            os_system.assert_called_with(
+                'PGPASSWORD="password" pg_dump -h xos-db -p 5432 -U postgres -c xos > /tmp/foo')
+
+    def test_postgres_backup_fail(self):
+        filename = "/tmp/foo"
+        with patch("os.system") as os_system:
+            os_system.return_value = 1
+
+            with self.assertRaises(Exception) as e:
+                self.handler_postgres.backup(filename)
+
+            self.assertEqual(str(e.exception), "pgdump failed")
+
+    def test_postgres_restore(self):
+        filename = "/tmp/foo"
+        with patch("os.system") as os_system:
+            os_system.return_value = 0
+            self.handler_postgres.restore(filename)
+
+            os_system.assert_called_with(
+                'PGPASSWORD="password" psql -h xos-db -p 5432 -U postgres xos < /tmp/foo > /dev/null')
+
+    def test_postgres_restore_fail(self):
+        filename = "/tmp/foo"
+        with patch("os.system") as os_system:
+            os_system.return_value = 1
+
+            with self.assertRaises(Exception) as e:
+                self.handler_postgres.restore(filename)
+
+            self.assertEqual(str(e.exception), "psql failed")
+
+
+def main():
+    unittest.main()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/xos/coreapi/test_backupprocessor.py b/xos/coreapi/test_backupprocessor.py
new file mode 100644
index 0000000..c2bbd24
--- /dev/null
+++ b/xos/coreapi/test_backupprocessor.py
@@ -0,0 +1,274 @@
+# Copyright 2017-present Open Networking Foundation
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import os
+import pdb
+import unittest
+from mock import MagicMock, patch, ANY, call
+from pyfakefs import fake_filesystem_unittest
+from io import open
+
+# pyfakefs breaks these
+from __builtin__ import dir as builtin_dir, True as builtin_True, False as builtin_False
+
+from xosconfig import Config
+
+
+def mock_make_backup(fn):
+    with open(fn, "w") as backup_f:
+        backup_f.write("stuff")
+
+
+class TestBackupProcessor(fake_filesystem_unittest.TestCase):
+    def setUp(self):
+        config = os.path.abspath(
+            os.path.dirname(os.path.realpath(__file__)) + "/test_config.yaml"
+        )
+        Config.clear()  # in case left unclean by a previous test case
+        Config.init(config)
+
+        import backupprocessor
+        self.backupprocessor = backupprocessor
+
+        self.setUpPyfakefs()
+
+        self.processor = backupprocessor.BackupProcessor()
+        self.mock_backuphandler = MagicMock(backup=MagicMock(), restore=MagicMock())
+
+    def tearDown(self):
+        pass
+
+    def test_compute_checksum(self):
+        with open("/tmp/somefile", "w") as somefile:
+            somefile.write("test")
+            self.assertEqual(self.processor.compute_checksum("/tmp/somefile"),
+                             "sha1:da39a3ee5e6b4b0d3255bfef95601890afd80709")
+
+    def test_try_models(self):
+        with patch("os.system") as os_system:
+            os_system.return_value = 0
+            self.processor.try_models()
+
+            os_system.assert_called_with("python try_models.py")
+
+    def test_emergency_rollback(self):
+        with patch.object(self.processor, "get_backuphandler") as get_backuphandler:
+            get_backuphandler.return_value = self.mock_backuphandler
+
+            self.processor.emergency_rollback("/tmp/foo")
+            self.mock_backuphandler.restore.assert_called_with("/tmp/foo")
+
+    def init_request(self, operation):
+        os.makedirs(self.processor.backup_request_dir)
+
+        request_fn = "request"
+        request_pathname = os.path.join(self.processor.backup_request_dir, request_fn)
+        backend_filename = os.path.join(self.processor.backup_file_dir, "mybackup")
+        file_details = {"id": 7,
+                        "name": "mybackup",
+                        "uri": "file://" + backend_filename,
+                        "backend_filename": backend_filename}
+        req = {"id": 3,
+               "operation": operation,
+               "file_details": file_details,
+               "request_fn": request_fn,
+               "request_pathname": request_pathname}
+
+        with open(request_pathname, "w") as req_fn:
+            req_fn.write(json.dumps(req))
+
+        return req
+
+    def test_finalize_response(self):
+        os.makedirs(self.processor.backup_response_dir)
+
+        req = self.init_request("create")
+        response_fn = os.path.join(self.processor.backup_response_dir, "request_response")
+
+        resp = {}
+
+        self.processor.finalize_response(req, resp, "created", checksum="1234")
+
+        self.assertTrue(os.path.exists(response_fn))
+        data = json.loads(open(response_fn).read())
+
+        expected_data = {u'status': u'created',
+                         u'effective_date': ANY,
+                         u'operation': u'create',
+                         u'id': 3,
+                         u'file_details': {u'backend_filename':
+                                           u'/var/run/xos/backup/local/mybackup',
+                                           u'checksum': u'1234',
+                                           u'uri': u'file:///var/run/xos/backup/local/mybackup',
+                                           u'name': u'mybackup',
+                                           u'id': 7}}
+        self.assertDictEqual(data, expected_data)
+
+    def test_handle_backup_request(self):
+        os.makedirs(self.processor.backup_response_dir)
+
+        req = self.init_request("create")
+
+        with patch.object(self.processor, "get_backuphandler") as get_backuphandler:
+            get_backuphandler.return_value = self.mock_backuphandler
+
+            self.mock_backuphandler.backup.side_effect = mock_make_backup
+
+            self.processor.handle_backup_request(req)
+
+            self.mock_backuphandler.backup.assert_called_with("/var/run/xos/backup/local/mybackup")
+
+            response_fn = os.path.join(self.processor.backup_response_dir, "request_response")
+            data = json.loads(open(response_fn).read())
+
+            expected_data = {u'status': u'created',
+                             u'effective_date': ANY,
+                             u'operation': u'create',
+                             u'id': 3,
+                             u'file_details': {u'backend_filename': u'/var/run/xos/backup/local/mybackup',
+                                               u'checksum': u'sha1:5eee38381388b6f30efdd5c5c6f067dbf32c0bb3',
+                                               u'uri': u'file:///var/run/xos/backup/local/mybackup',
+                                               u'name': u'mybackup',
+                                               u'id': 7}}
+            self.assertDictEqual(data, expected_data)
+
+    def test_handle_restore_request(self):
+        os.makedirs(self.processor.backup_response_dir)
+        os.makedirs(self.processor.backup_file_dir)
+
+        req = self.init_request("restore")
+
+        print req["file_details"]["backend_filename"]
+        with open(req["file_details"]["backend_filename"], "w") as backup_file:
+            backup_file.write("stuff")
+
+        with patch.object(self.processor, "get_backuphandler") as get_backuphandler, \
+                patch.object(self.processor, "try_models") as try_models:
+            get_backuphandler.return_value = self.mock_backuphandler
+            try_models.return_value = builtin_True
+
+            self.processor.handle_restore_request(req)
+
+            self.mock_backuphandler.backup.assert_called_with("/var/run/xos/backup/local/emergency_rollback")
+            self.mock_backuphandler.restore.assert_called_with("/var/run/xos/backup/local/mybackup")
+
+            response_fn = os.path.join(self.processor.backup_response_dir, "request_response")
+            data = json.loads(open(response_fn).read())
+
+            expected_data = {u'status': u'restored',
+                             u'effective_date': ANY,
+                             u'operation': u'restore',
+                             u'id': 3,
+                             u'file_details': {u'backend_filename': u'/var/run/xos/backup/local/mybackup',
+                                               u'uri': u'file:///var/run/xos/backup/local/mybackup',
+                                               u'name': u'mybackup',
+                                               u'id': 7}}
+            self.assertDictEqual(data, expected_data)
+
+    def test_handle_restore_request_fail_try(self):
+        """ Fails the restore operation during try_models, ensures that rollback was performed """
+
+        os.makedirs(self.processor.backup_response_dir)
+        os.makedirs(self.processor.backup_file_dir)
+
+        req = self.init_request("restore")
+
+        print req["file_details"]["backend_filename"]
+        with open(req["file_details"]["backend_filename"], "w") as backup_file:
+            backup_file.write("stuff")
+
+        with patch.object(self.processor, "get_backuphandler") as get_backuphandler, \
+                patch.object(self.processor, "try_models") as try_models:
+            get_backuphandler.return_value = self.mock_backuphandler
+            try_models.return_value = builtin_False
+
+            self.processor.handle_restore_request(req)
+
+            self.mock_backuphandler.backup.assert_called_with("/var/run/xos/backup/local/emergency_rollback")
+
+            self.assertEqual(self.mock_backuphandler.restore.mock_calls,
+                             [call("/var/run/xos/backup/local/mybackup"),
+                              call("/var/run/xos/backup/local/emergency_rollback")])
+
+            response_fn = os.path.join(self.processor.backup_response_dir, "request_response")
+            data = json.loads(open(response_fn).read())
+
+            expected_data = {u'status': u'failed',
+                             u'error_msg': u'Try_models failed, emergency rollback performed',
+                             u'effective_date': ANY,
+                             u'operation': u'restore',
+                             u'id': 3,
+                             u'file_details': {u'backend_filename': u'/var/run/xos/backup/local/mybackup',
+                                               u'uri': u'file:///var/run/xos/backup/local/mybackup',
+                                               u'name': u'mybackup',
+                                               u'id': 7}}
+            self.assertDictEqual(data, expected_data)
+
+    def test_run_create(self):
+        req = self.init_request("create")
+
+        with patch.object(self.processor, "handle_backup_request") as handle_backup_request:
+            self.processor.run()
+
+            handle_backup_request.assert_called()
+
+            handled_req = handle_backup_request.call_args[0][0]
+
+            self.assertDictEqual(handled_req, req)
+
+    def test_run_restore(self):
+        req = self.init_request("restore")
+
+        with patch.object(self.processor, "handle_restore_request") as handle_restore_request:
+            self.processor.run()
+
+            handle_restore_request.assert_called()
+
+            handled_req = handle_restore_request.call_args[0][0]
+
+            self.assertDictEqual(handled_req, req)
+
+    def test_run_not_json(self):
+        os.makedirs(self.processor.backup_request_dir)
+        with open(os.path.join(self.processor.backup_request_dir, "request"), "w") as req_f:
+            req_f.write("this is not json")
+
+        with patch.object(self.processor, "handle_restore_request") as handle_restore_request, \
+                patch.object(self.processor, "handle_backup_request") as handle_backup_request:
+            self.processor.run()
+
+            handle_restore_request.assert_not_called()
+            handle_backup_request.assert_not_called()
+
+    def test_run_not_understandable(self):
+        os.makedirs(self.processor.backup_request_dir)
+        request = {"somekey": "somevalue"}
+        with open(os.path.join(self.processor.backup_request_dir, "request"), "w") as req_f:
+            req_f.write(json.dumps(request))
+
+        with patch.object(self.processor, "handle_restore_request") as handle_restore_request, \
+                patch.object(self.processor, "handle_backup_request") as handle_backup_request:
+            self.processor.run()
+
+            handle_restore_request.assert_not_called()
+            handle_backup_request.assert_not_called()
+
+
+def main():
+    unittest.main()
+
+
+if __name__ == "__main__":
+    main()
diff --git a/xos/coreapi/test_backupsetwatcher.py b/xos/coreapi/test_backupsetwatcher.py
index 62acff3..014384d 100644
--- a/xos/coreapi/test_backupsetwatcher.py
+++ b/xos/coreapi/test_backupsetwatcher.py
@@ -12,17 +12,36 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+import functools
+import json
 import os
 import pdb
 import sys
 import unittest
-from mock import MagicMock, Mock
+from mock import MagicMock, Mock, patch
 from pyfakefs import fake_filesystem_unittest
 from io import open
 
+# pyfakefs breaks these
+from __builtin__ import dir as builtin_dir
+
 from xosconfig import Config
 
 
+def make_model(vars_dict, var_name, **kwargs):
+    """ Helper function to mock creating objects. Creates a MagicMock with the
+        kwargs, and also saves the variable into a dictionary where it can
+        easily be inspected.
+    """
+    name = kwargs.pop("name", None)
+    newmodel = MagicMock(**kwargs)
+    if name:
+        # Can't pass "name" in as an arg to MagicMock
+        newmodel.name = name
+    vars_dict[var_name] = newmodel
+    return newmodel
+
+
 class MockServer(object):
     def __init__(self):
         super(MockServer, self).__init__()
@@ -64,7 +83,7 @@
         self.watcher.init_request_dir()
         self.assertTrue(os.path.exists(self.watcher.backup_request_dir))
 
-        # Shoudl remove any existing files
+        # Should remove any existing files
         fn = os.path.join(self.watcher.backup_request_dir, "foo")
         open(fn, "w").write("foo")
         self.watcher.init_request_dir()
@@ -81,7 +100,8 @@
 
     def test_process_response_create(self):
         file_details = {"checksum": "1234"}
-        response = {"file_details": file_details}
+        response = {"file_details": file_details,
+                    "effective_date": "today"}
 
         file = MagicMock()
         op = MagicMock(file=file)
@@ -95,8 +115,205 @@
 
         self.assertEqual(op.status, "created")
         self.assertEqual(op.error_msg, "")
+        self.assertEqual(op.effective_date, "today")
         op.save.assert_called()
 
+    def test_process_response_restore_noexist(self):
+        file_details = {"id": 7,
+                        "name": "mybackup",
+                        "uri": "file:///mybackup",
+                        "checksum": "1234",
+                        "backend_filename": "/mybackup"}
+        response = {"file_details": file_details,
+                    "effective_date": "today"}
+
+        mockvars = {}
+
+        self.backupsetwatcher.BackupFile.objects.filter.return_value = []
+        self.backupsetwatcher.BackupFile.side_effect = functools.partial(make_model, mockvars, "newfile")
+
+        self.backupsetwatcher.BackupOperation.objects.filter.return_value = []
+        self.backupsetwatcher.BackupOperation.side_effect = functools.partial(make_model, mockvars, "newop")
+
+        self.watcher.process_response_restore(id=1, operation="restore", status="restored", response=response)
+
+        newfile = mockvars["newfile"]
+        self.assertEqual(newfile.name, "mybackup")
+        self.assertEqual(newfile.uri, "file:///mybackup")
+        self.assertEqual(newfile.checksum, "1234")
+        self.assertEqual(newfile.backend_filename, "/mybackup")
+
+        newop = mockvars["newop"]
+        self.assertEqual(newop.operation, "restore")
+        self.assertEqual(newop.file, newfile)
+        self.assertEqual(newop.status, "restored")
+        self.assertEqual(newop.error_msg, "")
+        self.assertEqual(newop.effective_date, "today")
+        newop.save.assert_called()
+
+    def test_process_response_restore_exists(self):
+        file_details = {}
+        response = {"file_details": file_details,
+                    "effective_date": "today"}
+
+        file = MagicMock()
+        op = MagicMock(file=file)
+
+        self.backupsetwatcher.BackupOperation.objects.filter.return_value = [op]
+
+        self.watcher.process_response_restore(id=1, operation="restore", status="restored", response=response)
+
+        self.assertEqual(op.status, "restored")
+        self.assertEqual(op.error_msg, "")
+        self.assertEqual(op.effective_date, "today")
+        op.save.assert_called()
+
+    def test_process_response_dir_create(self):
+        # BackupSetWatcher's __init__ method will have already called process_response_dir
+        # This means the backup_response_dir will be already created.
+
+        self.assertTrue(os.path.exists(self.watcher.backup_response_dir))
+
+        file_details = {"backend_filename": "/mybackup"}
+        resp = {"id": 7, "operation": "create", "status": "created", "file_details": file_details}
+        resp_fn = os.path.join(self.watcher.backup_response_dir, "response")
+
+        with open(resp_fn, "w") as resp_f:
+            resp_f.write(json.dumps(resp))
+
+        with patch.object(self.watcher, "process_response_create") as process_response_create, \
+                patch.object(self.watcher, "process_response_restore") as process_response_restore:
+            self.watcher.process_response_dir()
+
+            process_response_create.assert_called()
+            process_response_restore.assert_not_called()
+
+    def test_process_response_dir_restore(self):
+        # BackupSetWatcher's __init__ method will have already called process_response_dir
+        # This means the backup_response_dir will be already created.
+
+        self.assertTrue(os.path.exists(self.watcher.backup_response_dir))
+
+        file_details = {"backend_filename": "/mybackup"}
+        resp = {"id": 7, "operation": "restore", "status": "restored", "file_details": file_details}
+        resp_fn = os.path.join(self.watcher.backup_response_dir, "response")
+
+        with open(resp_fn, "w") as resp_f:
+            resp_f.write(json.dumps(resp))
+
+        with patch.object(self.watcher, "process_response_create") as process_response_create, \
+                patch.object(self.watcher, "process_response_restore") as process_response_restore:
+            self.watcher.process_response_dir()
+
+            process_response_create.assert_not_called()
+            process_response_restore.assert_called()
+
+    def test_save_request(self):
+        file = Mock(id=7,
+                    uri="file:///mybackup",
+                    checksum="1234",
+                    backend_filename="/mybackup")
+        file.name = "mybackup",
+
+        request = Mock(id=3,
+                       file=file,
+                       operation="create")
+
+        os.makedirs(self.watcher.backup_request_dir)
+
+        self.watcher.save_request(request)
+
+        req_fn = os.path.join(self.watcher.backup_request_dir, "request")
+        data = json.loads(open(req_fn).read())
+
+        expected_data = {u'operation': u'create',
+                         u'id': 3,
+                         u'file_details': {u'backend_filename': u'/mybackup',
+                                           u'checksum': u'1234',
+                                           u'uri': u'file:///mybackup',
+                                           u'name': [u'mybackup'],
+                                           u'id': 7}}
+
+        self.assertDictEqual(data, expected_data)
+
+    def test_run_once_create(self):
+        file = Mock(id=7,
+                    uri="file:///var/run/xos/backup/local/",
+                    checksum="1234",
+                    backend_filename="/mybackup")
+        file.name = "mybackup",
+
+        request = Mock(id=3,
+                       file=file,
+                       component="xos",
+                       operation="create",
+                       status=None)
+
+        self.backupsetwatcher.BackupOperation.objects.filter.return_value = [request]
+
+        with patch.object(self.watcher, "save_request") as save_request, \
+                patch.object(self.server, "delayed_shutdown") as delayed_shutdown:
+            self.watcher.run_once()
+
+            self.assertEqual(save_request.call_count, 1)
+            saved_op = save_request.call_args[0][0]
+
+            self.assertEqual(request, saved_op)
+            self.assertEqual(saved_op.status, "inprogress")
+            self.assertTrue(self.watcher.exiting)
+
+            delayed_shutdown.assert_called()
+
+    def test_run_once_restore(self):
+        file = Mock(id=7,
+                    uri="file:///var/run/xos/backup/local/",
+                    checksum="1234",
+                    backend_filename="/mybackup")
+        file.name = "mybackup",
+
+        request = Mock(id=3,
+                       file=file,
+                       component="xos",
+                       operation="restore",
+                       status=None)
+
+        self.backupsetwatcher.BackupOperation.objects.filter.return_value = [request]
+
+        with patch.object(self.watcher, "save_request") as save_request, \
+                patch.object(self.server, "delayed_shutdown") as delayed_shutdown:
+            self.watcher.run_once()
+
+            self.assertEqual(save_request.call_count, 1)
+            saved_op = save_request.call_args[0][0]
+
+            self.assertEqual(request, saved_op)
+            self.assertEqual(saved_op.status, "inprogress")
+            self.assertTrue(self.watcher.exiting)
+
+            delayed_shutdown.assert_called()
+
+    def test_run_once_not_xos(self):
+        file = Mock(id=7,
+                    uri="file:///var/run/xos/backup/local/",
+                    checksum="1234",
+                    backend_filename="/mybackup")
+        file.name = "mybackup",
+
+        request = Mock(id=3,
+                       file=file,
+                       component="somethingelse",
+                       operation="create",
+                       status=None)
+
+        self.backupsetwatcher.BackupOperation.objects.filter.return_value = [request]
+
+        with patch.object(self.watcher, "save_request") as save_request, \
+                patch.object(self.server, "delayed_shutdown") as delayed_shutdown:
+            self.watcher.run_once()
+
+            save_request.assert_not_called()
+            delayed_shutdown.assert_not_called()
+
 
 def main():
     unittest.main()
diff --git a/xos/coreapi/test_config.yaml b/xos/coreapi/test_config.yaml
index ae389f4..ab24db1 100644
--- a/xos/coreapi/test_config.yaml
+++ b/xos/coreapi/test_config.yaml
@@ -15,6 +15,10 @@
 
 
 name: test-coreapi
+database:
+  name: "xos"
+  username: "postgres"
+  password: "password"
 logging:
   version: 1
   handlers: