Index: server/lib/gutenbach/server/errors.py
===================================================================
--- server/lib/gutenbach/server/errors.py	(revision eee389a2ef90375dc33268c1828d860ef7be2907)
+++ server/lib/gutenbach/server/errors.py	(revision 951ab1bdfe4dd0ee569e0d9fdad9d257eac7623b)
@@ -35,4 +35,6 @@
                (self.errstr[self.state], hex(self.state))
 
+class InvalidDocument(Exception):
+    pass
 class MissingDataException(Exception):
     pass
Index: server/lib/gutenbach/server/job.py
===================================================================
--- server/lib/gutenbach/server/job.py	(revision a2b05827df68b47ce9fa7397689782f571172a24)
+++ server/lib/gutenbach/server/job.py	(revision 951ab1bdfe4dd0ee569e0d9fdad9d257eac7623b)
@@ -1,3 +1,3 @@
-from .errors import InvalidJobStateException, MissingDataException
+from . import errors
 from .player import Player
 from gutenbach.ipp import JobStates as States
@@ -23,8 +23,9 @@
         self.name = name
         self.priority = priority
-        self._why_done = None
 
         if document is not None:
             self.spool(document)
+
+        self._why_done = None
 
     def __repr__(self):
@@ -52,5 +53,5 @@
         try:
             self._id = max(int(val), -1)
-        except TypeError:
+        except:
             self._id = -1
 
@@ -62,5 +63,5 @@
         try:
             self._priority = max(int(val), 1)
-        except TypeError:
+        except:
             self._priority = 1
 
@@ -103,4 +104,8 @@
         return size
 
+    ######################################################################
+    ###                            State                               ###
+    ######################################################################
+
     @property
     def is_valid(self):
@@ -145,4 +150,8 @@
     @property
     def is_done(self):
+        """Whether the job is done playing, regardless of whether it
+        completed successfully or not.
+
+        """
         return (self.is_valid and \
                 self.player is not None and \
@@ -150,4 +159,25 @@
                 (self._why_done == "cancelled" or \
                  self._why_done == "aborted")
+
+    @property
+    def is_completed(self):
+        """Whether the job completed successfully.
+
+        """
+        return self.is_done and self._why_done == "completed"
+
+    @property
+    def is_cancelled(self):
+        """Whether the job was cancelled.
+
+        """
+        return self.is_done and self._why_done == "cancelled"
+
+    @property
+    def is_aborted(self):
+        """Whether the job was aborted.
+
+        """
+        return self.is_done and self._why_done == "aborted"
 
     @property
@@ -167,9 +197,9 @@
         elif self.is_playing and self.is_paused:
             state = States.STOPPED
-        elif self.is_done and self._why_done == "completed":
+        elif self.is_completed:
             state = States.COMPLETE
-        elif self.is_done and self._why_done == "cancelled":
+        elif self.is_cancelled:
             state = States.CANCELLED
-        elif self.is_done and self._why_done == "aborted":
+        elif self.is_aborted:
             state = States.ABORTED
         else:
@@ -181,7 +211,30 @@
     ######################################################################
 
-    def spool(self, document):
-        if not self.is_valid:
-            raise InvalidJobStateException(self.state)
+    @staticmethod
+    def verify_document(document):
+        if not hasattr(document, "name"):
+            raise errors.InvalidDocument, "no name attribute"
+        if not hasattr(document, "read"):
+            raise errors.InvalidDocument, "no read attribute"
+        if not hasattr(document, "close"):
+            raise errors.InvalidDocument, "no close attribute"
+
+    def spool(self, document=None):
+        """Non-blocking spool.  Job must be valid, and the document
+        must be an open file handler.
+
+        Raises
+        ------
+        InvalidDocument
+            If the document is not valid.
+        InvalidJobStateException
+            If the job is not valid or it is already
+            spooled/ready/finished.
+
+        """
+
+        if not self.is_valid or self.state != States.HELD:
+            raise errors.InvalidJobStateException(self.state)
+        self.verify_document(document)
         self.document = document.name
         self.player = Player(document)
@@ -189,6 +242,5 @@
 
     def play(self):
-        """Non-blocking play function.  Sets the job state to
-        PROCESSING.
+        """Non-blocking play.  Job must be ready.
 
         Raises
@@ -202,5 +254,5 @@
         # valid
         if not self.is_ready:
-            raise InvalidJobStateException(self.state)
+            raise errors.InvalidJobStateException(self.state)
         
         # and set the state to processing if we're good to go
@@ -214,14 +266,30 @@
 
     def pause(self):
-        """Non-blocking pause function.  Sets the job state to
-        STOPPED.
+        """Non-blocking pause.  Job must be playing.
+
+        Raises
+        ------
+        InvalidJobStateException
+            If the job is not playing.
 
         """
         
         if not self.is_playing:
-            raise InvalidJobStateException(self.state)
+            raise errors.InvalidJobStateException(self.state)
         self.player.mplayer_pause()
 
     def cancel(self):
+        """Non-blocking cancel. The job must not have previously
+        finished (i.e., cannot be aborted, cancelled, or completed).
+        This should be used to stop the job following an external
+        request.
+
+        Raises
+        ------
+        InvalidJobStateException
+            If the job has already finished.
+
+        """
+        
         def _cancelled():
             logger.info("cancelled job %s" % str(self))
@@ -232,9 +300,20 @@
             self.player.mplayer_stop()
         elif self.is_done and not self._why_done == "cancelled":
-            raise InvalidJobStateException(self.state)
+            raise errors.InvalidJobStateException(self.state)
         else:
             _cancelled()
 
     def abort(self):
+        """Non-blocking abort. The job must not have previously
+        finished (i.e., cannot be aborted, cancelled, or completed).
+        This should be used to stop the job following internal errors.
+
+        Raises
+        ------
+        InvalidJobStateException
+            If the job has already finished.
+
+        """
+
         def _aborted():
             logger.info("aborted job %s" % str(self))
@@ -245,5 +324,5 @@
             self.player.mplayer_stop()
         elif self.is_done and not self._why_done == "aborted":
-            raise InvalidJobStateException(self.state)
+            raise errors.InvalidJobStateException(self.state)
         else:
             _aborted()
Index: server/test/server/job.py
===================================================================
--- server/test/server/job.py	(revision 345c4768c17a73ba2749c01a590747645f06393f)
+++ server/test/server/job.py	(revision 951ab1bdfe4dd0ee569e0d9fdad9d257eac7623b)
@@ -4,4 +4,5 @@
 import unittest
 import tempfile
+import sys
 
 def make_tempfile():
@@ -37,19 +38,12 @@
         self.assertFalse(self.job.is_paused)
         self.assertFalse(self.job.is_done)
+        self.assertFalse(self.job.is_completed)
+        self.assertFalse(self.job.is_cancelled)
+        self.assertFalse(self.job.is_aborted)
 
     def testSpool(self):
-        fh = make_tempfile()
-        # This should fail, because the id hasn't been set
-        self.assertFalse(self.job.is_valid)
-        self.job.id = 1
-        self.assertTrue(self.job.is_valid)
-        self.assertFalse(self.job.is_ready)
-        self.job.spool(fh)
-        self.assertEqual(self.job.document, fh.name)
-        self.assertNotEqual(self.job.player, None)
-        self.assertEqual(self.job.creator, "")
-        self.assertEqual(self.job.state, States.PENDING)
-        self.assertTrue(self.job.is_ready)
-        self.job.abort()
+        fh = tempfile.NamedTemporaryFile()
+        self.assertRaises(errors.InvalidJobStateException, self.job.spool, fh)
+        fh.close()
     def testPlay(self):
         self.assertRaises(errors.InvalidJobStateException, self.job.play)
@@ -58,7 +52,9 @@
     def testCancel(self):
         self.job.cancel()
+        self.assertTrue(self.job.is_cancelled)
         self.assertEqual(self.job.state, States.CANCELLED)
     def testAbort(self):
         self.job.abort()
+        self.assertTrue(self.job.is_aborted)
         self.assertEqual(self.job.state, States.ABORTED)
 
@@ -66,24 +62,75 @@
 
     def testBadJobId(self):
-        self.job = GutenbachJob(job_id=-2)
-        self.assertEqual(self.job.id, -1)
-        self.job.id = -2
-        self.assertEqual(self.job.id, -1)
+        job = GutenbachJob(job_id=-2)
+        self.assertEqual(job.id, -1)
+        job.id = -2
+        self.assertEqual(job.id, -1)
 
     def testBadCreator(self):
-        self.job = GutenbachJob(job_id=1, creator=12345)
-        self.assertEqual(self.job.creator, "12345")
-        self.job.creator = None
-        self.assertEqual(self.job.creator, "")
-        self.job.creator = []
-        self.assertEqual(self.job.creator, "[]")
+        job = GutenbachJob(
+            job_id=1,
+            creator=12345)
+        self.assertEqual(job.creator, "12345")
+        job.creator = None
+        self.assertEqual(job.creator, "")
+        job.creator = []
+        self.assertEqual(job.creator, "[]")
 
     def testBadName(self):
-        self.job = GutenbachJob(job_id=1, creator="foo", name=12345)
-        self.assertEqual(self.job.name, "12345")
-        self.job.name = None
-        self.assertEqual(self.job.name, "")
-        self.job.name = []
-        self.assertEqual(self.job.name, "[]")
+        job = GutenbachJob(
+            job_id=1,
+            creator="foo",
+            name=12345)
+        self.assertEqual(job.name, "12345")
+        job.name = None
+        self.assertEqual(job.name, "")
+        job.name = []
+        self.assertEqual(job.name, "[]")
+
+    def testBadPriority(self):
+        job = GutenbachJob(
+            job_id=1,
+            creator="foo",
+            name="test",
+            priority=-1)
+        self.assertEqual(job.priority, 1)
+        job.priority = 0
+        self.assertEqual(job.priority, 1)
+        job.priority = 1
+        self.assertEqual(job.priority, 1)
+        job.priority = sys.maxint
+        self.assertEqual(job.priority, sys.maxint)
+        job.priority = "hello"
+        self.assertEqual(job.priority, 1)
+        job.priority = []
+        self.assertEqual(job.priority, 1)
+
+    def testBadDocument(self):
+        job = GutenbachJob(
+            job_id=1,
+            creator="foo",
+            name="test",
+            priority=1)
+        self.assertRaises(errors.InvalidDocument, job.spool, "hello")
+        
+class TestGoodGutenbachJob(unittest.TestCase):
+
+    def setUp(self):
+        self.job = GutenbachJob(job_id=1, creator="foo", name="test")
+
+    def testSpool(self):
+        fh = make_tempfile()
+        self.assertTrue(self.job.is_valid)
+        self.assertFalse(self.job.is_ready)
+        self.job.spool(fh)
+        self.assertTrue(self.job.is_ready)
+        
+        # Verify various properties
+        self.assertEqual(self.job.document, fh.name)
+        self.assertNotEqual(self.job.player, None)
+        self.assertEqual(self.job.state, States.PENDING)
+
+        self.job.abort()
+
 
 if __name__ == "__main__":
