diff --git a/.gitignore b/.gitignore
index 8f8a3ff..1dab14d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
+*~
*.pyc
build
dist
*.egg-info
-
+tests/tokens.py
diff --git a/tests/README.markdown b/tests/README.markdown
new file mode 100644
index 0000000..00a21df
--- /dev/null
+++ b/tests/README.markdown
@@ -0,0 +1,61 @@
+Tests for the Open Photo API / Python Library
+=======================
+#### OpenPhoto, a photo service for the masses
+
+----------------------------------------
+
+### Requirements
+A computer, Python 2.7 and an empty OpenPhoto instance.
+
+---------------------------------------
+
+### Setting up
+
+Create a tests/tokens.py file containing the following:
+
+ # tests/token.py
+ consumer_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ consumer_secret = "xxxxxxxxxx"
+ token = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+ token_secret = "xxxxxxxxxx"
+ host = "your_hostname"
+
+Make sure this is an empty test server, **not a production OpenPhoto server!!!**
+
+---------------------------------------
+
+### Running the tests
+
+ cd /path/to/openphoto-python
+ python -m unittest discover -c
+
+The "-c" lets you stop the tests gracefully with \[CTRL\]-c.
+
+The easiest way to run a subset of the tests is with nose:
+
+ cd /path/to/openphoto-python
+ nosetests -v -s tests/test_albums.py:TestAlbums.test_view
+
+---------------------------------------
+
+### Test Details
+
+These tests are intended to verify the Python library. They don't provide comprehensive testing of the OpenPhoto API, there are PHP unit tests for that.
+
+Each test class is run as follows:
+
+**SetUpClass:**
+
+Check that the server is empty
+
+**SetUp:**
+
+Ensure there are:
+
+ * Three test photos
+ * A single test tag applied to each
+ * A single album containing all three photos
+
+**TearDownClass:**
+
+Remove all photos, tags and albums
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_albums.py b/tests/test_albums.py
new file mode 100644
index 0000000..0c5fe11
--- /dev/null
+++ b/tests/test_albums.py
@@ -0,0 +1,88 @@
+import unittest
+import openphoto
+import test_base
+
+class TestAlbums(test_base.TestBase):
+
+ def test_create_delete(self):
+ """ Create an album then delete it """
+ album_name = "create_delete_album"
+ album = self.client.album.create(album_name, visible=True)
+
+ # Check the return value
+ self.assertEqual(album.name, album_name)
+ # Check that the album now exists
+ self.assertIn(album_name, [a.name for a in self.client.albums.list()])
+
+ # Delete the album
+ self.client.album.delete(album.id)
+ # Check that the album is now gone
+ self.assertNotIn(album_name, [a.name for a in self.client.albums.list()])
+
+ # Create it again, and delete it using the Album object
+ album = self.client.album.create(album_name, visible=True)
+ album.delete()
+ # Check that the album is now gone
+ self.assertNotIn(album_name, [a.name for a in self.client.albums.list()])
+
+ def test_update(self):
+ """ Test that an album can be updated """
+ # Update the album using the OpenPhoto class, passing in the album object
+ new_name = "New Name"
+ self.client.album.update(self.albums[0], name=new_name)
+
+ # Check that the album is updated
+ self.albums = self.client.albums.list()
+ self.assertEqual(self.albums[0].name, new_name)
+
+ # Update the album using the OpenPhoto class, passing in the album id
+ new_name = "Another New Name"
+ self.client.album.update(self.albums[0].id, name=new_name)
+
+ # Check that the album is updated
+ self.albums = self.client.albums.list()
+ self.assertEqual(self.albums[0].name, new_name)
+
+ # Update the album using the Album object directly
+ self.albums[0].update(name=self.TEST_ALBUM)
+
+ # Check that the album is updated
+ self.albums = self.client.albums.list()
+ self.assertEqual(self.albums[0].name, self.TEST_ALBUM)
+
+ def test_view(self):
+ """ Test the album view """
+ album = self.albums[0]
+ self.assertFalse(hasattr(album, "photos"))
+
+ # Get the photos in the album using the Album object directly
+ album.view()
+ # Make sure all photos are in the album
+ for photo in self.photos:
+ self.assertIn(photo.id, [p.id for p in album.photos])
+
+ @unittest.expectedFailure # Private albums are not visible - issue #929
+ def test_private(self):
+ """ Test that private albums can be created, and are visible """
+ # Create and check that the album now exists
+ album = self.client.album.create(album_name, visible=False)
+ self.assertIn(album_name, self.client.albums.list())
+
+ # Delete and check that the album is now gone
+ album.delete()
+ self.assertNotIn(album_name, self.client.albums.list())
+
+ def test_form(self):
+ """ If album.form gets implemented, write a test! """
+ with self.assertRaises(openphoto.NotImplementedError):
+ self.client.album.form(None)
+
+ def test_add_photos(self):
+ """ If album.add_photos gets implemented, write a test! """
+ with self.assertRaises(openphoto.NotImplementedError):
+ self.client.album.add_photos(None, None)
+
+ def test_remove_photos(self):
+ """ If album.remove_photos gets implemented, write a test! """
+ with self.assertRaises(openphoto.NotImplementedError):
+ self.client.album.remove_photos(None, None)
diff --git a/tests/test_base.py b/tests/test_base.py
new file mode 100644
index 0000000..8e9b980
--- /dev/null
+++ b/tests/test_base.py
@@ -0,0 +1,128 @@
+import unittest
+import openphoto
+
+try:
+ import tokens
+except ImportError:
+ print ("********************************************************************\n"
+ "You need to create a 'tokens.py' file containing the following:\n\n"
+ " host = \"\"\n"
+ " consumer_key = \"\"\n"
+ " consumer_secret = \"\"\n"
+ " token = \"\"\n"
+ " token_secret = \"\"\n"
+ " host = \"\"\n\n"
+ "WARNING: Don't use a production OpenPhoto instance for this!\n"
+ "********************************************************************\n")
+ raise
+
+class TestBase(unittest.TestCase):
+ TEST_TITLE = "Test Image - delete me!"
+ TEST_TAG = "test_tag"
+ TEST_ALBUM = "test_album"
+ MAXIMUM_TEST_PHOTOS = 4 # Never have more the 4 photos on the test server
+
+ def __init__(self, *args, **kwds):
+ unittest.TestCase.__init__(self, *args, **kwds)
+ self.photos = []
+
+ @classmethod
+ def setUpClass(cls):
+ """ Ensure there is nothing on the server before running any tests """
+ cls.client = openphoto.OpenPhoto(tokens.host,
+ tokens.consumer_key, tokens.consumer_secret,
+ tokens.token, tokens.token_secret)
+
+ if cls.client.photos.list() != []:
+ raise ValueError("The test server (%s) contains photos. "
+ "Please delete them before running the tests"
+ % tokens.host)
+
+ if cls.client.tags.list() != []:
+ raise ValueError("The test server (%s) contains tags. "
+ "Please delete them before running the tests"
+ % tokens.host)
+
+ if cls.client.albums.list() != []:
+ raise ValueError("The test server (%s) contains albums. "
+ "Please delete them before running the tests"
+ % tokens.host)
+
+ @classmethod
+ def tearDownClass(cls):
+ """ Once all tests have finished, delete all photos, tags and albums"""
+ cls._delete_all()
+
+ def setUp(self):
+ """
+ Ensure the three test photos are present before each test.
+ Give them each a tag.
+ Put them into an album.
+ """
+ self.photos = self.client.photos.list()
+ if len(self.photos) != 3:
+ print "[Regenerating Photos]"
+ if len(self.photos) > 0:
+ self._delete_all()
+ self._create_test_photos()
+ self.photos = self.client.photos.list()
+
+ self.tags = self.client.tags.list()
+ if (len(self.tags) != 1 or
+ self.tags[0].id != self.TEST_TAG or
+ self.tags[0].count != "3"):
+ print "[Regenerating Tags]"
+ self._delete_all()
+ self._create_test_photos()
+ self.photos = self.client.photos.list()
+ self.tags = self.client.tags.list()
+ if len(self.tags) != 1:
+ print "Tags: %s" % self.tags
+ raise Exception("Tag creation failed")
+
+ self.albums = self.client.albums.list()
+ if (len(self.albums) != 1 or
+ self.albums[0].name != self.TEST_ALBUM or
+ self.albums[0].count != "3"):
+ print "[Regenerating Albums]"
+ self._delete_all()
+ self._create_test_photos()
+ self.photos = self.client.photos.list()
+ self.tags = self.client.tags.list()
+ self.albums = self.client.albums.list()
+ if len(self.albums) != 1:
+ print "Albums: %s" % self.albums
+ raise Exception("Album creation failed")
+
+ @classmethod
+ def _create_test_photos(cls):
+ """ Upload three test photos """
+ album = cls.client.album.create(cls.TEST_ALBUM, visible=True)
+ photos = [
+ cls.client.photo.upload_encoded("tests/test_photo1.jpg",
+ title=cls.TEST_TITLE,
+ tags=cls.TEST_TAG),
+ cls.client.photo.upload_encoded("tests/test_photo2.jpg",
+ title=cls.TEST_TITLE,
+ tags=cls.TEST_TAG),
+ cls.client.photo.upload_encoded("tests/test_photo3.jpg",
+ title=cls.TEST_TITLE,
+ tags=cls.TEST_TAG),
+ ]
+ # Remove the auto-generated month/year tags
+ tags_to_remove = [p for p in photos[0].tags if p != cls.TEST_TAG]
+ for photo in photos:
+ photo.update(tagsRemove=tags_to_remove, albums=album.id)
+
+ @classmethod
+ def _delete_all(cls):
+ photos = cls.client.photos.list()
+ if len(photos) > cls.MAXIMUM_TEST_PHOTOS:
+ raise ValueError("There too many photos on the test server - must always be less than %d."
+ % cls.MAXIMUM_TEST_PHOTOS)
+ for photo in photos:
+ photo.delete()
+ for tag in cls.client.tags.list():
+ tag.delete()
+ for album in cls.client.albums.list():
+ album.delete()
diff --git a/tests/test_photo1.jpg b/tests/test_photo1.jpg
new file mode 100644
index 0000000..799c86b
Binary files /dev/null and b/tests/test_photo1.jpg differ
diff --git a/tests/test_photo2.jpg b/tests/test_photo2.jpg
new file mode 100644
index 0000000..103f87f
Binary files /dev/null and b/tests/test_photo2.jpg differ
diff --git a/tests/test_photo3.jpg b/tests/test_photo3.jpg
new file mode 100644
index 0000000..e3f708e
Binary files /dev/null and b/tests/test_photo3.jpg differ
diff --git a/tests/test_photos.py b/tests/test_photos.py
new file mode 100644
index 0000000..68e9cac
--- /dev/null
+++ b/tests/test_photos.py
@@ -0,0 +1,157 @@
+import unittest
+import openphoto
+import test_base
+
+class TestPhotos(test_base.TestBase):
+ def test_delete_upload(self):
+ """ Test photo deletion and upload """
+ # Delete one photo using the OpenPhoto class, passing in the id
+ self.client.photo.delete(self.photos[0].id)
+ # Delete one photo using the OpenPhoto class, passing in the object
+ self.client.photo.delete(self.photos[1])
+ # And another using the Photo object directly
+ self.photos[2].delete()
+
+ # Check that they're gone
+ self.assertEqual(self.client.photos.list(), [])
+
+ # Re-upload the photos
+ ret_val = self.client.photo.upload_encoded("tests/test_photo1.jpg",
+ title=self.TEST_TITLE)
+ self.client.photo.upload_encoded("tests/test_photo2.jpg",
+ title=self.TEST_TITLE)
+ self.client.photo.upload_encoded("tests/test_photo3.jpg",
+ title=self.TEST_TITLE)
+
+ # Check there are now three photos
+ self.photos = self.client.photos.list()
+ self.assertEqual(len(self.photos), 3)
+
+ # Check that the upload return value was correct
+ pathOriginals = [photo.pathOriginal for photo in self.photos]
+ self.assertIn(ret_val.pathOriginal, pathOriginals)
+
+ # Delete all photos in one go
+ self.client.photos.delete(self.photos)
+
+ # Check they're gone
+ self.photos = self.client.photos.list()
+ self.assertEqual(len(self.photos), 0)
+
+ # Regenerate the original test photos
+ self._delete_all()
+ self._create_test_photos()
+
+ def test_edit(self):
+ """ Check that the edit request returns an HTML form """
+ # Test using the OpenPhoto class
+ html = self.client.photo.edit(self.photos[0])
+ self.assertIn("