"""Tests for micronian.webapp
http://www.decafbad.com
mailto:l.m.orchard@pobox.com
Share and Enjoy
"""
# TODO: More testing / implementation of XPath-based access
# TODO: Assert the need for content-type: text/xml in most methods
# TODO: Test out page name and node ID sanitizing
import sys, unittest, os.path
from md5 import md5
from StringIO import StringIO
from random import random
from lxml import etree
from wsgi.stub import WSGIStub
from micronian.page import Page, NS
from micronian.store.filesystem import FilesystemStore
from micronian.webapp import WebApp
class TestCase(unittest.TestCase):
def __init__(self, name):
unittest.TestCase.__init__(self, name)
def setUp(self):
# Do this in __init__ to exercise run_once=False?
store_root = "data/test"
self.store = FilesystemStore(store_root)
self.app = WebApp(self.store)
self.uri = "http://localhost/wiki"
self.stub = WSGIStub(self.app)
self.stub.SCRIPT_NAME = "/wiki"
self.init_src = """
This is a test
Mauris libero. Suspendisse magna urna, nonummy
suscipit, mattis quis, molestie non, erat. Duis eu
risus. Vivamus nec pede. Cras sit amet dui. Ut nec
arcu sit amet dui bibendum volutpat.
""" % NS['xhtml']
def assertHeaders(self, expect_status, expect_headers, body=None):
"""Assert a status, set of headers, and response body."""
stub, uri = self.stub, self.uri
self.assertEqual(expect_status, stub.status,
"Status %s expected, %s / %s found." % \
(expect_status, stub.status, stub.body) )
self.assertEqual(expect_headers, stub.response_headers,
"Headers %s expected, %s found." % \
(expect_headers, stub.response_headers) )
def testCreateApp(self):
"""Try creating a stub with the web app."""
stub = WSGIStub(WebApp(self.store))
def testCreateNoName(self):
"""Try creating a page without a name."""
stub, uri = self.stub, self.uri
# Fire off the PUT request
stub.run(method="PUT", uri=uri)
# Double check the response headers.
expect_status = '405 Method Not Allowed'
expect_headers = [ ('Content-Type', 'text/xml') ]
self.assertHeaders(expect_status, expect_headers)
# Double check the XML in the response body.
out_root = etree.parse(StringIO(stub.body)).getroot()
expect_root = '{%s}error' % NS['micronian']
self.assertEqual(out_root.tag, expect_root,
'Expected %s, found %s' % (expect_root, out_root.tag))
self.assertEqual(out_root.get('status'), '405',
'Expected status="405", found status="%s".' % \
out_root.get('status'))
def testCreateNewBlank(self):
"""Try creating a new blank page."""
stub, uri = self.stub, self.uri
# Fire off the PUT request
page_name = 'TestPage%s' % (int(random()*100000))
stub.run(
method="PUT",
uri='%s/%s' % (uri, page_name)
)
# Double check the response headers.
expect_status = '201 Created'
expect_headers = [
('Content-Type', 'text/xml'),
('Location', '/wiki/%s' % page_name)
]
self.assertHeaders(expect_status, expect_headers)
# Expect to see the new page in the store
self.assert_(page_name in self.store.list(),
"Expected to find new page in store list.")
# Double check the XML in the response body.
out_root = etree.parse(StringIO(stub.body)).getroot()
expect_root = '{%s}created' % NS['micronian']
self.assertEqual(out_root.tag, expect_root,
'Expected %s, found %s' % (expect_root, out_root.tag))
self.assertEqual(out_root.get('status'), '201',
'Expected status="201", found status="%s".' % \
out_root.get('status'))
def testCreateNewWithContent(self):
"""Try creating a new page with initial content."""
stub, uri = self.stub, self.uri
# Fire off the PUT request
page_name = 'TestPage%s' % (int(random()*100000))
stub.run(
method="PUT",
uri='%s/%s' % (uri, page_name),
stdin=StringIO(self.init_src)
)
# Double check the response status and headers.
expect_status = '201 Created'
expect_headers = [
('Content-Type', 'text/xml'),
('Location', '/wiki/%s' % page_name)
]
self.assertHeaders(expect_status, expect_headers)
# Expect to see the new page in the store
self.assert_(page_name in self.store.list(),
"Expected to find new page in store list.")
# Double check the XML in the response body.
out_root = etree.parse(StringIO(stub.body)).getroot()
expect_root = '{%s}created' % NS['micronian']
self.assertEqual(out_root.tag, expect_root,
'Expected %s, found %s' % (expect_root, out_root.tag))
self.assertEqual(out_root.get('status'), '201',
'Expected status="201", found status="%s".' % \
out_root.get('status'))
# Double check the stored page.
stored = self.store.fetch(page_name)
div = stored.doc.xpath('//xhtml:body/xhtml:div', NS)[0]
self.assert_(div[0].tag == '{%s}h1' % NS['xhtml'],
"First child of page's first div should be broken, no?")
)
# Double check the response status and headers.
expect_status = '400 Bad Request'
expect_headers = [ ('Content-Type', 'text/xml') ]
self.assertHeaders(expect_status, expect_headers)
# Double check the XML in the response body.
out_root = etree.parse(StringIO(stub.body)).getroot()
expect_root = '{%s}error' % NS['micronian']
self.assertEqual(out_root.tag, expect_root,
'Expected %s, found %s' % (expect_root, out_root.tag))
self.assertEqual(out_root.get('status'), '400',
'Expected status="400", found status="%s".' % \
out_root.get('status'))
def testUpdate(self):
"""Try updating a page's content."""
stub, uri = self.stub, self.uri
# Create an initial page.
page_name = 'PageToBeUpdated'
page = Page(page_name)
page.replace("This should go away.
",
'/xhtml:div[1]')
self.store.store(page_name, page)
# Fire off the PUT request
stub.run(
method="PUT",
uri='%s/%s' % (uri, page_name),
stdin=StringIO(self.init_src)
)
# Double check the response status and headers.
expect_status = '200 OK'
expect_headers = [
('Content-Type', 'text/xml'),
('Location', '/wiki/%s' % page_name)
]
self.assertHeaders(expect_status, expect_headers)
# Double check the XML in the response body.
out_root = etree.parse(StringIO(stub.body)).getroot()
expect_root = '{%s}updated' % NS['micronian']
self.assertEqual(out_root.tag, expect_root,
'Expected %s, found %s' % (expect_root, out_root.tag))
self.assertEqual(out_root.get('status'), '200',
'Expected status="200", found status="%s".' % \
out_root.get('status'))
# Double check the stored page.
stored = self.store.fetch(page_name)
div = stored.doc.xpath('//xhtml:body/xhtml:div', NS)[0]
self.assert_(div[0].tag != '{%s}h2' % NS['xhtml'],
"First child of page's first div should not be ")
self.assert_(div[0].text != 'This should go away.',
"Text of first child didn't change.")
def testUpdateSubnodes(self):
stub, uri = self.stub, self.uri
# Create an initial page.
page_name = 'PageToBeSubUpdated'
page = Page(page_name)
page.append("""
This is the first paragraph.
This is the second paragraph.
This is the third paragraph.
This is the fourth paragraph.
""" % NS['xhtml'])
self.store.store(page_name, page)
# Update the third paragraph by ID shortcut
third_para = \
page.doc.xpath('//xhtml:div[2]/xhtml:p[3]', NS)[0]
third_para_id = third_para.get('id')
src1 = "New third paragraph goes here.
"
stub.run(
method="PUT",
uri="%s/%s$%s" % (uri, page_name, third_para_id),
stdin=StringIO(src1)
)
# Double check the response status and headers.
expect_status = '200 OK'
expect_headers = [
('Content-Type', 'text/xml'),
('Location', '/wiki/%s' % page_name)
]
self.assertHeaders(expect_status, expect_headers)
# Double check the XML in the response body.
out_root = etree.parse(StringIO(stub.body)).getroot()
expect_root = '{%s}updated' % NS['micronian']
self.assertEqual(out_root.tag, expect_root,
'Expected %s, found %s' % (expect_root, out_root.tag))
self.assertEqual(out_root.get('status'), '200',
'Expected status="200", found status="%s".' % \
out_root.get('status'))
# Update the second paragraph via path.
src2 = "New second paragraph goes here.
"
stub.run(
method="PUT",
uri="%s/%s/xhtml:div[2]/xhtml:p[2]" % (uri, page_name),
stdin=StringIO(src2)
)
# Double check the response status and headers.
expect_status = '200 OK'
expect_headers = [
('Content-Type', 'text/xml'),
('Location', '/wiki/%s' % page_name)
]
self.assertHeaders(expect_status, expect_headers)
# Double check the XML in the response body.
out_root = etree.parse(StringIO(stub.body)).getroot()
expect_root = '{%s}updated' % NS['micronian']
self.assertEqual(out_root.tag, expect_root,
'Expected %s, found %s' % (expect_root, out_root.tag))
self.assertEqual(out_root.get('status'), '200',
'Expected status="200", found status="%s".' % \
out_root.get('status'))
# Double check the stored page.
stored = self.store.fetch(page_name)
paras = stored.doc.xpath\
('//xhtml:body/xhtml:div[2]/xhtml:p', NS)
self.assertEqual(paras[1].text,
"New second paragraph goes here.")
self.assertEqual(paras[2].text,
"New third paragraph goes here.")
def testDeleteNoName(self):
"""Try creating a page without a name."""
stub, uri = self.stub, self.uri
# Fire off the PUT request
stub.run(method="DELETE", uri=uri)
# Double check the response headers.
expect_status = '405 Method Not Allowed'
expect_headers = [ ('Content-Type', 'text/xml') ]
self.assertHeaders(expect_status, expect_headers)
# Double check the XML in the response body.
out_root = etree.parse(StringIO(stub.body)).getroot()
expect_root = '{%s}error' % NS['micronian']
self.assertEqual(out_root.tag, expect_root,
'Expected %s, found %s' % (expect_root, out_root.tag))
self.assertEqual(out_root.get('status'), '405',
'Expected status="405", found status="%s".' % \
out_root.get('status'))
def testDelete(self):
stub, uri = self.stub, self.uri
# Create an initial page.
page_name = 'PageToBeDeleted'
page = Page(page_name)
page.replace("This should go away.
",
'/xhtml:div[1]')
self.store.store(page_name, page)
# Fire off the DELETE request
stub.run(
method="DELETE",
uri='%s/%s' % (uri, page_name),
stdin=StringIO(self.init_src)
)
# Double check the response status and headers.
expect_status = '410 Gone'
expect_headers = [
('Content-Type', 'text/xml'),
]
self.assertHeaders(expect_status, expect_headers)
# Expect to see the new page missing from the store
self.assert_(page_name not in self.store.list(),
"Expected to find new page not in store list.")
def testDeleteSubnode(self):
stub, uri = self.stub, self.uri
# Create an initial page.
page_name = 'TestPage%s' % (int(random()*100000))
page = Page(page_name)
page.append("""
This is the first paragraph.
This is the second paragraph.
This is the third paragraph.
This is the fourth paragraph.
""" % NS['xhtml'])
self.store.store(page_name, page)
# Update the third paragraph by ID shortcut
third_para = \
page.doc.xpath('//xhtml:div[2]/xhtml:p[3]', NS)[0]
third_para_id = third_para.get('id')
# Fire off the GET request
stub.run(
method="DELETE",
uri='%s/%s$%s' % (uri, page_name, third_para_id),
)
# Double check the response status and headers.
expect_status = '410 Gone'
expect_headers = [ ('Content-Type', 'text/xml') ]
self.assertHeaders(expect_status, expect_headers)
# Double check the third paragraph is missing
page = self.store.fetch(page_name)
def do_lookup(x):
page.get("//*[@id='%s']" % third_para_id)
self.assertRaises(KeyError, do_lookup,
"Lookup on third paragraph key should throw KeyError.")
# Check that there are only 3 of the 4 paras left.
paras = page.doc.xpath('//xhtml:div[2]/xhtml:p', NS)
self.assertEqual(len(paras), 3,
'Should have found 3 paras, found %s.' % len(paras))
def testGet(self):
"""Try getting a page from the store."""
stub, uri = self.stub, self.uri
# Create an initial page.
page_name = 'TestPage%s' % (int(random()*100000))
page = Page(page_name)
page.append("""
This is the first paragraph.
This is the second paragraph.
This is the third paragraph.
This is the fourth paragraph.
""" % NS['xhtml'])
self.store.store(page_name, page)
# Fire off the GET request
stub.run(
method="GET",
uri='%s/%s' % (uri, page_name),
)
# Double check the response status and headers.
expect_status = '200 OK'
expect_headers = [ ('Content-Type', 'text/xml') ]
self.assertHeaders(expect_status, expect_headers)
# Double check the contents of the page fetched
fetched_page = Page(page_name, stub.body)
self.assertEqual(page.toString(), fetched_page.toString(),
"Stored page and fetched page don't match.")
def testGetSubnode(self):
"""Try getting a page subnode from the store."""
stub, uri = self.stub, self.uri
# Create an initial page.
page_name = 'TestPage%s' % (int(random()*100000))
page = Page(page_name)
page.append("""
This is the first paragraph.
This is the second paragraph.
This is the third paragraph.
This is the fourth paragraph.
""" % NS['xhtml'])
self.store.store(page_name, page)
# Update the third paragraph by ID shortcut
third_para = \
page.doc.xpath('//xhtml:div[2]/xhtml:p[3]', NS)[0]
third_para_id = third_para.get('id')
# Fire off the GET request
stub.run(
method="GET",
uri='%s/%s$%s' % (uri, page_name, third_para_id),
)
# Double check the response status and headers.
expect_status = '200 OK'
expect_headers = [ ('Content-Type', 'text/xml') ]
self.assertHeaders(expect_status, expect_headers)
# Double check the third paragraph tag and text
fetched_ele = etree.parse(StringIO(stub.body)).getroot()
self.assertEqual(third_para.tag, fetched_ele.tag,
"Third para (%s) and fetched ele (%s) tag mismatch." % \
(third_para.tag, fetched_ele.tag) )
self.assertEqual(third_para.text, fetched_ele.text,
"Third para (%s) and fetched ele (%s) text mismatch." % \
(third_para.text, fetched_ele.text) )
def testGetIndex(self):
"""Try adding some pages and look for them in the index."""
stub, uri = self.stub, self.uri
# Create a few pages.
page_names = []
for x in range(5):
page_name = 'TestPage%s' % (int(random()*100000))
page = Page(page_name)
self.store.store(page_name, page)
page_names.append(page_name)
# Fire off the GET request.
stub.run(method="GET", uri=uri)
# Double check the response status and headers.
expect_status = '200 OK'
expect_headers = [ ('Content-Type', 'text/xml') ]
self.assertHeaders(expect_status, expect_headers)
# Parse the page index sent in response
doc = etree.parse(StringIO(stub.body))
page_lis = doc.xpath('//xhtml:li', NS)
# The index should be an XHTML list.
self.assert_(len(page_lis) > 0,
"Should have found some list items.")
# Match up all the found pages with the created pages.
for page_li in page_lis:
page_a = page_li[0]
if page_a.get('href') in page_names:
page_names.remove(page_a.get('href'))
# All of the added pages should have been found
self.assertEqual(len(page_names), 0,
"Should've found all of the added pages in the index,"+\
" %s left." % (len(page_names)))
def testInsertInvalidSubnode(self):
stub, uri = self.stub, self.uri
# Create an initial page.
page_name = 'PageForInsertion'
page = Page(page_name)
self.store.store(page_name, page)
# Fire off the POST request
stub.run(
method="POST",
uri='%s/%s' % (uri, page_name),
environ={ 'CONTENT_TYPE' : 'text/xml' },
stdin=StringIO("""<&&&broken><<
This is a new block.
""" % NS['xhtml'])
)
# Double check the response status and headers.
expect_status = '200 OK'
expect_headers = [
('Content-Type', 'text/xml'),
('Location', '/wiki/%s' % page_name)
]
self.assertHeaders(expect_status, expect_headers)
# Double check the XML in the response body.
out_root = etree.parse(StringIO(stub.body)).getroot()
expect_root = '{%s}appended' % NS['micronian']
self.assertEqual(out_root.tag, expect_root,
'Expected %s, found %s' % (expect_root, out_root.tag))
self.assertEqual(out_root.get('status'), '200',
'Expected status="200", found status="%s".' % \
out_root.get('status'))
# Double check the stored page.
stored = self.store.fetch(page_name)
div = stored.doc.xpath('//xhtml:body/xhtml:div[2]', NS)[0]
self.assert_(div[0].tag == '{%s}p' % NS['xhtml'],
"Should have found a second div paragraph.")
self.assert_(div[0].text == 'This is a new block.',
"Should have found correct text in paragraph.")
def testInsertBeforeSubnode(self):
stub, uri = self.stub, self.uri
# Create an initial page.
page_name = 'PageForInsertion'
page = Page(page_name)
page.append("""
""" % NS['xhtml'])
self.store.store(page_name, page)
# Find an ID for the first DIV on the page.
second_div = \
page.doc.xpath('//xhtml:div[2]', NS)[0]
second_div_id = second_div.get('id')
# Fire off the POST request
stub.run(
method="POST",
uri='%s/%s$%s' % (uri, page_name, second_div_id),
environ={ 'CONTENT_TYPE' : 'text/xml' },
stdin=StringIO("""
This should be the second DIV.
""" % NS['xhtml'])
)
# Double check the response status and headers.
expect_status = '200 OK'
expect_headers = [
('Content-Type', 'text/xml'),
# TODO: Location should include the new element's ID
('Location', '/wiki/%s' % page_name)
]
self.assertHeaders(expect_status, expect_headers)
# Double check the XML in the response body.
out_root = etree.parse(StringIO(stub.body)).getroot()
expect_root = '{%s}inserted' % NS['micronian']
self.assertEqual(out_root.tag, expect_root,
'Expected %s, found %s' % (expect_root, out_root.tag))
self.assertEqual(out_root.get('status'), '200',
'Expected status="200", found status="%s".' % \
out_root.get('status'))
# Double check the stored page.
stored = self.store.fetch(page_name)
div1 = stored.doc.xpath('//xhtml:body/xhtml:div[2]', NS)[0]
self.assert_(div1[0].tag == '{%s}p' % NS['xhtml'],
"Should have found a second div paragraph.")
self.assert_(div1[0].text == 'This should be the second DIV.',
"Should have found correct text in paragraph.")
div2 = stored.doc.xpath('//xhtml:body/xhtml:div[3]', NS)[0]
self.assert_(div2[0].tag == '{%s}p' % NS['xhtml'],
"Should have found a second div paragraph.")
self.assert_(div2[0].text == 'This should come last.',
"Should have found correct text in paragraph.")
def testSafariHTTPMethodHack(self):
"""Try a PUT request proxied as a PUT request."""
# TODO: Try out some other non-POST requests.
stub, uri = self.stub, self.uri
# Fire off the PUT request
page_name = 'TestPage%s' % (int(random()*100000))
stub.run(
method="POST",
uri='%s/%s?method=PUT' % (uri, page_name)
)
# Double check the response headers.
expect_status = '201 Created'
expect_headers = [
('Content-Type', 'text/xml'),
('Location', '/wiki/%s' % page_name)
]
self.assertHeaders(expect_status, expect_headers)
# Expect to see the new page in the store
self.assert_(page_name in self.store.list(),
"Expected to find new page in store list.")
# Double check the XML in the response body.
out_root = etree.parse(StringIO(stub.body)).getroot()
expect_root = '{%s}created' % NS['micronian']
self.assertEqual(out_root.tag, expect_root,
'Expected %s, found %s' % (expect_root, out_root.tag))
self.assertEqual(out_root.get('status'), '201',
'Expected status="201", found status="%s".' % \
out_root.get('status'))
if __name__ == '__main__': unittest.main()