]> git.rkrishnan.org Git - tahoe-lafs/tahoe-lafs.git/blobdiff - src/allmydata/test/test_web.py
Let Uploader retain History instead of passing it into upload(). Fixes #1079.
[tahoe-lafs/tahoe-lafs.git] / src / allmydata / test / test_web.py
index a7f882f33f3e19a3b45c29338073f42040c8ef4f..3a115ba8ccc993b9b7f596deb4c9dbf0cb0d647a 100644 (file)
-import os.path, re, urllib
+import os.path, re, urllib, time
 import simplejson
 from StringIO import StringIO
 from twisted.application import service
 from twisted.trial import unittest
 from twisted.internet import defer, reactor
+from twisted.internet.task import Clock
 from twisted.web import client, error, http
 from twisted.python import failure, log
-from allmydata import interfaces, uri, webish
-from allmydata.storage.mutable import MutableShareFile
-from allmydata.storage.immutable import ShareFile
-from allmydata.immutable import upload, download
+from nevow import rend
+from allmydata import interfaces, uri, webish, dirnode
+from allmydata.storage.shares import get_share_file
+from allmydata.storage_client import StorageFarmBroker
+from allmydata.immutable import upload
+from allmydata.immutable.downloader.status import DownloadStatus
+from allmydata.dirnode import DirectoryNode
+from allmydata.nodemaker import NodeMaker
+from allmydata.unknown import UnknownNode
 from allmydata.web import status, common
 from allmydata.scripts.debug import CorruptShareOptions, corrupt_share
-from allmydata.util import fileutil, base32
-from allmydata.util.assertutil import precondition
-from allmydata.test.common import FakeDirectoryNode, FakeCHKFileNode, \
-     FakeMutableFileNode, create_chk_filenode, WebErrorMixin, ShouldFailMixin
-from allmydata.interfaces import IURI, INewDirectoryURI, \
-     IReadonlyNewDirectoryURI, IFileURI, IMutableFileURI, IMutableFileNode
+from allmydata.util import fileutil, base32, hashutil
+from allmydata.util.consumer import download_to_data
+from allmydata.util.netstring import split_netstring
+from allmydata.util.encodingutil import to_str
+from allmydata.test.common import FakeCHKFileNode, FakeMutableFileNode, \
+     create_chk_filenode, WebErrorMixin, ShouldFailMixin, \
+     make_mutable_file_uri, create_mutable_filenode
+from allmydata.interfaces import IMutableFileNode, SDMF_VERSION, MDMF_VERSION
 from allmydata.mutable import servermap, publish, retrieve
-import common_util as testutil
+import allmydata.test.common_util as testutil
 from allmydata.test.no_network import GridTestMixin
-
 from allmydata.test.common_web import HTTPClientGETFactory, \
      HTTPClientHEADFactory
+from allmydata.client import Client, SecretHolder
 
 # create a fake uploader/downloader, and a couple of fake dirnodes, then
 # create a webserver that works against them
 
-class FakeIntroducerClient:
-    def get_all_connectors(self):
-        return {}
-    def get_all_connections_for(self, service_name):
-        return frozenset()
-    def get_all_peerids(self):
-        return frozenset()
+timeout = 480 # Most of these take longer than 240 seconds on Francois's arm box.
+
+unknown_rwcap = u"lafs://from_the_future_rw_\u263A".encode('utf-8')
+unknown_rocap = u"ro.lafs://readonly_from_the_future_ro_\u263A".encode('utf-8')
+unknown_immcap = u"imm.lafs://immutable_from_the_future_imm_\u263A".encode('utf-8')
 
 class FakeStatsProvider:
     def get_stats(self):
         stats = {'stats': {}, 'counters': {}}
         return stats
 
-class FakeClient(service.MultiService):
-    nodeid = "fake_nodeid"
-    nickname = "fake_nickname"
-    basedir = "fake_basedir"
-    def get_versions(self):
-        return {'allmydata': "fake",
-                'foolscap': "fake",
-                'twisted': "fake",
-                'zfec': "fake",
-                }
-    introducer_furl = "None"
-    introducer_client = FakeIntroducerClient()
-    _all_upload_status = [upload.UploadStatus()]
-    _all_download_status = [download.DownloadStatus()]
-    _all_mapupdate_statuses = [servermap.UpdateStatus()]
-    _all_publish_statuses = [publish.PublishStatus()]
-    _all_retrieve_statuses = [retrieve.RetrieveStatus()]
-    convergence = "some random string"
-    stats_provider = FakeStatsProvider()
-
-    def connected_to_introducer(self):
-        return False
-
-    def get_nickname_for_peerid(self, peerid):
-        return u"John Doe"
-
-    def get_permuted_peers(self, service_name, key):
-        return []
-
-    def create_node_from_uri(self, auri):
-        precondition(isinstance(auri, str), auri)
-        u = uri.from_string(auri)
-        if (INewDirectoryURI.providedBy(u)
-            or IReadonlyNewDirectoryURI.providedBy(u)):
-            return FakeDirectoryNode(self).init_from_uri(u)
-        if IFileURI.providedBy(u):
-            return FakeCHKFileNode(u, self)
-        assert IMutableFileURI.providedBy(u), u
-        return FakeMutableFileNode(self).init_from_uri(u)
-
-    def create_empty_dirnode(self):
-        n = FakeDirectoryNode(self)
-        d = n.create()
-        d.addCallback(lambda res: n)
-        return d
-
-    MUTABLE_SIZELIMIT = FakeMutableFileNode.MUTABLE_SIZELIMIT
-    def create_mutable_file(self, contents=""):
-        n = FakeMutableFileNode(self)
-        return n.create(contents)
-
+class FakeNodeMaker(NodeMaker):
+    encoding_params = {
+        'k': 3,
+        'n': 10,
+        'happy': 7,
+        'max_segment_size':128*1024 # 1024=KiB
+    }
+    def _create_lit(self, cap):
+        return FakeCHKFileNode(cap)
+    def _create_immutable(self, cap):
+        return FakeCHKFileNode(cap)
+    def _create_mutable(self, cap):
+        return FakeMutableFileNode(None,
+                                   None,
+                                   self.encoding_params, None).init_from_cap(cap)
+    def create_mutable_file(self, contents="", keysize=None,
+                            version=SDMF_VERSION):
+        n = FakeMutableFileNode(None, None, self.encoding_params, None)
+        return n.create(contents, version=version)
+
+class FakeUploader(service.Service):
+    name = "uploader"
     def upload(self, uploadable):
         d = uploadable.get_size()
         d.addCallback(lambda size: uploadable.read(size))
         def _got_data(datav):
             data = "".join(datav)
-            n = create_chk_filenode(self, data)
+            n = create_chk_filenode(data)
             results = upload.UploadResults()
             results.uri = n.get_uri()
             return results
         d.addCallback(_got_data)
         return d
+    def get_helper_info(self):
+        return (None, False)
+
+class FakeIServer:
+    def __init__(self, binaryserverid):
+        self.binaryserverid = binaryserverid
+    def get_name(self): return "short"
+    def get_longname(self): return "long"
+    def get_serverid(self): return self.binaryserverid
+
+def build_one_ds():
+    ds = DownloadStatus("storage_index", 1234)
+    now = time.time()
+
+    serverA = FakeIServer(hashutil.tagged_hash("foo", "serverid_a")[:20])
+    serverB = FakeIServer(hashutil.tagged_hash("foo", "serverid_b")[:20])
+    storage_index = hashutil.storage_index_hash("SI")
+    e0 = ds.add_segment_request(0, now)
+    e0.activate(now+0.5)
+    e0.deliver(now+1, 0, 100, 0.5) # when, start,len, decodetime
+    e1 = ds.add_segment_request(1, now+2)
+    e1.error(now+3)
+    # two outstanding requests
+    e2 = ds.add_segment_request(2, now+4)
+    e3 = ds.add_segment_request(3, now+5)
+    del e2,e3 # hush pyflakes
+
+    # simulate a segment which gets delivered faster than a system clock tick (ticket #1166)
+    e = ds.add_segment_request(4, now)
+    e.activate(now)
+    e.deliver(now, 0, 140, 0.5)
+
+    e = ds.add_dyhb_request(serverA, now)
+    e.finished([1,2], now+1)
+    e = ds.add_dyhb_request(serverB, now+2) # left unfinished
+
+    e = ds.add_read_event(0, 120, now)
+    e.update(60, 0.5, 0.1) # bytes, decrypttime, pausetime
+    e.finished(now+1)
+    e = ds.add_read_event(120, 30, now+2) # left unfinished
+
+    e = ds.add_block_request(serverA, 1, 100, 20, now)
+    e.finished(20, now+1)
+    e = ds.add_block_request(serverB, 1, 120, 30, now+1) # left unfinished
+
+    # make sure that add_read_event() can come first too
+    ds1 = DownloadStatus(storage_index, 1234)
+    e = ds1.add_read_event(0, 120, now)
+    e.update(60, 0.5, 0.1) # bytes, decrypttime, pausetime
+    e.finished(now+1)
+
+    return ds
+
+class FakeHistory:
+    _all_upload_status = [upload.UploadStatus()]
+    _all_download_status = [build_one_ds()]
+    _all_mapupdate_statuses = [servermap.UpdateStatus()]
+    _all_publish_statuses = [publish.PublishStatus()]
+    _all_retrieve_statuses = [retrieve.RetrieveStatus()]
 
     def list_all_upload_statuses(self):
         return self._all_upload_status
@@ -117,17 +153,51 @@ class FakeClient(service.MultiService):
     def list_all_helper_statuses(self):
         return []
 
+class FakeClient(Client):
+    def __init__(self):
+        # don't upcall to Client.__init__, since we only want to initialize a
+        # minimal subset
+        service.MultiService.__init__(self)
+        self.nodeid = "fake_nodeid"
+        self.nickname = "fake_nickname"
+        self.introducer_furl = "None"
+        self.stats_provider = FakeStatsProvider()
+        self._secret_holder = SecretHolder("lease secret", "convergence secret")
+        self.helper = None
+        self.convergence = "some random string"
+        self.storage_broker = StorageFarmBroker(None, permute_peers=True)
+        self.introducer_client = None
+        self.history = FakeHistory()
+        self.uploader = FakeUploader()
+        self.uploader.setServiceParent(self)
+        self.blacklist = None
+        self.nodemaker = FakeNodeMaker(None, self._secret_holder, None,
+                                       self.uploader, None,
+                                       None, None)
+        self.mutable_file_default = SDMF_VERSION
+
+    def startService(self):
+        return service.MultiService.startService(self)
+    def stopService(self):
+        return service.MultiService.stopService(self)
+
+    MUTABLE_SIZELIMIT = FakeMutableFileNode.MUTABLE_SIZELIMIT
+
 class WebMixin(object):
     def setUp(self):
         self.s = FakeClient()
         self.s.startService()
         self.staticdir = self.mktemp()
-        self.ws = s = webish.WebishServer(self.s, "0", staticdir=self.staticdir)
-        s.setServiceParent(self.s)
-        self.webish_port = port = s.listener._port.getHost().port
-        self.webish_url = "http://localhost:%d" % port
-
-        l = [ self.s.create_empty_dirnode() for x in range(6) ]
+        self.clock = Clock()
+        self.ws = webish.WebishServer(self.s, "0", staticdir=self.staticdir,
+                                      clock=self.clock)
+        self.ws.setServiceParent(self.s)
+        self.webish_port = self.ws.getPortnum()
+        self.webish_url = self.ws.getURL()
+        assert self.webish_url.endswith("/")
+        self.webish_url = self.webish_url[:-1] # these tests add their own /
+
+        l = [ self.s.create_dirnode() for x in range(6) ]
         d = defer.DeferredList(l)
         def _then(res):
             self.public_root = res[0][1]
@@ -142,41 +212,57 @@ class WebMixin(object):
             self._foo_verifycap = foo.get_verify_cap().to_string()
             # NOTE: we ignore the deferred on all set_uri() calls, because we
             # know the fake nodes do these synchronously
-            self.public_root.set_uri(u"foo", foo.get_uri())
+            self.public_root.set_uri(u"foo", foo.get_uri(),
+                                     foo.get_readonly_uri())
 
             self.BAR_CONTENTS, n, self._bar_txt_uri = self.makefile(0)
-            foo.set_uri(u"bar.txt", self._bar_txt_uri)
+            foo.set_uri(u"bar.txt", self._bar_txt_uri, self._bar_txt_uri)
             self._bar_txt_verifycap = n.get_verify_cap().to_string()
 
-            foo.set_uri(u"empty", res[3][1].get_uri())
+            # sdmf
+            # XXX: Do we ever use this?
+            self.BAZ_CONTENTS, n, self._baz_txt_uri, self._baz_txt_readonly_uri = self.makefile_mutable(0)
+
+            foo.set_uri(u"baz.txt", self._baz_txt_uri, self._baz_txt_readonly_uri)
+
+            # mdmf
+            self.QUUX_CONTENTS, n, self._quux_txt_uri, self._quux_txt_readonly_uri = self.makefile_mutable(0, mdmf=True)
+            assert self._quux_txt_uri.startswith("URI:MDMF")
+            foo.set_uri(u"quux.txt", self._quux_txt_uri, self._quux_txt_readonly_uri)
+
+            foo.set_uri(u"empty", res[3][1].get_uri(),
+                        res[3][1].get_readonly_uri())
             sub_uri = res[4][1].get_uri()
             self._sub_uri = sub_uri
-            foo.set_uri(u"sub", sub_uri)
+            foo.set_uri(u"sub", sub_uri, sub_uri)
             sub = self.s.create_node_from_uri(sub_uri)
 
             _ign, n, blocking_uri = self.makefile(1)
-            foo.set_uri(u"blockingfile", blocking_uri)
+            foo.set_uri(u"blockingfile", blocking_uri, blocking_uri)
 
             unicode_filename = u"n\u00fc.txt" # n u-umlaut . t x t
             # ok, unicode calls it LATIN SMALL LETTER U WITH DIAERESIS but I
             # still think of it as an umlaut
-            foo.set_uri(unicode_filename, self._bar_txt_uri)
+            foo.set_uri(unicode_filename, self._bar_txt_uri, self._bar_txt_uri)
 
             _ign, n, baz_file = self.makefile(2)
             self._baz_file_uri = baz_file
-            sub.set_uri(u"baz.txt", baz_file)
+            sub.set_uri(u"baz.txt", baz_file, baz_file)
 
             _ign, n, self._bad_file_uri = self.makefile(3)
             # this uri should not be downloadable
             del FakeCHKFileNode.all_contents[self._bad_file_uri]
 
             rodir = res[5][1]
-            self.public_root.set_uri(u"reedownlee", rodir.get_readonly_uri())
-            rodir.set_uri(u"nor", baz_file)
+            self.public_root.set_uri(u"reedownlee", rodir.get_readonly_uri(),
+                                     rodir.get_readonly_uri())
+            rodir.set_uri(u"nor", baz_file, baz_file)
 
             # public/
             # public/foo/
             # public/foo/bar.txt
+            # public/foo/baz.txt
+            # public/foo/quux.txt
             # public/foo/blockingfile
             # public/foo/empty/
             # public/foo/sub/
@@ -194,25 +280,55 @@ class WebMixin(object):
 
     def makefile(self, number):
         contents = "contents of file %s\n" % number
-        n = create_chk_filenode(self.s, contents)
+        n = create_chk_filenode(contents)
         return contents, n, n.get_uri()
 
+    def makefile_mutable(self, number, mdmf=False):
+        contents = "contents of mutable file %s\n" % number
+        n = create_mutable_filenode(contents, mdmf)
+        return contents, n, n.get_uri(), n.get_readonly_uri()
+
     def tearDown(self):
         return self.s.stopService()
 
     def failUnlessIsBarDotTxt(self, res):
-        self.failUnlessEqual(res, self.BAR_CONTENTS, res)
+        self.failUnlessReallyEqual(res, self.BAR_CONTENTS, res)
+
+    def failUnlessIsQuuxDotTxt(self, res):
+        self.failUnlessReallyEqual(res, self.QUUX_CONTENTS, res)
+
+    def failUnlessIsBazDotTxt(self, res):
+        self.failUnlessReallyEqual(res, self.BAZ_CONTENTS, res)
 
     def failUnlessIsBarJSON(self, res):
         data = simplejson.loads(res)
         self.failUnless(isinstance(data, list))
-        self.failUnlessEqual(data[0], u"filenode")
+        self.failUnlessEqual(data[0], "filenode")
         self.failUnless(isinstance(data[1], dict))
         self.failIf(data[1]["mutable"])
         self.failIf("rw_uri" in data[1]) # immutable
-        self.failUnlessEqual(data[1]["ro_uri"], self._bar_txt_uri)
-        self.failUnlessEqual(data[1]["verify_uri"], self._bar_txt_verifycap)
-        self.failUnlessEqual(data[1]["size"], len(self.BAR_CONTENTS))
+        self.failUnlessReallyEqual(to_str(data[1]["ro_uri"]), self._bar_txt_uri)
+        self.failUnlessReallyEqual(to_str(data[1]["verify_uri"]), self._bar_txt_verifycap)
+        self.failUnlessReallyEqual(data[1]["size"], len(self.BAR_CONTENTS))
+
+    def failUnlessIsQuuxJSON(self, res, readonly=False):
+        data = simplejson.loads(res)
+        self.failUnless(isinstance(data, list))
+        self.failUnlessEqual(data[0], "filenode")
+        self.failUnless(isinstance(data[1], dict))
+        metadata = data[1]
+        return self.failUnlessIsQuuxDotTxtMetadata(metadata, readonly)
+
+    def failUnlessIsQuuxDotTxtMetadata(self, metadata, readonly):
+        self.failUnless(metadata['mutable'])
+        if readonly:
+            self.failIf("rw_uri" in metadata)
+        else:
+            self.failUnless("rw_uri" in metadata)
+            self.failUnlessEqual(metadata['rw_uri'], self._quux_txt_uri)
+        self.failUnless("ro_uri" in metadata)
+        self.failUnlessEqual(metadata['ro_uri'], self._quux_txt_readonly_uri)
+        self.failUnlessReallyEqual(metadata['size'], len(self.QUUX_CONTENTS))
 
     def failUnlessIsFooJSON(self, res):
         data = simplejson.loads(res)
@@ -221,30 +337,39 @@ class WebMixin(object):
         self.failUnless(isinstance(data[1], dict))
         self.failUnless(data[1]["mutable"])
         self.failUnless("rw_uri" in data[1]) # mutable
-        self.failUnlessEqual(data[1]["rw_uri"], self._foo_uri)
-        self.failUnlessEqual(data[1]["ro_uri"], self._foo_readonly_uri)
-        self.failUnlessEqual(data[1]["verify_uri"], self._foo_verifycap)
+        self.failUnlessReallyEqual(to_str(data[1]["rw_uri"]), self._foo_uri)
+        self.failUnlessReallyEqual(to_str(data[1]["ro_uri"]), self._foo_readonly_uri)
+        self.failUnlessReallyEqual(to_str(data[1]["verify_uri"]), self._foo_verifycap)
 
         kidnames = sorted([unicode(n) for n in data[1]["children"]])
         self.failUnlessEqual(kidnames,
-                             [u"bar.txt", u"blockingfile", u"empty",
-                              u"n\u00fc.txt", u"sub"])
+                             [u"bar.txt", u"baz.txt", u"blockingfile",
+                              u"empty", u"n\u00fc.txt", u"quux.txt", u"sub"])
         kids = dict( [(unicode(name),value)
                       for (name,value)
                       in data[1]["children"].iteritems()] )
         self.failUnlessEqual(kids[u"sub"][0], "dirnode")
-        self.failUnless("metadata" in kids[u"sub"][1])
-        self.failUnless("ctime" in kids[u"sub"][1]["metadata"])
-        self.failUnless("mtime" in kids[u"sub"][1]["metadata"])
+        self.failUnlessIn("metadata", kids[u"sub"][1])
+        self.failUnlessIn("tahoe", kids[u"sub"][1]["metadata"])
+        tahoe_md = kids[u"sub"][1]["metadata"]["tahoe"]
+        self.failUnlessIn("linkcrtime", tahoe_md)
+        self.failUnlessIn("linkmotime", tahoe_md)
         self.failUnlessEqual(kids[u"bar.txt"][0], "filenode")
-        self.failUnlessEqual(kids[u"bar.txt"][1]["size"], len(self.BAR_CONTENTS))
-        self.failUnlessEqual(kids[u"bar.txt"][1]["ro_uri"], self._bar_txt_uri)
-        self.failUnlessEqual(kids[u"bar.txt"][1]["verify_uri"],
-                             self._bar_txt_verifycap)
-        self.failUnlessEqual(kids[u"bar.txt"][1]["metadata"]["ctime"],
-                             self._bar_txt_metadata["ctime"])
-        self.failUnlessEqual(kids[u"n\u00fc.txt"][1]["ro_uri"],
-                             self._bar_txt_uri)
+        self.failUnlessReallyEqual(kids[u"bar.txt"][1]["size"], len(self.BAR_CONTENTS))
+        self.failUnlessReallyEqual(to_str(kids[u"bar.txt"][1]["ro_uri"]), self._bar_txt_uri)
+        self.failUnlessReallyEqual(to_str(kids[u"bar.txt"][1]["verify_uri"]),
+                                   self._bar_txt_verifycap)
+        self.failUnlessIn("metadata", kids[u"bar.txt"][1])
+        self.failUnlessIn("tahoe", kids[u"bar.txt"][1]["metadata"])
+        self.failUnlessReallyEqual(kids[u"bar.txt"][1]["metadata"]["tahoe"]["linkcrtime"],
+                                   self._bar_txt_metadata["tahoe"]["linkcrtime"])
+        self.failUnlessReallyEqual(to_str(kids[u"n\u00fc.txt"][1]["ro_uri"]),
+                                   self._bar_txt_uri)
+        self.failUnlessIn("quux.txt", kids)
+        self.failUnlessReallyEqual(to_str(kids[u"quux.txt"][1]["rw_uri"]),
+                                   self._quux_txt_uri)
+        self.failUnlessReallyEqual(to_str(kids[u"quux.txt"][1]["ro_uri"]),
+                                   self._quux_txt_readonly_uri)
 
     def GET(self, urlpath, followRedirect=False, return_response=False,
             **kwargs):
@@ -283,7 +408,6 @@ class WebMixin(object):
         return client.getPage(url, method="DELETE")
 
     def POST(self, urlpath, followRedirect=False, **fields):
-        url = self.webish_url + urlpath
         sepbase = "boogabooga"
         sep = "--" + sepbase
         form = []
@@ -308,9 +432,15 @@ class WebMixin(object):
             form.append(value)
             form.append(sep)
         form[-1] += "--"
-        body = "\r\n".join(form) + "\r\n"
-        headers = {"content-type": "multipart/form-data; boundary=%s" % sepbase,
-                   }
+        body = ""
+        headers = {}
+        if fields:
+            body = "\r\n".join(form) + "\r\n"
+            headers["content-type"] = "multipart/form-data; boundary=%s" % sepbase
+        return self.POST2(urlpath, body, headers, followRedirect)
+
+    def POST2(self, urlpath, body="", headers={}, followRedirect=False):
+        url = self.webish_url + urlpath
         return client.getPage(url, method="POST", postdata=body,
                               headers=headers, followRedirect=followRedirect)
 
@@ -357,21 +487,28 @@ class WebMixin(object):
     def should404(self, res, which):
         if isinstance(res, failure.Failure):
             res.trap(error.Error)
-            self.failUnlessEqual(res.value.status, "404")
+            self.failUnlessReallyEqual(res.value.status, "404")
         else:
             self.fail("%s was supposed to Error(404), not get '%s'" %
                       (which, res))
 
+    def should302(self, res, which):
+        if isinstance(res, failure.Failure):
+            res.trap(error.Error)
+            self.failUnlessReallyEqual(res.value.status, "302")
+        else:
+            self.fail("%s was supposed to Error(302), not get '%s'" %
+                        (which, res))
+
 
-class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
+class Web(WebMixin, WebErrorMixin, testutil.StallMixin, testutil.ReallyEqualMixin, unittest.TestCase):
     def test_create(self):
         pass
 
     def test_welcome(self):
         d = self.GET("/")
         def _check(res):
-            self.failUnless('Welcome To AllMyData' in res)
-            self.failUnless('Tahoe' in res)
+            self.failUnless('Welcome To Tahoe-LAFS' in res, res)
 
             self.s.basedir = 'web/test_welcome'
             fileutil.make_dirs("web/test_welcome")
@@ -383,7 +520,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
     def test_provisioning(self):
         d = self.GET("/provisioning/")
         def _check(res):
-            self.failUnless('Tahoe Provisioning Tool' in res)
+            self.failUnless('Provisioning Tool' in res)
             fields = {'filled': True,
                       "num_users": int(50e3),
                       "files_per_user": 1000,
@@ -401,7 +538,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
 
         d.addCallback(_check)
         def _check2(res):
-            self.failUnless('Tahoe Provisioning Tool' in res)
+            self.failUnless('Provisioning Tool' in res)
             self.failUnless("Share space consumed: 167.01TB" in res)
 
             fields = {'filled': True,
@@ -435,12 +572,13 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         try:
             from allmydata import reliability
             _hush_pyflakes = reliability
+            del _hush_pyflakes
         except:
             raise unittest.SkipTest("reliability tool requires NumPy")
 
         d = self.GET("/reliability/")
         def _check(res):
-            self.failUnless('Tahoe Reliability Tool' in res)
+            self.failUnless('Reliability Tool' in res)
             fields = {'drive_lifetime': "8Y",
                       "k": "3",
                       "R": "7",
@@ -454,18 +592,19 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
 
         d.addCallback(_check)
         def _check2(res):
-            self.failUnless('Tahoe Reliability Tool' in res)
+            self.failUnless('Reliability Tool' in res)
             r = r'Probability of loss \(no maintenance\):\s+<span>0.033591'
             self.failUnless(re.search(r, res), res)
         d.addCallback(_check2)
         return d
 
     def test_status(self):
-        dl_num = self.s.list_all_download_statuses()[0].get_counter()
-        ul_num = self.s.list_all_upload_statuses()[0].get_counter()
-        mu_num = self.s.list_all_mapupdate_statuses()[0].get_counter()
-        pub_num = self.s.list_all_publish_statuses()[0].get_counter()
-        ret_num = self.s.list_all_retrieve_statuses()[0].get_counter()
+        h = self.s.get_history()
+        dl_num = h.list_all_download_statuses()[0].get_counter()
+        ul_num = h.list_all_upload_statuses()[0].get_counter()
+        mu_num = h.list_all_mapupdate_statuses()[0].get_counter()
+        pub_num = h.list_all_publish_statuses()[0].get_counter()
+        ret_num = h.list_all_retrieve_statuses()[0].get_counter()
         d = self.GET("/status", followRedirect=True)
         def _check(res):
             self.failUnless('Upload and Download Status' in res, res)
@@ -479,7 +618,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         def _check_json(res):
             data = simplejson.loads(res)
             self.failUnless(isinstance(data, dict))
-            active = data["active"]
+            #active = data["active"]
             # TODO: test more. We need a way to fake an active operation
             # here.
         d.addCallback(_check_json)
@@ -488,6 +627,26 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         def _check_dl(res):
             self.failUnless("File Download Status" in res, res)
         d.addCallback(_check_dl)
+        d.addCallback(lambda res: self.GET("/status/down-%d/event_json" % dl_num))
+        def _check_dl_json(res):
+            data = simplejson.loads(res)
+            self.failUnless(isinstance(data, dict))
+            self.failUnless("read" in data)
+            self.failUnlessEqual(data["read"][0]["length"], 120)
+            self.failUnlessEqual(data["segment"][0]["segment_length"], 100)
+            self.failUnlessEqual(data["segment"][2]["segment_number"], 2)
+            self.failUnlessEqual(data["segment"][2]["finish_time"], None)
+            phwr_id = base32.b2a(hashutil.tagged_hash("foo", "serverid_a")[:20])
+            cmpu_id = base32.b2a(hashutil.tagged_hash("foo", "serverid_b")[:20])
+            # serverids[] keys are strings, since that's what JSON does, but
+            # we'd really like them to be ints
+            self.failUnlessEqual(data["serverids"]["0"], "phwr")
+            self.failUnless(data["serverids"].has_key("1"), data["serverids"])
+            self.failUnlessEqual(data["serverids"]["1"], "cmpu", data["serverids"])
+            self.failUnlessEqual(data["server_info"][phwr_id]["short"], "phwr")
+            self.failUnlessEqual(data["server_info"][cmpu_id]["short"], "cmpu")
+            self.failUnless("dyhb" in data)
+        d.addCallback(_check_dl_json)
         d.addCallback(lambda res: self.GET("/status/up-%d" % ul_num))
         def _check_ul(res):
             self.failUnless("File Upload Status" in res, res)
@@ -509,26 +668,26 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
 
     def test_status_numbers(self):
         drrm = status.DownloadResultsRendererMixin()
-        self.failUnlessEqual(drrm.render_time(None, None), "")
-        self.failUnlessEqual(drrm.render_time(None, 2.5), "2.50s")
-        self.failUnlessEqual(drrm.render_time(None, 0.25), "250ms")
-        self.failUnlessEqual(drrm.render_time(None, 0.0021), "2.1ms")
-        self.failUnlessEqual(drrm.render_time(None, 0.000123), "123us")
-        self.failUnlessEqual(drrm.render_rate(None, None), "")
-        self.failUnlessEqual(drrm.render_rate(None, 2500000), "2.50MBps")
-        self.failUnlessEqual(drrm.render_rate(None, 30100), "30.1kBps")
-        self.failUnlessEqual(drrm.render_rate(None, 123), "123Bps")
+        self.failUnlessReallyEqual(drrm.render_time(None, None), "")
+        self.failUnlessReallyEqual(drrm.render_time(None, 2.5), "2.50s")
+        self.failUnlessReallyEqual(drrm.render_time(None, 0.25), "250ms")
+        self.failUnlessReallyEqual(drrm.render_time(None, 0.0021), "2.1ms")
+        self.failUnlessReallyEqual(drrm.render_time(None, 0.000123), "123us")
+        self.failUnlessReallyEqual(drrm.render_rate(None, None), "")
+        self.failUnlessReallyEqual(drrm.render_rate(None, 2500000), "2.50MBps")
+        self.failUnlessReallyEqual(drrm.render_rate(None, 30100), "30.1kBps")
+        self.failUnlessReallyEqual(drrm.render_rate(None, 123), "123Bps")
 
         urrm = status.UploadResultsRendererMixin()
-        self.failUnlessEqual(urrm.render_time(None, None), "")
-        self.failUnlessEqual(urrm.render_time(None, 2.5), "2.50s")
-        self.failUnlessEqual(urrm.render_time(None, 0.25), "250ms")
-        self.failUnlessEqual(urrm.render_time(None, 0.0021), "2.1ms")
-        self.failUnlessEqual(urrm.render_time(None, 0.000123), "123us")
-        self.failUnlessEqual(urrm.render_rate(None, None), "")
-        self.failUnlessEqual(urrm.render_rate(None, 2500000), "2.50MBps")
-        self.failUnlessEqual(urrm.render_rate(None, 30100), "30.1kBps")
-        self.failUnlessEqual(urrm.render_rate(None, 123), "123Bps")
+        self.failUnlessReallyEqual(urrm.render_time(None, None), "")
+        self.failUnlessReallyEqual(urrm.render_time(None, 2.5), "2.50s")
+        self.failUnlessReallyEqual(urrm.render_time(None, 0.25), "250ms")
+        self.failUnlessReallyEqual(urrm.render_time(None, 0.0021), "2.1ms")
+        self.failUnlessReallyEqual(urrm.render_time(None, 0.000123), "123us")
+        self.failUnlessReallyEqual(urrm.render_rate(None, None), "")
+        self.failUnlessReallyEqual(urrm.render_rate(None, 2500000), "2.50MBps")
+        self.failUnlessReallyEqual(urrm.render_rate(None, 30100), "30.1kBps")
+        self.failUnlessReallyEqual(urrm.render_rate(None, 123), "123Bps")
 
     def test_GET_FILEURL(self):
         d = self.GET(self.public_url + "/foo/bar.txt")
@@ -540,11 +699,11 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d = self.GET(self.public_url + "/foo/bar.txt", headers=headers,
                      return_response=True)
         def _got((res, status, headers)):
-            self.failUnlessEqual(int(status), 206)
+            self.failUnlessReallyEqual(int(status), 206)
             self.failUnless(headers.has_key("content-range"))
-            self.failUnlessEqual(headers["content-range"][0],
-                                 "bytes 1-10/%d" % len(self.BAR_CONTENTS))
-            self.failUnlessEqual(res, self.BAR_CONTENTS[1:11])
+            self.failUnlessReallyEqual(headers["content-range"][0],
+                                       "bytes 1-10/%d" % len(self.BAR_CONTENTS))
+            self.failUnlessReallyEqual(res, self.BAR_CONTENTS[1:11])
         d.addCallback(_got)
         return d
 
@@ -554,24 +713,47 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d = self.GET(self.public_url + "/foo/bar.txt", headers=headers,
                      return_response=True)
         def _got((res, status, headers)):
-            self.failUnlessEqual(int(status), 206)
+            self.failUnlessReallyEqual(int(status), 206)
             self.failUnless(headers.has_key("content-range"))
-            self.failUnlessEqual(headers["content-range"][0],
-                                 "bytes 5-%d/%d" % (length-1, length))
-            self.failUnlessEqual(res, self.BAR_CONTENTS[5:])
+            self.failUnlessReallyEqual(headers["content-range"][0],
+                                       "bytes 5-%d/%d" % (length-1, length))
+            self.failUnlessReallyEqual(res, self.BAR_CONTENTS[5:])
         d.addCallback(_got)
         return d
 
+    def test_GET_FILEURL_partial_end_range(self):
+        headers = {"range": "bytes=-5"}
+        length  = len(self.BAR_CONTENTS)
+        d = self.GET(self.public_url + "/foo/bar.txt", headers=headers,
+                     return_response=True)
+        def _got((res, status, headers)):
+            self.failUnlessReallyEqual(int(status), 206)
+            self.failUnless(headers.has_key("content-range"))
+            self.failUnlessReallyEqual(headers["content-range"][0],
+                                       "bytes %d-%d/%d" % (length-5, length-1, length))
+            self.failUnlessReallyEqual(res, self.BAR_CONTENTS[-5:])
+        d.addCallback(_got)
+        return d
+
+    def test_GET_FILEURL_partial_range_overrun(self):
+        headers = {"range": "bytes=100-200"}
+        d = self.shouldFail2(error.Error, "test_GET_FILEURL_range_overrun",
+                             "416 Requested Range not satisfiable",
+                             "First beyond end of file",
+                             self.GET, self.public_url + "/foo/bar.txt",
+                             headers=headers)
+        return d
+
     def test_HEAD_FILEURL_range(self):
         headers = {"range": "bytes=1-10"}
         d = self.HEAD(self.public_url + "/foo/bar.txt", headers=headers,
                      return_response=True)
         def _got((res, status, headers)):
-            self.failUnlessEqual(res, "")
-            self.failUnlessEqual(int(status), 206)
+            self.failUnlessReallyEqual(res, "")
+            self.failUnlessReallyEqual(int(status), 206)
             self.failUnless(headers.has_key("content-range"))
-            self.failUnlessEqual(headers["content-range"][0],
-                                 "bytes 1-10/%d" % len(self.BAR_CONTENTS))
+            self.failUnlessReallyEqual(headers["content-range"][0],
+                                       "bytes 1-10/%d" % len(self.BAR_CONTENTS))
         d.addCallback(_got)
         return d
 
@@ -581,29 +763,53 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d = self.HEAD(self.public_url + "/foo/bar.txt", headers=headers,
                      return_response=True)
         def _got((res, status, headers)):
-            self.failUnlessEqual(int(status), 206)
+            self.failUnlessReallyEqual(int(status), 206)
             self.failUnless(headers.has_key("content-range"))
-            self.failUnlessEqual(headers["content-range"][0],
-                                 "bytes 5-%d/%d" % (length-1, length))
+            self.failUnlessReallyEqual(headers["content-range"][0],
+                                       "bytes 5-%d/%d" % (length-1, length))
         d.addCallback(_got)
         return d
 
+    def test_HEAD_FILEURL_partial_end_range(self):
+        headers = {"range": "bytes=-5"}
+        length  = len(self.BAR_CONTENTS)
+        d = self.HEAD(self.public_url + "/foo/bar.txt", headers=headers,
+                     return_response=True)
+        def _got((res, status, headers)):
+            self.failUnlessReallyEqual(int(status), 206)
+            self.failUnless(headers.has_key("content-range"))
+            self.failUnlessReallyEqual(headers["content-range"][0],
+                                       "bytes %d-%d/%d" % (length-5, length-1, length))
+        d.addCallback(_got)
+        return d
+
+    def test_HEAD_FILEURL_partial_range_overrun(self):
+        headers = {"range": "bytes=100-200"}
+        d = self.shouldFail2(error.Error, "test_HEAD_FILEURL_range_overrun",
+                             "416 Requested Range not satisfiable",
+                             "",
+                             self.HEAD, self.public_url + "/foo/bar.txt",
+                             headers=headers)
+        return d
+
     def test_GET_FILEURL_range_bad(self):
         headers = {"range": "BOGUS=fizbop-quarnak"}
-        d = self.shouldFail2(error.Error, "test_GET_FILEURL_range_bad",
-                             "400 Bad Request",
-                             "Syntactically invalid http range header",
-                             self.GET, self.public_url + "/foo/bar.txt",
-                             headers=headers)
+        d = self.GET(self.public_url + "/foo/bar.txt", headers=headers,
+                     return_response=True)
+        def _got((res, status, headers)):
+            self.failUnlessReallyEqual(int(status), 200)
+            self.failUnless(not headers.has_key("content-range"))
+            self.failUnlessReallyEqual(res, self.BAR_CONTENTS)
+        d.addCallback(_got)
         return d
 
     def test_HEAD_FILEURL(self):
         d = self.HEAD(self.public_url + "/foo/bar.txt", return_response=True)
         def _got((res, status, headers)):
-            self.failUnlessEqual(res, "")
-            self.failUnlessEqual(headers["content-length"][0],
-                                 str(len(self.BAR_CONTENTS)))
-            self.failUnlessEqual(headers["content-type"], ["text/plain"])
+            self.failUnlessReallyEqual(res, "")
+            self.failUnlessReallyEqual(headers["content-length"][0],
+                                       str(len(self.BAR_CONTENTS)))
+            self.failUnlessReallyEqual(headers["content-type"], ["text/plain"])
         d.addCallback(_got)
         return d
 
@@ -636,6 +842,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
                              self.PUT, base + "/@@name=/blah.txt", "")
         return d
 
+
     def test_GET_DIRURL_named_bad(self):
         base = "/file/%s" % urllib.quote(self._foo_uri)
         d = self.shouldFail2(error.Error, "test_PUT_DIRURL_named_bad",
@@ -657,8 +864,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         base = "/file/%s" % urllib.quote(verifier_cap)
         # client.create_node_from_uri() can't handle verify-caps
         d = self.shouldFail2(error.Error, "GET_unhandled_URI_named",
-                             "400 Bad Request",
-                             "is not a valid file- or directory- cap",
+                             "400 Bad Request", "is not a file-cap",
                              self.GET, base)
         return d
 
@@ -669,7 +875,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         # client.create_node_from_uri() can't handle verify-caps
         d = self.shouldFail2(error.Error, "test_GET_unhandled_URI",
                              "400 Bad Request",
-                             "is not a valid file- or directory- cap",
+                             "GET unknown URI type: can only do t=info",
                              self.GET, base)
         return d
 
@@ -679,6 +885,39 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d.addCallback(self.failUnlessIsBarDotTxt)
         return d
 
+    def test_GET_FILE_URI_mdmf(self):
+        base = "/uri/%s" % urllib.quote(self._quux_txt_uri)
+        d = self.GET(base)
+        d.addCallback(self.failUnlessIsQuuxDotTxt)
+        return d
+
+    def test_GET_FILE_URI_mdmf_extensions(self):
+        base = "/uri/%s" % urllib.quote("%s:3:131073" % self._quux_txt_uri)
+        d = self.GET(base)
+        d.addCallback(self.failUnlessIsQuuxDotTxt)
+        return d
+
+    def test_GET_FILE_URI_mdmf_bare_cap(self):
+        cap_elements = self._quux_txt_uri.split(":")
+        # 6 == expected cap length with two extensions.
+        self.failUnlessEqual(len(cap_elements), 6)
+
+        # Now lop off the extension parameters and stitch everything
+        # back together
+        quux_uri = ":".join(cap_elements[:len(cap_elements) - 2])
+
+        # Now GET that. We should get back quux.
+        base = "/uri/%s" % urllib.quote(quux_uri)
+        d = self.GET(base)
+        d.addCallback(self.failUnlessIsQuuxDotTxt)
+        return d
+
+    def test_GET_FILE_URI_mdmf_readonly(self):
+        base = "/uri/%s" % urllib.quote(self._quux_txt_readonly_uri)
+        d = self.GET(base)
+        d.addCallback(self.failUnlessIsQuuxDotTxt)
+        return d
+
     def test_GET_FILE_URI_badchild(self):
         base = "/uri/%s/boguschild" % urllib.quote(self._bar_txt_uri)
         errmsg = "Files have no children, certainly not named 'boguschild'"
@@ -695,11 +934,81 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
                              self.PUT, base, "")
         return d
 
+    def test_PUT_FILE_URI_mdmf(self):
+        base = "/uri/%s" % urllib.quote(self._quux_txt_uri)
+        self._quux_new_contents = "new_contents"
+        d = self.GET(base)
+        d.addCallback(lambda res:
+            self.failUnlessIsQuuxDotTxt(res))
+        d.addCallback(lambda ignored:
+            self.PUT(base, self._quux_new_contents))
+        d.addCallback(lambda ignored:
+            self.GET(base))
+        d.addCallback(lambda res:
+            self.failUnlessReallyEqual(res, self._quux_new_contents))
+        return d
+
+    def test_PUT_FILE_URI_mdmf_extensions(self):
+        base = "/uri/%s" % urllib.quote("%s:3:131073" % self._quux_txt_uri)
+        self._quux_new_contents = "new_contents"
+        d = self.GET(base)
+        d.addCallback(lambda res: self.failUnlessIsQuuxDotTxt(res))
+        d.addCallback(lambda ignored: self.PUT(base, self._quux_new_contents))
+        d.addCallback(lambda ignored: self.GET(base))
+        d.addCallback(lambda res: self.failUnlessEqual(self._quux_new_contents,
+                                                       res))
+        return d
+
+    def test_PUT_FILE_URI_mdmf_bare_cap(self):
+        elements = self._quux_txt_uri.split(":")
+        self.failUnlessEqual(len(elements), 6)
+
+        quux_uri = ":".join(elements[:len(elements) - 2])
+        base = "/uri/%s" % urllib.quote(quux_uri)
+        self._quux_new_contents = "new_contents" * 50000
+
+        d = self.GET(base)
+        d.addCallback(self.failUnlessIsQuuxDotTxt)
+        d.addCallback(lambda ignored: self.PUT(base, self._quux_new_contents))
+        d.addCallback(lambda ignored: self.GET(base))
+        d.addCallback(lambda res:
+            self.failUnlessEqual(res, self._quux_new_contents))
+        return d
+
+    def test_PUT_FILE_URI_mdmf_readonly(self):
+        # We're not allowed to PUT things to a readonly cap.
+        base = "/uri/%s" % self._quux_txt_readonly_uri
+        d = self.GET(base)
+        d.addCallback(lambda res:
+            self.failUnlessIsQuuxDotTxt(res))
+        # What should we get here? We get a 500 error now; that's not right.
+        d.addCallback(lambda ignored:
+            self.shouldFail2(error.Error, "test_PUT_FILE_URI_mdmf_readonly",
+                             "400 Bad Request", "read-only cap",
+                             self.PUT, base, "new data"))
+        return d
+
+    def test_PUT_FILE_URI_sdmf_readonly(self):
+        # We're not allowed to put things to a readonly cap.
+        base = "/uri/%s" % self._baz_txt_readonly_uri
+        d = self.GET(base)
+        d.addCallback(lambda res:
+            self.failUnlessIsBazDotTxt(res))
+        d.addCallback(lambda ignored:
+            self.shouldFail2(error.Error, "test_PUT_FILE_URI_sdmf_readonly",
+                             "400 Bad Request", "read-only cap",
+                             self.PUT, base, "new_data"))
+        return d
+
+    # TODO: version of this with a Unicode filename
     def test_GET_FILEURL_save(self):
-        d = self.GET(self.public_url + "/foo/bar.txt?filename=bar.txt&save=true")
-        # TODO: look at the headers, expect a Content-Disposition: attachment
-        # header.
-        d.addCallback(self.failUnlessIsBarDotTxt)
+        d = self.GET(self.public_url + "/foo/bar.txt?filename=bar.txt&save=true",
+                     return_response=True)
+        def _got((res, statuscode, headers)):
+            content_disposition = headers["content-disposition"][0]
+            self.failUnless(content_disposition == 'attachment; filename="bar.txt"', content_disposition)
+            self.failUnlessIsBarDotTxt(res)
+        d.addCallback(_got)
         return d
 
     def test_GET_FILEURL_missing(self):
@@ -707,16 +1016,124 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d.addBoth(self.should404, "test_GET_FILEURL_missing")
         return d
 
+    def test_GET_FILEURL_info_mdmf(self):
+        d = self.GET("/uri/%s?t=info" % self._quux_txt_uri)
+        def _got(res):
+            self.failUnlessIn("mutable file (mdmf)", res)
+            self.failUnlessIn(self._quux_txt_uri, res)
+            self.failUnlessIn(self._quux_txt_readonly_uri, res)
+        d.addCallback(_got)
+        return d
+
+    def test_GET_FILEURL_info_mdmf_readonly(self):
+        d = self.GET("/uri/%s?t=info" % self._quux_txt_readonly_uri)
+        def _got(res):
+            self.failUnlessIn("mutable file (mdmf)", res)
+            self.failIfIn(self._quux_txt_uri, res)
+            self.failUnlessIn(self._quux_txt_readonly_uri, res)
+        d.addCallback(_got)
+        return d
+
+    def test_GET_FILEURL_info_sdmf(self):
+        d = self.GET("/uri/%s?t=info" % self._baz_txt_uri)
+        def _got(res):
+            self.failUnlessIn("mutable file (sdmf)", res)
+            self.failUnlessIn(self._baz_txt_uri, res)
+        d.addCallback(_got)
+        return d
+
+    def test_GET_FILEURL_info_mdmf_extensions(self):
+        d = self.GET("/uri/%s:3:131073?t=info" % self._quux_txt_uri)
+        def _got(res):
+            self.failUnlessIn("mutable file (mdmf)", res)
+            self.failUnlessIn(self._quux_txt_uri, res)
+            self.failUnlessIn(self._quux_txt_readonly_uri, res)
+        d.addCallback(_got)
+        return d
+
+    def test_GET_FILEURL_info_mdmf_bare_cap(self):
+        elements = self._quux_txt_uri.split(":")
+        self.failUnlessEqual(len(elements), 6)
+
+        quux_uri = ":".join(elements[:len(elements) - 2])
+        base = "/uri/%s?t=info" % urllib.quote(quux_uri)
+        d = self.GET(base)
+        def _got(res):
+            self.failUnlessIn("mutable file (mdmf)", res)
+            self.failUnlessIn(quux_uri, res)
+        d.addCallback(_got)
+        return d
+
+    def test_PUT_overwrite_only_files(self):
+        # create a directory, put a file in that directory.
+        contents, n, filecap = self.makefile(8)
+        d = self.PUT(self.public_url + "/foo/dir?t=mkdir", "")
+        d.addCallback(lambda res:
+            self.PUT(self.public_url + "/foo/dir/file1.txt",
+                     self.NEWFILE_CONTENTS))
+        # try to overwrite the file with replace=only-files
+        # (this should work)
+        d.addCallback(lambda res:
+            self.PUT(self.public_url + "/foo/dir/file1.txt?t=uri&replace=only-files",
+                     filecap))
+        d.addCallback(lambda res:
+            self.shouldFail2(error.Error, "PUT_bad_t", "409 Conflict",
+                 "There was already a child by that name, and you asked me "
+                 "to not replace it",
+                 self.PUT, self.public_url + "/foo/dir?t=uri&replace=only-files",
+                 filecap))
+        return d
+
     def test_PUT_NEWFILEURL(self):
         d = self.PUT(self.public_url + "/foo/new.txt", self.NEWFILE_CONTENTS)
         # TODO: we lose the response code, so we can't check this
-        #self.failUnlessEqual(responsecode, 201)
-        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"new.txt")
+        #self.failUnlessReallyEqual(responsecode, 201)
+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"new.txt")
         d.addCallback(lambda res:
                       self.failUnlessChildContentsAre(self._foo_node, u"new.txt",
                                                       self.NEWFILE_CONTENTS))
         return d
 
+    def test_PUT_NEWFILEURL_not_mutable(self):
+        d = self.PUT(self.public_url + "/foo/new.txt?mutable=false",
+                     self.NEWFILE_CONTENTS)
+        # TODO: we lose the response code, so we can't check this
+        #self.failUnlessReallyEqual(responsecode, 201)
+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"new.txt")
+        d.addCallback(lambda res:
+                      self.failUnlessChildContentsAre(self._foo_node, u"new.txt",
+                                                      self.NEWFILE_CONTENTS))
+        return d
+
+    def test_PUT_NEWFILEURL_unlinked_mdmf(self):
+        # this should get us a few segments of an MDMF mutable file,
+        # which we can then test for.
+        contents = self.NEWFILE_CONTENTS * 300000
+        d = self.PUT("/uri?mutable=true&mutable-type=mdmf",
+                     contents)
+        def _got_filecap(filecap):
+            self.failUnless(filecap.startswith("URI:MDMF"))
+            return filecap
+        d.addCallback(_got_filecap)
+        d.addCallback(lambda filecap: self.GET("/uri/%s?t=json" % filecap))
+        d.addCallback(lambda json: self.failUnlessIn("mdmf", json))
+        return d
+
+    def test_PUT_NEWFILEURL_unlinked_sdmf(self):
+        contents = self.NEWFILE_CONTENTS * 300000
+        d = self.PUT("/uri?mutable=true&mutable-type=sdmf",
+                     contents)
+        d.addCallback(lambda filecap: self.GET("/uri/%s?t=json" % filecap))
+        d.addCallback(lambda json: self.failUnlessIn("sdmf", json))
+        return d
+
+    def test_PUT_NEWFILEURL_unlinked_bad_mutable_type(self):
+        contents = self.NEWFILE_CONTENTS * 300000
+        return self.shouldHTTPError("test bad mutable type",
+                                    400, "Bad Request", "Unknown type: foo",
+                                    self.PUT, "/uri?mutable=true&mutable-type=foo",
+                                    contents)
+
     def test_PUT_NEWFILEURL_range_bad(self):
         headers = {"content-range": "bytes 1-10/%d" % len(self.NEWFILE_CONTENTS)}
         target = self.public_url + "/foo/new.txt"
@@ -734,14 +1151,14 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d = self.PUT(self.public_url + "/foo/new.txt?mutable=true",
                      self.NEWFILE_CONTENTS)
         # TODO: we lose the response code, so we can't check this
-        #self.failUnlessEqual(responsecode, 201)
+        #self.failUnlessReallyEqual(responsecode, 201)
         def _check_uri(res):
             u = uri.from_string_mutable_filenode(res)
             self.failUnless(u.is_mutable())
             self.failIf(u.is_readonly())
             return res
         d.addCallback(_check_uri)
-        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"new.txt")
+        d.addCallback(self.failUnlessURIMatchesRWChild, self._foo_node, u"new.txt")
         d.addCallback(lambda res:
                       self.failUnlessMutableChildContentsAre(self._foo_node,
                                                              u"new.txt",
@@ -749,19 +1166,17 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         return d
 
     def test_PUT_NEWFILEURL_mutable_toobig(self):
-        d = self.shouldFail2(error.Error, "test_PUT_NEWFILEURL_mutable_toobig",
-                             "413 Request Entity Too Large",
-                             "SDMF is limited to one segment, and 10001 > 10000",
-                             self.PUT,
-                             self.public_url + "/foo/new.txt?mutable=true",
-                             "b" * (self.s.MUTABLE_SIZELIMIT+1))
+        # It is okay to upload large mutable files, so we should be able
+        # to do that.
+        d = self.PUT(self.public_url + "/foo/new.txt?mutable=true",
+                     "b" * (self.s.MUTABLE_SIZELIMIT + 1))
         return d
 
     def test_PUT_NEWFILEURL_replace(self):
         d = self.PUT(self.public_url + "/foo/bar.txt", self.NEWFILE_CONTENTS)
         # TODO: we lose the response code, so we can't check this
-        #self.failUnlessEqual(responsecode, 200)
-        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"bar.txt")
+        #self.failUnlessReallyEqual(responsecode, 200)
+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"bar.txt")
         d.addCallback(lambda res:
                       self.failUnlessChildContentsAre(self._foo_node, u"bar.txt",
                                                       self.NEWFILE_CONTENTS))
@@ -786,7 +1201,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
     def test_PUT_NEWFILEURL_mkdirs(self):
         d = self.PUT(self.public_url + "/foo/newdir/new.txt", self.NEWFILE_CONTENTS)
         fn = self._foo_node
-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"newdir/new.txt")
+        d.addCallback(self.failUnlessURIMatchesROChild, fn, u"newdir/new.txt")
         d.addCallback(lambda res: self.failIfNodeHasChild(fn, u"new.txt"))
         d.addCallback(lambda res: self.failUnlessNodeHasChild(fn, u"newdir"))
         d.addCallback(lambda res:
@@ -802,6 +1217,14 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
                   "Unable to create directory 'blockingfile': a file was in the way")
         return d
 
+    def test_PUT_NEWFILEURL_emptyname(self):
+        # an empty pathname component (i.e. a double-slash) is disallowed
+        d = self.shouldFail2(error.Error, "test_PUT_NEWFILEURL_emptyname",
+                             "400 Bad Request",
+                             "The webapi does not allow empty pathname components",
+                             self.PUT, self.public_url + "/foo//new.txt", "")
+        return d
+
     def test_DELETE_FILEURL(self):
         d = self.DELETE(self.public_url + "/foo/bar.txt")
         d.addCallback(lambda res:
@@ -818,13 +1241,101 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d.addBoth(self.should404, "test_DELETE_FILEURL_missing2")
         return d
 
+    def failUnlessHasBarDotTxtMetadata(self, res):
+        data = simplejson.loads(res)
+        self.failUnless(isinstance(data, list))
+        self.failUnlessIn("metadata", data[1])
+        self.failUnlessIn("tahoe", data[1]["metadata"])
+        self.failUnlessIn("linkcrtime", data[1]["metadata"]["tahoe"])
+        self.failUnlessIn("linkmotime", data[1]["metadata"]["tahoe"])
+        self.failUnlessReallyEqual(data[1]["metadata"]["tahoe"]["linkcrtime"],
+                                   self._bar_txt_metadata["tahoe"]["linkcrtime"])
+
     def test_GET_FILEURL_json(self):
         # twisted.web.http.parse_qs ignores any query args without an '=', so
         # I can't do "GET /path?json", I have to do "GET /path/t=json"
         # instead. This may make it tricky to emulate the S3 interface
         # completely.
         d = self.GET(self.public_url + "/foo/bar.txt?t=json")
-        d.addCallback(self.failUnlessIsBarJSON)
+        def _check1(data):
+            self.failUnlessIsBarJSON(data)
+            self.failUnlessHasBarDotTxtMetadata(data)
+            return
+        d.addCallback(_check1)
+        return d
+
+    def test_GET_FILEURL_json_mutable_type(self):
+        # The JSON should include mutable-type, which says whether the
+        # file is SDMF or MDMF
+        d = self.PUT("/uri?mutable=true&mutable-type=mdmf",
+                     self.NEWFILE_CONTENTS * 300000)
+        d.addCallback(lambda filecap: self.GET("/uri/%s?t=json" % filecap))
+        def _got_json(json, version):
+            data = simplejson.loads(json)
+            assert "filenode" == data[0]
+            data = data[1]
+            assert isinstance(data, dict)
+
+            self.failUnlessIn("mutable-type", data)
+            self.failUnlessEqual(data['mutable-type'], version)
+
+        d.addCallback(_got_json, "mdmf")
+        # Now make an SDMF file and check that it is reported correctly.
+        d.addCallback(lambda ignored:
+            self.PUT("/uri?mutable=true&mutable-type=sdmf",
+                      self.NEWFILE_CONTENTS * 300000))
+        d.addCallback(lambda filecap: self.GET("/uri/%s?t=json" % filecap))
+        d.addCallback(_got_json, "sdmf")
+        return d
+
+    def test_GET_FILEURL_json_mdmf_extensions(self):
+        # A GET invoked against a URL that includes an MDMF cap with
+        # extensions should fetch the same JSON information as a GET
+        # invoked against a bare cap.
+        self._quux_txt_uri = "%s:3:131073" % self._quux_txt_uri
+        self._quux_txt_readonly_uri = "%s:3:131073" % self._quux_txt_readonly_uri
+        d = self.GET("/uri/%s?t=json" % urllib.quote(self._quux_txt_uri))
+        d.addCallback(self.failUnlessIsQuuxJSON)
+        return d
+
+    def test_GET_FILEURL_json_mdmf_bare_cap(self):
+        elements = self._quux_txt_uri.split(":")
+        self.failUnlessEqual(len(elements), 6)
+
+        quux_uri = ":".join(elements[:len(elements) - 2])
+        # so failUnlessIsQuuxJSON will work.
+        self._quux_txt_uri = quux_uri
+
+        # we need to alter the readonly URI in the same way, again so
+        # failUnlessIsQuuxJSON will work
+        elements = self._quux_txt_readonly_uri.split(":")
+        self.failUnlessEqual(len(elements), 6)
+        quux_ro_uri = ":".join(elements[:len(elements) - 2])
+        self._quux_txt_readonly_uri = quux_ro_uri
+
+        base = "/uri/%s?t=json" % urllib.quote(quux_uri)
+        d = self.GET(base)
+        d.addCallback(self.failUnlessIsQuuxJSON)
+        return d
+
+    def test_GET_FILEURL_json_mdmf_bare_readonly_cap(self):
+        elements = self._quux_txt_readonly_uri.split(":")
+        self.failUnlessEqual(len(elements), 6)
+
+        quux_readonly_uri = ":".join(elements[:len(elements) - 2])
+        # so failUnlessIsQuuxJSON will work
+        self._quux_txt_readonly_uri = quux_readonly_uri
+        base = "/uri/%s?t=json" % quux_readonly_uri
+        d = self.GET(base)
+        # XXX: We may need to make a method that knows how to check for
+        # readonly JSON, or else alter that one so that it knows how to
+        # do that.
+        d.addCallback(self.failUnlessIsQuuxJSON, readonly=True)
+        return d
+
+    def test_GET_FILEURL_json_mdmf(self):
+        d = self.GET("/uri/%s?t=json" % urllib.quote(self._quux_txt_uri))
+        d.addCallback(self.failUnlessIsQuuxJSON)
         return d
 
     def test_GET_FILEURL_json_missing(self):
@@ -835,13 +1346,13 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
     def test_GET_FILEURL_uri(self):
         d = self.GET(self.public_url + "/foo/bar.txt?t=uri")
         def _check(res):
-            self.failUnlessEqual(res, self._bar_txt_uri)
+            self.failUnlessReallyEqual(res, self._bar_txt_uri)
         d.addCallback(_check)
         d.addCallback(lambda res:
                       self.GET(self.public_url + "/foo/bar.txt?t=readonly-uri"))
         def _check2(res):
             # for now, for files, uris and readonly-uris are the same
-            self.failUnlessEqual(res, self._bar_txt_uri)
+            self.failUnlessReallyEqual(res, self._bar_txt_uri)
         d.addCallback(_check2)
         return d
 
@@ -852,11 +1363,82 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
                                  self.public_url + "/foo/bar.txt?t=bogus")
         return d
 
+    def test_CSS_FILE(self):
+        d = self.GET("/tahoe_css", followRedirect=True)
+        def _check(res):
+            CSS_STYLE=re.compile('toolbar\s{.+text-align:\scenter.+toolbar-item.+display:\sinline',re.DOTALL)
+            self.failUnless(CSS_STYLE.search(res), res)
+        d.addCallback(_check)
+        return d
+
     def test_GET_FILEURL_uri_missing(self):
         d = self.GET(self.public_url + "/foo/missing?t=uri")
         d.addBoth(self.should404, "test_GET_FILEURL_uri_missing")
         return d
 
+    def test_GET_DIRECTORY_html(self):
+        d = self.GET(self.public_url + "/foo", followRedirect=True)
+        def _check(res):
+            self.failUnlessIn('<div class="toolbar-item"><a href="../../..">Return to Welcome page</a></div>',res)
+            # These are radio buttons that allow a user to toggle
+            # whether a particular mutable file is SDMF or MDMF.
+            self.failUnlessIn("mutable-type-mdmf", res)
+            self.failUnlessIn("mutable-type-sdmf", res)
+            # Similarly, these toggle whether a particular directory
+            # should be MDMF or SDMF.
+            self.failUnlessIn("mutable-directory-mdmf", res)
+            self.failUnlessIn("mutable-directory-sdmf", res)
+            self.failUnlessIn("quux", res)
+        d.addCallback(_check)
+        return d
+
+    def test_GET_root_html(self):
+        # make sure that we have the option to upload an unlinked
+        # mutable file in SDMF and MDMF formats.
+        d = self.GET("/")
+        def _got_html(html):
+            # These are radio buttons that allow the user to toggle
+            # whether a particular mutable file is MDMF or SDMF.
+            self.failUnlessIn("mutable-type-mdmf", html)
+            self.failUnlessIn("mutable-type-sdmf", html)
+            # We should also have the ability to create a mutable directory.
+            self.failUnlessIn("mkdir", html)
+            # ...and we should have the ability to say whether that's an
+            # MDMF or SDMF directory
+            self.failUnlessIn("mutable-directory-mdmf", html)
+            self.failUnlessIn("mutable-directory-sdmf", html)
+        d.addCallback(_got_html)
+        return d
+
+    def test_mutable_type_defaults(self):
+        # The checked="checked" attribute of the inputs corresponding to
+        # the mutable-type parameter should change as expected with the
+        # value configured in tahoe.cfg.
+        #
+        # By default, the value configured with the client is
+        # SDMF_VERSION, so that should be checked.
+        assert self.s.mutable_file_default == SDMF_VERSION
+
+        d = self.GET("/")
+        def _got_html(html, value):
+            i = 'input checked="checked" type="radio" id="mutable-type-%s"'
+            self.failUnlessIn(i % value, html)
+        d.addCallback(_got_html, "sdmf")
+        d.addCallback(lambda ignored:
+            self.GET(self.public_url + "/foo", followRedirect=True))
+        d.addCallback(_got_html, "sdmf")
+        # Now switch the configuration value to MDMF. The MDMF radio
+        # buttons should now be checked on these pages.
+        def _swap_values(ignored):
+            self.s.mutable_file_default = MDMF_VERSION
+        d.addCallback(_swap_values)
+        d.addCallback(lambda ignored: self.GET("/"))
+        d.addCallback(_got_html, "mdmf")
+        d.addCallback(lambda ignored:
+            self.GET(self.public_url + "/foo", followRedirect=True))
+        d.addCallback(_got_html, "mdmf")
+        return d
+
     def test_GET_DIRURL(self):
         # the addSlash means we get a redirect here
         # from /uri/$URI/foo/ , we need ../../../ to get back to the root
@@ -868,41 +1450,45 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
             # the FILE reference points to a URI, but it should end in bar.txt
             bar_url = ("%s/file/%s/@@named=/bar.txt" %
                        (ROOT, urllib.quote(self._bar_txt_uri)))
-            get_bar = "".join([r'<td>',
+            get_bar = "".join([r'<td>FILE</td>',
+                               r'\s+<td>',
                                r'<a href="%s">bar.txt</a>' % bar_url,
                                r'</td>',
-                               r'\s+<td>FILE</td>',
-                               r'\s+<td>%d</td>' % len(self.BAR_CONTENTS),
+                               r'\s+<td align="right">%d</td>' % len(self.BAR_CONTENTS),
                                ])
             self.failUnless(re.search(get_bar, res), res)
-            for line in res.split("\n"):
-                # find the line that contains the delete button for bar.txt
-                if ("form action" in line and
-                    'value="delete"' in line and
-                    'value="bar.txt"' in line):
-                    # the form target should use a relative URL
-                    foo_url = urllib.quote("%s/uri/%s/" % (ROOT, self._foo_uri))
-                    self.failUnless(('action="%s"' % foo_url) in line, line)
-                    # and the when_done= should too
-                    #done_url = urllib.quote(???)
-                    #self.failUnless(('name="when_done" value="%s"' % done_url)
-                    #                in line, line)
-                    break
-            else:
-                self.fail("unable to find delete-bar.txt line", res)
+            for label in ['unlink', 'rename']:
+                for line in res.split("\n"):
+                    # find the line that contains the relevant button for bar.txt
+                    if ("form action" in line and
+                        ('value="%s"' % (label,)) in line and
+                        'value="bar.txt"' in line):
+                        # the form target should use a relative URL
+                        foo_url = urllib.quote("%s/uri/%s/" % (ROOT, self._foo_uri))
+                        self.failUnlessIn('action="%s"' % foo_url, line)
+                        # and the when_done= should too
+                        #done_url = urllib.quote(???)
+                        #self.failUnlessIn('name="when_done" value="%s"' % done_url, line)
+
+                        # 'unlink' needs to use POST because it directly has a side effect
+                        if label == 'unlink':
+                            self.failUnlessIn('method="post"', line)
+                        break
+                else:
+                    self.fail("unable to find '%s bar.txt' line" % (label,), res)
 
             # the DIR reference just points to a URI
             sub_url = ("%s/uri/%s/" % (ROOT, urllib.quote(self._sub_uri)))
-            get_sub = ((r'<td><a href="%s">sub</a></td>' % sub_url)
-                       + r'\s+<td>DIR</td>')
+            get_sub = ((r'<td>DIR</td>')
+                       +r'\s+<td><a href="%s">sub</a></td>' % sub_url)
             self.failUnless(re.search(get_sub, res), res)
         d.addCallback(_check)
 
-        # look at a directory which is readonly
+        # look at a readonly directory
         d.addCallback(lambda res:
                       self.GET(self.public_url + "/reedownlee", followRedirect=True))
         def _check2(res):
-            self.failUnless("(readonly)" in res, res)
+            self.failUnless("(read-only)" in res, res)
             self.failIf("Upload a file" in res, res)
         d.addCallback(_check2)
 
@@ -910,10 +1496,27 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d.addCallback(lambda res:
                       self.GET(self.public_url, followRedirect=True))
         def _check3(res):
-            self.failUnless(re.search(r'<td><a href="[\.\/]+/uri/URI%3ADIR2-RO%3A[^"]+">reedownlee</a>'
-                                      '</td>\s+<td>DIR-RO</td>', res))
+            self.failUnless(re.search('<td>DIR-RO</td>'
+                                      r'\s+<td><a href="[\.\/]+/uri/URI%3ADIR2-RO%3A[^"]+">reedownlee</a></td>', res), res)
         d.addCallback(_check3)
 
+        # and an empty directory
+        d.addCallback(lambda res: self.GET(self.public_url + "/foo/empty/"))
+        def _check4(res):
+            self.failUnless("directory is empty" in res, res)
+            MKDIR_BUTTON_RE=re.compile('<input type="hidden" name="t" value="mkdir" />.*<legend class="freeform-form-label">Create a new directory in this directory</legend>.*<input type="submit" value="Create" />', re.I)
+            self.failUnless(MKDIR_BUTTON_RE.search(res), res)
+        d.addCallback(_check4)
+
+        # and at a literal directory
+        tiny_litdir_uri = "URI:DIR2-LIT:gqytunj2onug64tufqzdcosvkjetutcjkq5gw4tvm5vwszdgnz5hgyzufqydulbshj5x2lbm" # contains one child which is itself also LIT
+        d.addCallback(lambda res:
+                      self.GET("/uri/" + tiny_litdir_uri + "/", followRedirect=True))
+        def _check5(res):
+            self.failUnless('(immutable)' in res, res)
+            self.failUnless(re.search('<td>FILE</td>'
+                                      r'\s+<td><a href="[\.\/]+/file/URI%3ALIT%3Akrugkidfnzsc4/@@named=/short">short</a></td>', res), res)
+        d.addCallback(_check5)
         return d
 
     def test_GET_DIRURL_badtype(self):
@@ -929,6 +1532,35 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d.addCallback(self.failUnlessIsFooJSON)
         return d
 
+    def test_GET_DIRURL_json_mutable_type(self):
+        d = self.PUT(self.public_url + \
+                     "/foo/sdmf.txt?mutable=true&mutable-type=sdmf",
+                     self.NEWFILE_CONTENTS * 300000)
+        d.addCallback(lambda ignored:
+            self.PUT(self.public_url + \
+                     "/foo/mdmf.txt?mutable=true&mutable-type=mdmf",
+                     self.NEWFILE_CONTENTS * 300000))
+        # Now we have an MDMF and SDMF file in the directory. If we GET
+        # its JSON, we should see their encodings.
+        d.addCallback(lambda ignored:
+            self.GET(self.public_url + "/foo?t=json"))
+        def _got_json(json):
+            data = simplejson.loads(json)
+            assert data[0] == "dirnode"
+
+            data = data[1]
+            kids = data['children']
+
+            mdmf_data = kids['mdmf.txt'][1]
+            self.failUnlessIn("mutable-type", mdmf_data)
+            self.failUnlessEqual(mdmf_data['mutable-type'], "mdmf")
+
+            sdmf_data = kids['sdmf.txt'][1]
+            self.failUnlessIn("mutable-type", sdmf_data)
+            self.failUnlessEqual(sdmf_data['mutable-type'], "sdmf")
+        d.addCallback(_got_json)
+        return d
+
 
     def test_POST_DIRURL_manifest_no_ophandle(self):
         d = self.shouldFail2(error.Error,
@@ -971,7 +1603,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
             got = {}
             for (path_list, cap) in data:
                 got[tuple(path_list)] = cap
-            self.failUnlessEqual(got[(u"sub",)], self._sub_uri)
+            self.failUnlessReallyEqual(to_str(got[(u"sub",)]), self._sub_uri)
             self.failUnless((u"sub",u"baz.txt") in got)
             self.failUnless("finished" in res)
             self.failUnless("origin" in res)
@@ -995,7 +1627,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d.addCallback(self.wait_for_operation, "126")
         d.addCallback(self.get_operation_results, "126", "json")
         def _got_json(data):
-            self.failUnlessEqual(data["finished"], True)
+            self.failUnlessReallyEqual(data["finished"], True)
             size = data["size"]
             self.failUnless(size > 1000)
         d.addCallback(_got_json)
@@ -1024,23 +1656,23 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d.addCallback(self.get_operation_results, "127", "json")
         def _got_json(stats):
             expected = {"count-immutable-files": 3,
-                        "count-mutable-files": 0,
+                        "count-mutable-files": 2,
                         "count-literal-files": 0,
-                        "count-files": 3,
+                        "count-files": 5,
                         "count-directories": 3,
                         "size-immutable-files": 57,
                         "size-literal-files": 0,
                         #"size-directories": 1912, # varies
                         #"largest-directory": 1590,
-                        "largest-directory-children": 5,
+                        "largest-directory-children": 7,
                         "largest-immutable-file": 19,
                         }
             for k,v in expected.iteritems():
-                self.failUnlessEqual(stats[k], v,
-                                     "stats[%s] was %s, not %s" %
-                                     (k, stats[k], v))
-            self.failUnlessEqual(stats["size-files-histogram"],
-                                 [ [11, 31, 3] ])
+                self.failUnlessReallyEqual(stats[k], v,
+                                           "stats[%s] was %s, not %s" %
+                                           (k, stats[k], v))
+            self.failUnlessReallyEqual(stats["size-files-histogram"],
+                                       [ [11, 31, 3] ])
         d.addCallback(_got_json)
         return d
 
@@ -1049,17 +1681,18 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         def _check(res):
             self.failUnless(res.endswith("\n"))
             units = [simplejson.loads(t) for t in res[:-1].split("\n")]
-            self.failUnlessEqual(len(units), 7)
+            self.failUnlessReallyEqual(len(units), 9)
             self.failUnlessEqual(units[-1]["type"], "stats")
             first = units[0]
             self.failUnlessEqual(first["path"], [])
-            self.failUnlessEqual(first["cap"], self._foo_uri)
+            self.failUnlessReallyEqual(to_str(first["cap"]), self._foo_uri)
             self.failUnlessEqual(first["type"], "directory")
-            baz = [u for u in units[:-1] if u["cap"] == self._baz_file_uri][0]
+            baz = [u for u in units[:-1] if to_str(u["cap"]) == self._baz_file_uri][0]
             self.failUnlessEqual(baz["path"], ["sub", "baz.txt"])
             self.failIfEqual(baz["storage-index"], None)
             self.failIfEqual(baz["verifycap"], None)
             self.failIfEqual(baz["repaircap"], None)
+            # XXX: Add quux and baz to this test.
             return
         d.addCallback(_check)
         return d
@@ -1067,14 +1700,14 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
     def test_GET_DIRURL_uri(self):
         d = self.GET(self.public_url + "/foo?t=uri")
         def _check(res):
-            self.failUnlessEqual(res, self._foo_uri)
+            self.failUnlessReallyEqual(to_str(res), self._foo_uri)
         d.addCallback(_check)
         return d
 
     def test_GET_DIRURL_readonly_uri(self):
         d = self.GET(self.public_url + "/foo?t=readonly-uri")
         def _check(res):
-            self.failUnlessEqual(res, self._foo_readonly_uri)
+            self.failUnlessReallyEqual(to_str(res), self._foo_readonly_uri)
         d.addCallback(_check)
         return d
 
@@ -1086,6 +1719,191 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d.addCallback(self.failUnlessNodeKeysAre, [])
         return d
 
+    def test_PUT_NEWDIRURL_mdmf(self):
+        d = self.PUT(self.public_url + "/foo/newdir?t=mkdir&mutable-type=mdmf", "")
+        d.addCallback(lambda res:
+                      self.failUnlessNodeHasChild(self._foo_node, u"newdir"))
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(lambda node:
+            self.failUnlessEqual(node._node.get_version(), MDMF_VERSION))
+        return d
+
+    def test_PUT_NEWDIRURL_sdmf(self):
+        d = self.PUT(self.public_url + "/foo/newdir?t=mkdir&mutable-type=sdmf",
+                     "")
+        d.addCallback(lambda res:
+                      self.failUnlessNodeHasChild(self._foo_node, u"newdir"))
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(lambda node:
+            self.failUnlessEqual(node._node.get_version(), SDMF_VERSION))
+        return d
+
+    def test_PUT_NEWDIRURL_bad_mutable_type(self):
+        return self.shouldHTTPError("test bad mutable type",
+                             400, "Bad Request", "Unknown type: foo",
+                             self.PUT, self.public_url + \
+                             "/foo/newdir=?t=mkdir&mutable-type=foo", "")
+
+    def test_POST_NEWDIRURL(self):
+        d = self.POST2(self.public_url + "/foo/newdir?t=mkdir", "")
+        d.addCallback(lambda res:
+                      self.failUnlessNodeHasChild(self._foo_node, u"newdir"))
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(self.failUnlessNodeKeysAre, [])
+        return d
+
+    def test_POST_NEWDIRURL_mdmf(self):
+        d = self.POST2(self.public_url + "/foo/newdir?t=mkdir&mutable-type=mdmf", "")
+        d.addCallback(lambda res:
+                      self.failUnlessNodeHasChild(self._foo_node, u"newdir"))
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(lambda node:
+            self.failUnlessEqual(node._node.get_version(), MDMF_VERSION))
+        return d
+
+    def test_POST_NEWDIRURL_sdmf(self):
+        d = self.POST2(self.public_url + "/foo/newdir?t=mkdir&mutable-type=sdmf", "")
+        d.addCallback(lambda res:
+            self.failUnlessNodeHasChild(self._foo_node, u"newdir"))
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(lambda node:
+            self.failUnlessEqual(node._node.get_version(), SDMF_VERSION))
+        return d
+
+    def test_POST_NEWDIRURL_bad_mutable_type(self):
+        return self.shouldHTTPError("test bad mutable type",
+                                    400, "Bad Request", "Unknown type: foo",
+                                    self.POST2, self.public_url + \
+                                    "/foo/newdir?t=mkdir&mutable-type=foo", "")
+
+    def test_POST_NEWDIRURL_emptyname(self):
+        # an empty pathname component (i.e. a double-slash) is disallowed
+        d = self.shouldFail2(error.Error, "test_POST_NEWDIRURL_emptyname",
+                             "400 Bad Request",
+                             "The webapi does not allow empty pathname components, i.e. a double slash",
+                             self.POST, self.public_url + "//?t=mkdir")
+        return d
+
+    def _do_POST_NEWDIRURL_initial_children_test(self, version=None):
+        (newkids, caps) = self._create_initial_children()
+        query = "/foo/newdir?t=mkdir-with-children"
+        if version == MDMF_VERSION:
+            query += "&mutable-type=mdmf"
+        elif version == SDMF_VERSION:
+            query += "&mutable-type=sdmf"
+        else:
+            version = SDMF_VERSION # for later
+        d = self.POST2(self.public_url + query,
+                       simplejson.dumps(newkids))
+        def _check(uri):
+            n = self.s.create_node_from_uri(uri.strip())
+            d2 = self.failUnlessNodeKeysAre(n, newkids.keys())
+            self.failUnlessEqual(n._node.get_version(), version)
+            d2.addCallback(lambda ign:
+                           self.failUnlessROChildURIIs(n, u"child-imm",
+                                                       caps['filecap1']))
+            d2.addCallback(lambda ign:
+                           self.failUnlessRWChildURIIs(n, u"child-mutable",
+                                                       caps['filecap2']))
+            d2.addCallback(lambda ign:
+                           self.failUnlessROChildURIIs(n, u"child-mutable-ro",
+                                                       caps['filecap3']))
+            d2.addCallback(lambda ign:
+                           self.failUnlessROChildURIIs(n, u"unknownchild-ro",
+                                                       caps['unknown_rocap']))
+            d2.addCallback(lambda ign:
+                           self.failUnlessRWChildURIIs(n, u"unknownchild-rw",
+                                                       caps['unknown_rwcap']))
+            d2.addCallback(lambda ign:
+                           self.failUnlessROChildURIIs(n, u"unknownchild-imm",
+                                                       caps['unknown_immcap']))
+            d2.addCallback(lambda ign:
+                           self.failUnlessRWChildURIIs(n, u"dirchild",
+                                                       caps['dircap']))
+            d2.addCallback(lambda ign:
+                           self.failUnlessROChildURIIs(n, u"dirchild-lit",
+                                                       caps['litdircap']))
+            d2.addCallback(lambda ign:
+                           self.failUnlessROChildURIIs(n, u"dirchild-empty",
+                                                       caps['emptydircap']))
+            return d2
+        d.addCallback(_check)
+        d.addCallback(lambda res:
+                      self.failUnlessNodeHasChild(self._foo_node, u"newdir"))
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(self.failUnlessNodeKeysAre, newkids.keys())
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(self.failUnlessROChildURIIs, u"child-imm", caps['filecap1'])
+        return d
+
+    def test_POST_NEWDIRURL_initial_children(self):
+        return self._do_POST_NEWDIRURL_initial_children_test()
+
+    def test_POST_NEWDIRURL_initial_children_mdmf(self):
+        return self._do_POST_NEWDIRURL_initial_children_test(MDMF_VERSION)
+
+    def test_POST_NEWDIRURL_initial_children_sdmf(self):
+        return self._do_POST_NEWDIRURL_initial_children_test(SDMF_VERSION)
+
+    def test_POST_NEWDIRURL_initial_children_bad_mutable_type(self):
+        (newkids, caps) = self._create_initial_children()
+        return self.shouldHTTPError("test bad mutable type",
+                                    400, "Bad Request", "Unknown type: foo",
+                                    self.POST2, self.public_url + \
+                                    "/foo/newdir?t=mkdir-with-children&mutable-type=foo",
+                                    simplejson.dumps(newkids))
+
+    def test_POST_NEWDIRURL_immutable(self):
+        (newkids, caps) = self._create_immutable_children()
+        d = self.POST2(self.public_url + "/foo/newdir?t=mkdir-immutable",
+                       simplejson.dumps(newkids))
+        def _check(uri):
+            n = self.s.create_node_from_uri(uri.strip())
+            d2 = self.failUnlessNodeKeysAre(n, newkids.keys())
+            d2.addCallback(lambda ign:
+                           self.failUnlessROChildURIIs(n, u"child-imm",
+                                                       caps['filecap1']))
+            d2.addCallback(lambda ign:
+                           self.failUnlessROChildURIIs(n, u"unknownchild-imm",
+                                                       caps['unknown_immcap']))
+            d2.addCallback(lambda ign:
+                           self.failUnlessROChildURIIs(n, u"dirchild-imm",
+                                                       caps['immdircap']))
+            d2.addCallback(lambda ign:
+                           self.failUnlessROChildURIIs(n, u"dirchild-lit",
+                                                       caps['litdircap']))
+            d2.addCallback(lambda ign:
+                           self.failUnlessROChildURIIs(n, u"dirchild-empty",
+                                                       caps['emptydircap']))
+            return d2
+        d.addCallback(_check)
+        d.addCallback(lambda res:
+                      self.failUnlessNodeHasChild(self._foo_node, u"newdir"))
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(self.failUnlessNodeKeysAre, newkids.keys())
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(self.failUnlessROChildURIIs, u"child-imm", caps['filecap1'])
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(self.failUnlessROChildURIIs, u"unknownchild-imm", caps['unknown_immcap'])
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(self.failUnlessROChildURIIs, u"dirchild-imm", caps['immdircap'])
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(self.failUnlessROChildURIIs, u"dirchild-lit", caps['litdircap'])
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(self.failUnlessROChildURIIs, u"dirchild-empty", caps['emptydircap'])
+        d.addErrback(self.explain_web_error)
+        return d
+
+    def test_POST_NEWDIRURL_immutable_bad(self):
+        (newkids, caps) = self._create_initial_children()
+        d = self.shouldFail2(error.Error, "test_POST_NEWDIRURL_immutable_bad",
+                             "400 Bad Request",
+                             "needed to be immutable but was not",
+                             self.POST2,
+                             self.public_url + "/foo/newdir?t=mkdir-immutable",
+                             simplejson.dumps(newkids))
+        return d
+
     def test_PUT_NEWDIRURL_exists(self):
         d = self.PUT(self.public_url + "/foo/sub?t=mkdir", "")
         d.addCallback(lambda res:
@@ -1115,9 +1933,9 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
             d = self.POST(url)
             def made_subsub(ssuri):
                 d = self._foo_node.get_child_at_path(u"mkp/sub1/sub2")
-                d.addCallback(lambda ssnode: self.failUnlessEqual(ssnode.get_uri(), ssuri))
+                d.addCallback(lambda ssnode: self.failUnlessReallyEqual(ssnode.get_uri(), ssuri))
                 d = self.POST(url)
-                d.addCallback(lambda uri2: self.failUnlessEqual(uri2, ssuri))
+                d.addCallback(lambda uri2: self.failUnlessReallyEqual(uri2, ssuri))
                 return d
             d.addCallback(made_subsub)
             return d
@@ -1135,6 +1953,49 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d.addCallback(self.failUnlessNodeKeysAre, [])
         return d
 
+    def test_PUT_NEWDIRURL_mkdirs_mdmf(self):
+        d = self.PUT(self.public_url + "/foo/subdir/newdir?t=mkdir&mutable-type=mdmf", "")
+        d.addCallback(lambda ignored:
+            self.failUnlessNodeHasChild(self._foo_node, u"subdir"))
+        d.addCallback(lambda ignored:
+            self.failIfNodeHasChild(self._foo_node, u"newdir"))
+        d.addCallback(lambda ignored:
+            self._foo_node.get_child_at_path(u"subdir"))
+        def _got_subdir(subdir):
+            # XXX: What we want?
+            #self.failUnlessEqual(subdir._node.get_version(), MDMF_VERSION)
+            self.failUnlessNodeHasChild(subdir, u"newdir")
+            return subdir.get_child_at_path(u"newdir")
+        d.addCallback(_got_subdir)
+        d.addCallback(lambda newdir:
+            self.failUnlessEqual(newdir._node.get_version(), MDMF_VERSION))
+        return d
+
+    def test_PUT_NEWDIRURL_mkdirs_sdmf(self):
+        d = self.PUT(self.public_url + "/foo/subdir/newdir?t=mkdir&mutable-type=sdmf", "")
+        d.addCallback(lambda ignored:
+            self.failUnlessNodeHasChild(self._foo_node, u"subdir"))
+        d.addCallback(lambda ignored:
+            self.failIfNodeHasChild(self._foo_node, u"newdir"))
+        d.addCallback(lambda ignored:
+            self._foo_node.get_child_at_path(u"subdir"))
+        def _got_subdir(subdir):
+            # XXX: What we want?
+            #self.failUnlessEqual(subdir._node.get_version(), MDMF_VERSION)
+            self.failUnlessNodeHasChild(subdir, u"newdir")
+            return subdir.get_child_at_path(u"newdir")
+        d.addCallback(_got_subdir)
+        d.addCallback(lambda newdir:
+            self.failUnlessEqual(newdir._node.get_version(), SDMF_VERSION))
+        return d
+
+    def test_PUT_NEWDIRURL_mkdirs_bad_mutable_type(self):
+        return self.shouldHTTPError("test bad mutable type",
+                                    400, "Bad Request", "Unknown type: foo",
+                                    self.PUT, self.public_url + \
+                                    "/foo/subdir/newdir?t=mkdir&mutable-type=foo",
+                                    "")
+
     def test_DELETE_DIRURL(self):
         d = self.DELETE(self.public_url + "/foo")
         d.addCallback(lambda res:
@@ -1166,7 +2027,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
             assert isinstance(k, unicode)
         d = node.list()
         def _check(children):
-            self.failUnlessEqual(sorted(children.keys()), sorted(expected_keys))
+            self.failUnlessReallyEqual(sorted(children.keys()), sorted(expected_keys))
         d.addCallback(_check)
         return d
     def failUnlessNodeHasChild(self, node, name):
@@ -1187,9 +2048,9 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
     def failUnlessChildContentsAre(self, node, name, expected_contents):
         assert isinstance(name, unicode)
         d = node.get_child_at_path(name)
-        d.addCallback(lambda node: node.download_to_data())
+        d.addCallback(lambda node: download_to_data(node))
         def _check(contents):
-            self.failUnlessEqual(contents, expected_contents)
+            self.failUnlessReallyEqual(contents, expected_contents)
         d.addCallback(_check)
         return d
 
@@ -1198,23 +2059,55 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d = node.get_child_at_path(name)
         d.addCallback(lambda node: node.download_best_version())
         def _check(contents):
-            self.failUnlessEqual(contents, expected_contents)
+            self.failUnlessReallyEqual(contents, expected_contents)
+        d.addCallback(_check)
+        return d
+
+    def failUnlessRWChildURIIs(self, node, name, expected_uri):
+        assert isinstance(name, unicode)
+        d = node.get_child_at_path(name)
+        def _check(child):
+            self.failUnless(child.is_unknown() or not child.is_readonly())
+            self.failUnlessReallyEqual(child.get_uri(), expected_uri.strip())
+            self.failUnlessReallyEqual(child.get_write_uri(), expected_uri.strip())
+            expected_ro_uri = self._make_readonly(expected_uri)
+            if expected_ro_uri:
+                self.failUnlessReallyEqual(child.get_readonly_uri(), expected_ro_uri.strip())
+        d.addCallback(_check)
+        return d
+
+    def failUnlessROChildURIIs(self, node, name, expected_uri):
+        assert isinstance(name, unicode)
+        d = node.get_child_at_path(name)
+        def _check(child):
+            self.failUnless(child.is_unknown() or child.is_readonly())
+            self.failUnlessReallyEqual(child.get_write_uri(), None)
+            self.failUnlessReallyEqual(child.get_uri(), expected_uri.strip())
+            self.failUnlessReallyEqual(child.get_readonly_uri(), expected_uri.strip())
         d.addCallback(_check)
         return d
 
-    def failUnlessChildURIIs(self, node, name, expected_uri):
+    def failUnlessURIMatchesRWChild(self, got_uri, node, name):
         assert isinstance(name, unicode)
         d = node.get_child_at_path(name)
         def _check(child):
-            self.failUnlessEqual(child.get_uri(), expected_uri.strip())
+            self.failUnless(child.is_unknown() or not child.is_readonly())
+            self.failUnlessReallyEqual(child.get_uri(), got_uri.strip())
+            self.failUnlessReallyEqual(child.get_write_uri(), got_uri.strip())
+            expected_ro_uri = self._make_readonly(got_uri)
+            if expected_ro_uri:
+                self.failUnlessReallyEqual(child.get_readonly_uri(), expected_ro_uri.strip())
         d.addCallback(_check)
         return d
 
-    def failUnlessURIMatchesChild(self, got_uri, node, name):
+    def failUnlessURIMatchesROChild(self, got_uri, node, name):
         assert isinstance(name, unicode)
         d = node.get_child_at_path(name)
         def _check(child):
-            self.failUnlessEqual(got_uri.strip(), child.get_uri())
+            self.failUnless(child.is_unknown() or child.is_readonly())
+            self.failUnlessReallyEqual(child.get_write_uri(), None)
+            self.failUnlessReallyEqual(got_uri.strip(), child.get_uri())
+            self.failUnlessReallyEqual(got_uri.strip(), child.get_readonly_uri())
         d.addCallback(_check)
         return d
 
@@ -1225,7 +2118,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d = self.POST(self.public_url + "/foo", t="upload",
                       file=("new.txt", self.NEWFILE_CONTENTS))
         fn = self._foo_node
-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt")
+        d.addCallback(self.failUnlessURIMatchesROChild, fn, u"new.txt")
         d.addCallback(lambda res:
                       self.failUnlessChildContentsAre(fn, u"new.txt",
                                                       self.NEWFILE_CONTENTS))
@@ -1236,15 +2129,15 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d = self.POST(self.public_url + "/foo", t="upload",
                       file=(filename, self.NEWFILE_CONTENTS))
         fn = self._foo_node
-        d.addCallback(self.failUnlessURIMatchesChild, fn, filename)
+        d.addCallback(self.failUnlessURIMatchesROChild, fn, filename)
         d.addCallback(lambda res:
                       self.failUnlessChildContentsAre(fn, filename,
                                                       self.NEWFILE_CONTENTS))
         target_url = self.public_url + "/foo/" + filename.encode("utf-8")
         d.addCallback(lambda res: self.GET(target_url))
-        d.addCallback(lambda contents: self.failUnlessEqual(contents,
-                                                            self.NEWFILE_CONTENTS,
-                                                            contents))
+        d.addCallback(lambda contents: self.failUnlessReallyEqual(contents,
+                                                                  self.NEWFILE_CONTENTS,
+                                                                  contents))
         return d
 
     def test_POST_upload_unicode_named(self):
@@ -1253,15 +2146,15 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
                       name=filename,
                       file=("overridden", self.NEWFILE_CONTENTS))
         fn = self._foo_node
-        d.addCallback(self.failUnlessURIMatchesChild, fn, filename)
+        d.addCallback(self.failUnlessURIMatchesROChild, fn, filename)
         d.addCallback(lambda res:
                       self.failUnlessChildContentsAre(fn, filename,
                                                       self.NEWFILE_CONTENTS))
         target_url = self.public_url + "/foo/" + filename.encode("utf-8")
         d.addCallback(lambda res: self.GET(target_url))
-        d.addCallback(lambda contents: self.failUnlessEqual(contents,
-                                                            self.NEWFILE_CONTENTS,
-                                                            contents))
+        d.addCallback(lambda contents: self.failUnlessReallyEqual(contents,
+                                                                  self.NEWFILE_CONTENTS,
+                                                                  contents))
         return d
 
     def test_POST_upload_no_link(self):
@@ -1302,7 +2195,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
 
     def test_POST_upload_no_link_whendone_results(self):
         def check(statuscode, target):
-            self.failUnlessEqual(statuscode, str(http.FOUND))
+            self.failUnlessReallyEqual(statuscode, str(http.FOUND))
             self.failUnless(target.startswith(self.webish_url), target)
             return client.getPage(target, method="GET")
         d = self.shouldRedirect2("test_POST_upload_no_link_whendone_results",
@@ -1311,51 +2204,114 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
                                  when_done="/uri/%(uri)s",
                                  file=("new.txt", self.NEWFILE_CONTENTS))
         d.addCallback(lambda res:
-                      self.failUnlessEqual(res, self.NEWFILE_CONTENTS))
+                      self.failUnlessReallyEqual(res, self.NEWFILE_CONTENTS))
         return d
 
     def test_POST_upload_no_link_mutable(self):
         d = self.POST("/uri", t="upload", mutable="true",
                       file=("new.txt", self.NEWFILE_CONTENTS))
-        def _check(new_uri):
-            new_uri = new_uri.strip()
-            self.new_uri = new_uri
-            u = IURI(new_uri)
-            self.failUnless(IMutableFileURI.providedBy(u))
-            self.failUnless(u.storage_index in FakeMutableFileNode.all_contents)
-            n = self.s.create_node_from_uri(new_uri)
+        def _check(filecap):
+            filecap = filecap.strip()
+            self.failUnless(filecap.startswith("URI:SSK:"), filecap)
+            self.filecap = filecap
+            u = uri.WriteableSSKFileURI.init_from_string(filecap)
+            self.failUnless(u.get_storage_index() in FakeMutableFileNode.all_contents)
+            n = self.s.create_node_from_uri(filecap)
             return n.download_best_version()
         d.addCallback(_check)
         def _check2(data):
-            self.failUnlessEqual(data, self.NEWFILE_CONTENTS)
-            return self.GET("/uri/%s" % urllib.quote(self.new_uri))
+            self.failUnlessReallyEqual(data, self.NEWFILE_CONTENTS)
+            return self.GET("/uri/%s" % urllib.quote(self.filecap))
         d.addCallback(_check2)
         def _check3(data):
-            self.failUnlessEqual(data, self.NEWFILE_CONTENTS)
-            return self.GET("/file/%s" % urllib.quote(self.new_uri))
+            self.failUnlessReallyEqual(data, self.NEWFILE_CONTENTS)
+            return self.GET("/file/%s" % urllib.quote(self.filecap))
         d.addCallback(_check3)
         def _check4(data):
-            self.failUnlessEqual(data, self.NEWFILE_CONTENTS)
+            self.failUnlessReallyEqual(data, self.NEWFILE_CONTENTS)
         d.addCallback(_check4)
         return d
 
     def test_POST_upload_no_link_mutable_toobig(self):
-        d = self.shouldFail2(error.Error,
-                             "test_POST_upload_no_link_mutable_toobig",
-                             "413 Request Entity Too Large",
-                             "SDMF is limited to one segment, and 10001 > 10000",
-                             self.POST,
-                             "/uri", t="upload", mutable="true",
-                             file=("new.txt",
-                                   "b" * (self.s.MUTABLE_SIZELIMIT+1)) )
+        # The SDMF size limit is no longer in place, so we should be
+        # able to upload mutable files that are as large as we want them
+        # to be.
+        d = self.POST("/uri", t="upload", mutable="true",
+                      file=("new.txt", "b" * (self.s.MUTABLE_SIZELIMIT + 1)))
         return d
 
+
+    def test_POST_upload_mutable_type_unlinked(self):
+        d = self.POST("/uri?t=upload&mutable=true&mutable-type=sdmf",
+                      file=("sdmf.txt", self.NEWFILE_CONTENTS * 300000))
+        d.addCallback(lambda filecap: self.GET("/uri/%s?t=json" % filecap))
+        def _got_json(json, version):
+            data = simplejson.loads(json)
+            data = data[1]
+
+            self.failUnlessIn("mutable-type", data)
+            self.failUnlessEqual(data['mutable-type'], version)
+        d.addCallback(_got_json, "sdmf")
+        d.addCallback(lambda ignored:
+            self.POST("/uri?t=upload&mutable=true&mutable-type=mdmf",
+                      file=('mdmf.txt', self.NEWFILE_CONTENTS * 300000)))
+        def _got_filecap(filecap):
+            self.failUnless(filecap.startswith("URI:MDMF"))
+            return filecap
+        d.addCallback(_got_filecap)
+        d.addCallback(lambda filecap: self.GET("/uri/%s?t=json" % filecap))
+        d.addCallback(_got_json, "mdmf")
+        return d
+
+    def test_POST_upload_mutable_type_unlinked_bad_mutable_type(self):
+        return self.shouldHTTPError("test bad mutable type",
+                                    400, "Bad Request", "Unknown type: foo",
+                                    self.POST,
+                                    "/uri?5=upload&mutable=true&mutable-type=foo",
+                                    file=("foo.txt", self.NEWFILE_CONTENTS * 300000))
+
+    def test_POST_upload_mutable_type(self):
+        d = self.POST(self.public_url + \
+                      "/foo?t=upload&mutable=true&mutable-type=sdmf",
+                      file=("sdmf.txt", self.NEWFILE_CONTENTS * 300000))
+        fn = self._foo_node
+        def _got_cap(filecap, filename):
+            filenameu = unicode(filename)
+            self.failUnlessURIMatchesRWChild(filecap, fn, filenameu)
+            return self.GET(self.public_url + "/foo/%s?t=json" % filename)
+        def _got_mdmf_cap(filecap):
+            self.failUnless(filecap.startswith("URI:MDMF"))
+            return filecap
+        d.addCallback(_got_cap, "sdmf.txt")
+        def _got_json(json, version):
+            data = simplejson.loads(json)
+            data = data[1]
+
+            self.failUnlessIn("mutable-type", data)
+            self.failUnlessEqual(data['mutable-type'], version)
+        d.addCallback(_got_json, "sdmf")
+        d.addCallback(lambda ignored:
+            self.POST(self.public_url + \
+                      "/foo?t=upload&mutable=true&mutable-type=mdmf",
+                      file=("mdmf.txt", self.NEWFILE_CONTENTS * 300000)))
+        d.addCallback(_got_mdmf_cap)
+        d.addCallback(_got_cap, "mdmf.txt")
+        d.addCallback(_got_json, "mdmf")
+        return d
+
+    def test_POST_upload_bad_mutable_type(self):
+        return self.shouldHTTPError("test bad mutable type",
+                                    400, "Bad Request", "Unknown type: foo",
+                                    self.POST, self.public_url + \
+                                    "/foo?t=upload&mutable=true&mutable-type=foo",
+                                    file=("foo.txt", self.NEWFILE_CONTENTS * 300000))
+
     def test_POST_upload_mutable(self):
         # this creates a mutable file
         d = self.POST(self.public_url + "/foo", t="upload", mutable="true",
                       file=("new.txt", self.NEWFILE_CONTENTS))
         fn = self._foo_node
-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt")
+        d.addCallback(self.failUnlessURIMatchesRWChild, fn, u"new.txt")
         d.addCallback(lambda res:
                       self.failUnlessMutableChildContentsAre(fn, u"new.txt",
                                                              self.NEWFILE_CONTENTS))
@@ -1374,7 +2330,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
                       self.POST(self.public_url + "/foo", t="upload",
                                 mutable="true",
                                 file=("new.txt", NEWER_CONTENTS)))
-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt")
+        d.addCallback(self.failUnlessURIMatchesRWChild, fn, u"new.txt")
         d.addCallback(lambda res:
                       self.failUnlessMutableChildContentsAre(fn, u"new.txt",
                                                              NEWER_CONTENTS))
@@ -1383,14 +2339,14 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
             self.failUnless(IMutableFileNode.providedBy(newnode))
             self.failUnless(newnode.is_mutable())
             self.failIf(newnode.is_readonly())
-            self.failUnlessEqual(self._mutable_uri, newnode.get_uri())
+            self.failUnlessReallyEqual(self._mutable_uri, newnode.get_uri())
         d.addCallback(_got2)
 
         # upload a second time, using PUT instead of POST
         NEW2_CONTENTS = NEWER_CONTENTS + "overwrite with PUT\n"
         d.addCallback(lambda res:
                       self.PUT(self.public_url + "/foo/new.txt", NEW2_CONTENTS))
-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt")
+        d.addCallback(self.failUnlessURIMatchesRWChild, fn, u"new.txt")
         d.addCallback(lambda res:
                       self.failUnlessMutableChildContentsAre(fn, u"new.txt",
                                                              NEW2_CONTENTS))
@@ -1412,7 +2368,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
             self.failUnless(IMutableFileNode.providedBy(newnode))
             self.failUnless(newnode.is_mutable())
             self.failIf(newnode.is_readonly())
-            self.failUnlessEqual(self._mutable_uri, newnode.get_uri())
+            self.failUnlessReallyEqual(self._mutable_uri, newnode.get_uri())
         d.addCallback(_got3)
 
         # look at the JSON form of the enclosing directory
@@ -1425,13 +2381,13 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
             children = dict( [(unicode(name),value)
                               for (name,value)
                               in parsed[1]["children"].iteritems()] )
-            self.failUnless("new.txt" in children)
-            new_json = children["new.txt"]
+            self.failUnless(u"new.txt" in children)
+            new_json = children[u"new.txt"]
             self.failUnlessEqual(new_json[0], "filenode")
             self.failUnless(new_json[1]["mutable"])
-            self.failUnlessEqual(new_json[1]["rw_uri"], self._mutable_uri)
-            ro_uri = unicode(self._mutable_node.get_readonly().to_string())
-            self.failUnlessEqual(new_json[1]["ro_uri"], ro_uri)
+            self.failUnlessReallyEqual(to_str(new_json[1]["rw_uri"]), self._mutable_uri)
+            ro_uri = self._mutable_node.get_readonly().to_string()
+            self.failUnlessReallyEqual(to_str(new_json[1]["ro_uri"]), ro_uri)
         d.addCallback(_check_page_json)
 
         # and the JSON form of the file
@@ -1441,65 +2397,57 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
             parsed = simplejson.loads(res)
             self.failUnlessEqual(parsed[0], "filenode")
             self.failUnless(parsed[1]["mutable"])
-            self.failUnlessEqual(parsed[1]["rw_uri"], self._mutable_uri)
-            ro_uri = unicode(self._mutable_node.get_readonly().to_string())
-            self.failUnlessEqual(parsed[1]["ro_uri"], ro_uri)
+            self.failUnlessReallyEqual(to_str(parsed[1]["rw_uri"]), self._mutable_uri)
+            ro_uri = self._mutable_node.get_readonly().to_string()
+            self.failUnlessReallyEqual(to_str(parsed[1]["ro_uri"]), ro_uri)
         d.addCallback(_check_file_json)
 
         # and look at t=uri and t=readonly-uri
         d.addCallback(lambda res:
                       self.GET(self.public_url + "/foo/new.txt?t=uri"))
-        d.addCallback(lambda res: self.failUnlessEqual(res, self._mutable_uri))
+        d.addCallback(lambda res: self.failUnlessReallyEqual(res, self._mutable_uri))
         d.addCallback(lambda res:
                       self.GET(self.public_url + "/foo/new.txt?t=readonly-uri"))
         def _check_ro_uri(res):
-            ro_uri = unicode(self._mutable_node.get_readonly().to_string())
-            self.failUnlessEqual(res, ro_uri)
+            ro_uri = self._mutable_node.get_readonly().to_string()
+            self.failUnlessReallyEqual(res, ro_uri)
         d.addCallback(_check_ro_uri)
 
         # make sure we can get to it from /uri/URI
         d.addCallback(lambda res:
                       self.GET("/uri/%s" % urllib.quote(self._mutable_uri)))
         d.addCallback(lambda res:
-                      self.failUnlessEqual(res, NEW2_CONTENTS))
+                      self.failUnlessReallyEqual(res, NEW2_CONTENTS))
 
         # and that HEAD computes the size correctly
         d.addCallback(lambda res:
                       self.HEAD(self.public_url + "/foo/new.txt",
                                 return_response=True))
         def _got_headers((res, status, headers)):
-            self.failUnlessEqual(res, "")
-            self.failUnlessEqual(headers["content-length"][0],
-                                 str(len(NEW2_CONTENTS)))
-            self.failUnlessEqual(headers["content-type"], ["text/plain"])
+            self.failUnlessReallyEqual(res, "")
+            self.failUnlessReallyEqual(headers["content-length"][0],
+                                       str(len(NEW2_CONTENTS)))
+            self.failUnlessReallyEqual(headers["content-type"], ["text/plain"])
         d.addCallback(_got_headers)
 
-        # make sure that size errors are displayed correctly for overwrite
-        d.addCallback(lambda res:
-                      self.shouldFail2(error.Error,
-                                       "test_POST_upload_mutable-toobig",
-                                       "413 Request Entity Too Large",
-                                       "SDMF is limited to one segment, and 10001 > 10000",
-                                       self.POST,
-                                       self.public_url + "/foo", t="upload",
-                                       mutable="true",
-                                       file=("new.txt",
-                                             "b" * (self.s.MUTABLE_SIZELIMIT+1)),
-                                       ))
-
+        # make sure that outdated size limits aren't enforced anymore.
+        d.addCallback(lambda ignored:
+            self.POST(self.public_url + "/foo", t="upload",
+                      mutable="true",
+                      file=("new.txt",
+                            "b" * (self.s.MUTABLE_SIZELIMIT+1))))
         d.addErrback(self.dump_error)
         return d
 
     def test_POST_upload_mutable_toobig(self):
-        d = self.shouldFail2(error.Error,
-                             "test_POST_upload_no_link_mutable_toobig",
-                             "413 Request Entity Too Large",
-                             "SDMF is limited to one segment, and 10001 > 10000",
-                             self.POST,
-                             self.public_url + "/foo",
-                             t="upload", mutable="true",
-                             file=("new.txt",
-                                   "b" * (self.s.MUTABLE_SIZELIMIT+1)) )
+        # SDMF had a size limti that was removed a while ago. MDMF has
+        # never had a size limit. Test to make sure that we do not
+        # encounter errors when trying to upload large mutable files,
+        # since there should be no coded prohibitions regarding large
+        # mutable files.
+        d = self.POST(self.public_url + "/foo",
+                      t="upload", mutable="true",
+                      file=("new.txt", "b" * (self.s.MUTABLE_SIZELIMIT + 1)))
         return d
 
     def dump_error(self, f):
@@ -1519,7 +2467,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d = self.POST(self.public_url + "/foo", t="upload",
                       file=("bar.txt", self.NEWFILE_CONTENTS))
         fn = self._foo_node
-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"bar.txt")
+        d.addCallback(self.failUnlessURIMatchesROChild, fn, u"bar.txt")
         d.addCallback(lambda res:
                       self.failUnlessChildContentsAre(fn, u"bar.txt",
                                                       self.NEWFILE_CONTENTS))
@@ -1529,8 +2477,8 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d = self.POST(self.public_url + "/foo?replace=false", t="upload",
                       file=("new.txt", self.NEWFILE_CONTENTS))
         d.addCallback(lambda res: self.GET(self.public_url + "/foo/new.txt"))
-        d.addCallback(lambda res: self.failUnlessEqual(res,
-                                                       self.NEWFILE_CONTENTS))
+        d.addCallback(lambda res: self.failUnlessReallyEqual(res,
+                                                             self.NEWFILE_CONTENTS))
         return d
 
     def test_POST_upload_no_replace_queryarg(self):
@@ -1570,7 +2518,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         fn = self._foo_node
         d = self.POST(self.public_url + "/foo", t="upload",
                       name="new.txt", file=self.NEWFILE_CONTENTS)
-        d.addCallback(self.failUnlessURIMatchesChild, fn, u"new.txt")
+        d.addCallback(self.failUnlessURIMatchesROChild, fn, u"new.txt")
         d.addCallback(lambda res:
                       self.failUnlessChildContentsAre(fn, u"new.txt",
                                                       self.NEWFILE_CONTENTS))
@@ -1587,8 +2535,8 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         # make sure that nothing was added
         d.addCallback(lambda res:
                       self.failUnlessNodeKeysAre(self._foo_node,
-                                                 [u"bar.txt", u"blockingfile",
-                                                  u"empty", u"n\u00fc.txt",
+                                                 [u"bar.txt", u"baz.txt", u"blockingfile",
+                                                  u"empty", u"n\u00fc.txt", u"quux.txt",
                                                   u"sub"]))
         return d
 
@@ -1600,8 +2548,8 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d.addCallback(_check)
         redir_url = "http://allmydata.org/TARGET"
         def _check2(statuscode, target):
-            self.failUnlessEqual(statuscode, str(http.FOUND))
-            self.failUnlessEqual(target, redir_url)
+            self.failUnlessReallyEqual(statuscode, str(http.FOUND))
+            self.failUnlessReallyEqual(target, redir_url)
         d.addCallback(lambda res:
                       self.shouldRedirect2("test_POST_FILEURL_check",
                                            _check2,
@@ -1612,7 +2560,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
                       self.POST(bar_url, t="check", return_to=redir_url))
         def _check3(res):
             self.failUnless("Healthy :" in res)
-            self.failUnless("Return to parent directory" in res)
+            self.failUnless("Return to file" in res)
             self.failUnless(redir_url in res)
         d.addCallback(_check3)
 
@@ -1634,8 +2582,8 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d.addCallback(_check)
         redir_url = "http://allmydata.org/TARGET"
         def _check2(statuscode, target):
-            self.failUnlessEqual(statuscode, str(http.FOUND))
-            self.failUnlessEqual(target, redir_url)
+            self.failUnlessReallyEqual(statuscode, str(http.FOUND))
+            self.failUnlessReallyEqual(target, redir_url)
         d.addCallback(lambda res:
                       self.shouldRedirect2("test_POST_FILEURL_check_and_repair",
                                            _check2,
@@ -1646,7 +2594,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
                       self.POST(bar_url, t="check", return_to=redir_url))
         def _check3(res):
             self.failUnless("Healthy :" in res)
-            self.failUnless("Return to parent directory" in res)
+            self.failUnless("Return to file" in res)
             self.failUnless(redir_url in res)
         d.addCallback(_check3)
         return d
@@ -1659,8 +2607,8 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d.addCallback(_check)
         redir_url = "http://allmydata.org/TARGET"
         def _check2(statuscode, target):
-            self.failUnlessEqual(statuscode, str(http.FOUND))
-            self.failUnlessEqual(target, redir_url)
+            self.failUnlessReallyEqual(statuscode, str(http.FOUND))
+            self.failUnlessReallyEqual(target, redir_url)
         d.addCallback(lambda res:
                       self.shouldRedirect2("test_POST_DIRURL_check",
                                            _check2,
@@ -1671,7 +2619,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
                       self.POST(foo_url, t="check", return_to=redir_url))
         def _check3(res):
             self.failUnless("Healthy :" in res, res)
-            self.failUnless("Return to parent directory" in res)
+            self.failUnless("Return to file/directory" in res)
             self.failUnless(redir_url in res)
         d.addCallback(_check3)
 
@@ -1693,8 +2641,8 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d.addCallback(_check)
         redir_url = "http://allmydata.org/TARGET"
         def _check2(statuscode, target):
-            self.failUnlessEqual(statuscode, str(http.FOUND))
-            self.failUnlessEqual(target, redir_url)
+            self.failUnlessReallyEqual(statuscode, str(http.FOUND))
+            self.failUnlessReallyEqual(target, redir_url)
         d.addCallback(lambda res:
                       self.shouldRedirect2("test_POST_DIRURL_check_and_repair",
                                            _check2,
@@ -1705,11 +2653,36 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
                       self.POST(foo_url, t="check", return_to=redir_url))
         def _check3(res):
             self.failUnless("Healthy :" in res)
-            self.failUnless("Return to parent directory" in res)
+            self.failUnless("Return to file/directory" in res)
             self.failUnless(redir_url in res)
         d.addCallback(_check3)
         return d
 
+    def test_POST_FILEURL_mdmf_check(self):
+        quux_url = "/uri/%s" % urllib.quote(self._quux_txt_uri)
+        d = self.POST(quux_url, t="check")
+        def _check(res):
+            self.failUnlessIn("Healthy", res)
+        d.addCallback(_check)
+        quux_extension_url = "/uri/%s" % urllib.quote("%s:3:131073" % self._quux_txt_uri)
+        d.addCallback(lambda ignored:
+            self.POST(quux_extension_url, t="check"))
+        d.addCallback(_check)
+        return d
+
+    def test_POST_FILEURL_mdmf_check_and_repair(self):
+        quux_url = "/uri/%s" % urllib.quote(self._quux_txt_uri)
+        d = self.POST(quux_url, t="check", repair="true")
+        def _check(res):
+            self.failUnlessIn("Healthy", res)
+        d.addCallback(_check)
+        quux_extension_url = "/uri/%s" %\
+            urllib.quote("%s:3:131073" % self._quux_txt_uri)
+        d.addCallback(lambda ignored:
+            self.POST(quux_extension_url, t="check", repair="true"))
+        d.addCallback(_check)
+        return d
+
     def wait_for_operation(self, ignored, ophandle):
         url = "/operations/" + ophandle
         url += "?t=status&output=JSON"
@@ -1747,21 +2720,21 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
 
     def test_POST_DIRURL_deepcheck(self):
         def _check_redirect(statuscode, target):
-            self.failUnlessEqual(statuscode, str(http.FOUND))
+            self.failUnlessReallyEqual(statuscode, str(http.FOUND))
             self.failUnless(target.endswith("/operations/123"))
         d = self.shouldRedirect2("test_POST_DIRURL_deepcheck", _check_redirect,
                                  self.POST, self.public_url,
                                  t="start-deep-check", ophandle="123")
         d.addCallback(self.wait_for_operation, "123")
         def _check_json(data):
-            self.failUnlessEqual(data["finished"], True)
-            self.failUnlessEqual(data["count-objects-checked"], 8)
-            self.failUnlessEqual(data["count-objects-healthy"], 8)
+            self.failUnlessReallyEqual(data["finished"], True)
+            self.failUnlessReallyEqual(data["count-objects-checked"], 10)
+            self.failUnlessReallyEqual(data["count-objects-healthy"], 10)
         d.addCallback(_check_json)
         d.addCallback(self.get_operation_results, "123", "html")
         def _check_html(res):
-            self.failUnless("Objects Checked: <span>8</span>" in res)
-            self.failUnless("Objects Healthy: <span>8</span>" in res)
+            self.failUnless("Objects Checked: <span>10</span>" in res)
+            self.failUnless("Objects Healthy: <span>10</span>" in res)
         d.addCallback(_check_html)
 
         d.addCallback(lambda res:
@@ -1789,23 +2762,23 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
                       ophandle="124", output="json", followRedirect=True)
         d.addCallback(self.wait_for_operation, "124")
         def _check_json(data):
-            self.failUnlessEqual(data["finished"], True)
-            self.failUnlessEqual(data["count-objects-checked"], 8)
-            self.failUnlessEqual(data["count-objects-healthy-pre-repair"], 8)
-            self.failUnlessEqual(data["count-objects-unhealthy-pre-repair"], 0)
-            self.failUnlessEqual(data["count-corrupt-shares-pre-repair"], 0)
-            self.failUnlessEqual(data["count-repairs-attempted"], 0)
-            self.failUnlessEqual(data["count-repairs-successful"], 0)
-            self.failUnlessEqual(data["count-repairs-unsuccessful"], 0)
-            self.failUnlessEqual(data["count-objects-healthy-post-repair"], 8)
-            self.failUnlessEqual(data["count-objects-unhealthy-post-repair"], 0)
-            self.failUnlessEqual(data["count-corrupt-shares-post-repair"], 0)
+            self.failUnlessReallyEqual(data["finished"], True)
+            self.failUnlessReallyEqual(data["count-objects-checked"], 10)
+            self.failUnlessReallyEqual(data["count-objects-healthy-pre-repair"], 10)
+            self.failUnlessReallyEqual(data["count-objects-unhealthy-pre-repair"], 0)
+            self.failUnlessReallyEqual(data["count-corrupt-shares-pre-repair"], 0)
+            self.failUnlessReallyEqual(data["count-repairs-attempted"], 0)
+            self.failUnlessReallyEqual(data["count-repairs-successful"], 0)
+            self.failUnlessReallyEqual(data["count-repairs-unsuccessful"], 0)
+            self.failUnlessReallyEqual(data["count-objects-healthy-post-repair"], 10)
+            self.failUnlessReallyEqual(data["count-objects-unhealthy-post-repair"], 0)
+            self.failUnlessReallyEqual(data["count-corrupt-shares-post-repair"], 0)
         d.addCallback(_check_json)
         d.addCallback(self.get_operation_results, "124", "html")
         def _check_html(res):
-            self.failUnless("Objects Checked: <span>8</span>" in res)
+            self.failUnless("Objects Checked: <span>10</span>" in res)
 
-            self.failUnless("Objects Healthy (before repair): <span>8</span>" in res)
+            self.failUnless("Objects Healthy (before repair): <span>10</span>" in res)
             self.failUnless("Objects Unhealthy (before repair): <span>0</span>" in res)
             self.failUnless("Corrupt Shares (before repair): <span>0</span>" in res)
 
@@ -1813,7 +2786,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
             self.failUnless("Repairs Successful: <span>0</span>" in res)
             self.failUnless("Repairs Unsuccessful: <span>0</span>" in res)
 
-            self.failUnless("Objects Healthy (after repair): <span>8</span>" in res)
+            self.failUnless("Objects Healthy (after repair): <span>10</span>" in res)
             self.failUnless("Objects Unhealthy (after repair): <span>0</span>" in res)
             self.failUnless("Corrupt Shares (after repair): <span>0</span>" in res)
         d.addCallback(_check_html)
@@ -1832,6 +2805,110 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d.addCallback(self.failUnlessNodeKeysAre, [])
         return d
 
+    def test_POST_mkdir_mdmf(self):
+        d = self.POST(self.public_url + "/foo?t=mkdir&name=newdir&mutable-type=mdmf")
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(lambda node:
+            self.failUnlessEqual(node._node.get_version(), MDMF_VERSION))
+        return d
+
+    def test_POST_mkdir_sdmf(self):
+        d = self.POST(self.public_url + "/foo?t=mkdir&name=newdir&mutable-type=sdmf")
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(lambda node:
+            self.failUnlessEqual(node._node.get_version(), SDMF_VERSION))
+        return d
+
+    def test_POST_mkdir_bad_mutable_type(self):
+        return self.shouldHTTPError("test bad mutable type",
+                                    400, "Bad Request", "Unknown type: foo",
+                                    self.POST, self.public_url + \
+                                    "/foo?t=mkdir&name=newdir&mutable-type=foo")
+
+    def test_POST_mkdir_initial_children(self):
+        (newkids, caps) = self._create_initial_children()
+        d = self.POST2(self.public_url +
+                       "/foo?t=mkdir-with-children&name=newdir",
+                       simplejson.dumps(newkids))
+        d.addCallback(lambda res:
+                      self.failUnlessNodeHasChild(self._foo_node, u"newdir"))
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(self.failUnlessNodeKeysAre, newkids.keys())
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(self.failUnlessROChildURIIs, u"child-imm", caps['filecap1'])
+        return d
+
+    def test_POST_mkdir_initial_children_mdmf(self):
+        (newkids, caps) = self._create_initial_children()
+        d = self.POST2(self.public_url +
+                       "/foo?t=mkdir-with-children&name=newdir&mutable-type=mdmf",
+                       simplejson.dumps(newkids))
+        d.addCallback(lambda res:
+                      self.failUnlessNodeHasChild(self._foo_node, u"newdir"))
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(lambda node:
+            self.failUnlessEqual(node._node.get_version(), MDMF_VERSION))
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(self.failUnlessROChildURIIs, u"child-imm",
+                       caps['filecap1'])
+        return d
+
+    # XXX: Duplication.
+    def test_POST_mkdir_initial_children_sdmf(self):
+        (newkids, caps) = self._create_initial_children()
+        d = self.POST2(self.public_url +
+                       "/foo?t=mkdir-with-children&name=newdir&mutable-type=sdmf",
+                       simplejson.dumps(newkids))
+        d.addCallback(lambda res:
+                      self.failUnlessNodeHasChild(self._foo_node, u"newdir"))
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(lambda node:
+            self.failUnlessEqual(node._node.get_version(), SDMF_VERSION))
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(self.failUnlessROChildURIIs, u"child-imm",
+                       caps['filecap1'])
+        return d
+
+    def test_POST_mkdir_initial_children_bad_mutable_type(self):
+        (newkids, caps) = self._create_initial_children()
+        return self.shouldHTTPError("test bad mutable type",
+                                    400, "Bad Request", "Unknown type: foo",
+                                    self.POST, self.public_url + \
+                                    "/foo?t=mkdir-with-children&name=newdir&mutable-type=foo",
+                                    simplejson.dumps(newkids))
+
+    def test_POST_mkdir_immutable(self):
+        (newkids, caps) = self._create_immutable_children()
+        d = self.POST2(self.public_url +
+                       "/foo?t=mkdir-immutable&name=newdir",
+                       simplejson.dumps(newkids))
+        d.addCallback(lambda res:
+                      self.failUnlessNodeHasChild(self._foo_node, u"newdir"))
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(self.failUnlessNodeKeysAre, newkids.keys())
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(self.failUnlessROChildURIIs, u"child-imm", caps['filecap1'])
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(self.failUnlessROChildURIIs, u"unknownchild-imm", caps['unknown_immcap'])
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(self.failUnlessROChildURIIs, u"dirchild-imm", caps['immdircap'])
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(self.failUnlessROChildURIIs, u"dirchild-lit", caps['litdircap'])
+        d.addCallback(lambda res: self._foo_node.get(u"newdir"))
+        d.addCallback(self.failUnlessROChildURIIs, u"dirchild-empty", caps['emptydircap'])
+        return d
+
+    def test_POST_mkdir_immutable_bad(self):
+        (newkids, caps) = self._create_initial_children()
+        d = self.shouldFail2(error.Error, "test_POST_mkdir_immutable_bad",
+                             "400 Bad Request",
+                             "needed to be immutable but was not",
+                             self.POST2,
+                             self.public_url +
+                             "/foo?t=mkdir-immutable&name=newdir",
+                             simplejson.dumps(newkids))
+        return d
+
     def test_POST_mkdir_2(self):
         d = self.POST(self.public_url + "/foo/newdir?t=mkdir", "")
         d.addCallback(lambda res:
@@ -1849,11 +2926,43 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d.addCallback(self.failUnlessNodeKeysAre, [])
         return d
 
-    def test_POST_mkdir_no_parentdir_noredirect(self):
-        d = self.POST("/uri?t=mkdir")
+    def test_POST_mkdir_no_parentdir_noredirect(self):
+        d = self.POST("/uri?t=mkdir")
+        def _after_mkdir(res):
+            uri.DirectoryURI.init_from_string(res)
+        d.addCallback(_after_mkdir)
+        return d
+
+    def test_POST_mkdir_no_parentdir_noredirect_mdmf(self):
+        d = self.POST("/uri?t=mkdir&mutable-type=mdmf")
+        def _after_mkdir(res):
+            u = uri.from_string(res)
+            # Check that this is an MDMF writecap
+            self.failUnlessIsInstance(u, uri.MDMFDirectoryURI)
+        d.addCallback(_after_mkdir)
+        return d
+
+    def test_POST_mkdir_no_parentdir_noredirect_sdmf(self):
+        d = self.POST("/uri?t=mkdir&mutable-type=sdmf")
+        def _after_mkdir(res):
+            u = uri.from_string(res)
+            self.failUnlessIsInstance(u, uri.DirectoryURI)
+        d.addCallback(_after_mkdir)
+        return d
+
+    def test_POST_mkdir_no_parentdir_noredirect_bad_mutable_type(self):
+        return self.shouldHTTPError("test bad mutable type",
+                                    400, "Bad Request", "Unknown type: foo",
+                                    self.POST, self.public_url + \
+                                    "/uri?t=mkdir&mutable-type=foo")
+
+    def test_POST_mkdir_no_parentdir_noredirect2(self):
+        # make sure form-based arguments (as on the welcome page) still work
+        d = self.POST("/uri", t="mkdir")
         def _after_mkdir(res):
-            uri.NewDirectoryURI.init_from_string(res)
+            uri.DirectoryURI.init_from_string(res)
         d.addCallback(_after_mkdir)
+        d.addErrback(self.explain_web_error)
         return d
 
     def test_POST_mkdir_no_parentdir_redirect(self):
@@ -1865,6 +2974,127 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d.addCallback(_check_target)
         return d
 
+    def test_POST_mkdir_no_parentdir_redirect2(self):
+        d = self.POST("/uri", t="mkdir", redirect_to_result="true")
+        d.addBoth(self.shouldRedirect, None, statuscode='303')
+        def _check_target(target):
+            target = urllib.unquote(target)
+            self.failUnless(target.startswith("uri/URI:DIR2:"), target)
+        d.addCallback(_check_target)
+        d.addErrback(self.explain_web_error)
+        return d
+
+    def _make_readonly(self, u):
+        ro_uri = uri.from_string(u).get_readonly()
+        if ro_uri is None:
+            return None
+        return ro_uri.to_string()
+
+    def _create_initial_children(self):
+        contents, n, filecap1 = self.makefile(12)
+        md1 = {"metakey1": "metavalue1"}
+        filecap2 = make_mutable_file_uri()
+        node3 = self.s.create_node_from_uri(make_mutable_file_uri())
+        filecap3 = node3.get_readonly_uri()
+        node4 = self.s.create_node_from_uri(make_mutable_file_uri())
+        dircap = DirectoryNode(node4, None, None).get_uri()
+        mdmfcap = make_mutable_file_uri(mdmf=True)
+        litdircap = "URI:DIR2-LIT:ge3dumj2mewdcotyfqydulbshj5x2lbm"
+        emptydircap = "URI:DIR2-LIT:"
+        newkids = {u"child-imm":        ["filenode", {"rw_uri": filecap1,
+                                                      "ro_uri": self._make_readonly(filecap1),
+                                                      "metadata": md1, }],
+                   u"child-mutable":    ["filenode", {"rw_uri": filecap2,
+                                                      "ro_uri": self._make_readonly(filecap2)}],
+                   u"child-mutable-ro": ["filenode", {"ro_uri": filecap3}],
+                   u"unknownchild-rw":  ["unknown",  {"rw_uri": unknown_rwcap,
+                                                      "ro_uri": unknown_rocap}],
+                   u"unknownchild-ro":  ["unknown",  {"ro_uri": unknown_rocap}],
+                   u"unknownchild-imm": ["unknown",  {"ro_uri": unknown_immcap}],
+                   u"dirchild":         ["dirnode",  {"rw_uri": dircap,
+                                                      "ro_uri": self._make_readonly(dircap)}],
+                   u"dirchild-lit":     ["dirnode",  {"ro_uri": litdircap}],
+                   u"dirchild-empty":   ["dirnode",  {"ro_uri": emptydircap}],
+                   u"child-mutable-mdmf": ["filenode", {"rw_uri": mdmfcap,
+                                                        "ro_uri": self._make_readonly(mdmfcap)}],
+                   }
+        return newkids, {'filecap1': filecap1,
+                         'filecap2': filecap2,
+                         'filecap3': filecap3,
+                         'unknown_rwcap': unknown_rwcap,
+                         'unknown_rocap': unknown_rocap,
+                         'unknown_immcap': unknown_immcap,
+                         'dircap': dircap,
+                         'litdircap': litdircap,
+                         'emptydircap': emptydircap,
+                         'mdmfcap': mdmfcap}
+
+    def _create_immutable_children(self):
+        contents, n, filecap1 = self.makefile(12)
+        md1 = {"metakey1": "metavalue1"}
+        tnode = create_chk_filenode("immutable directory contents\n"*10)
+        dnode = DirectoryNode(tnode, None, None)
+        assert not dnode.is_mutable()
+        immdircap = dnode.get_uri()
+        litdircap = "URI:DIR2-LIT:ge3dumj2mewdcotyfqydulbshj5x2lbm"
+        emptydircap = "URI:DIR2-LIT:"
+        newkids = {u"child-imm":        ["filenode", {"ro_uri": filecap1,
+                                                      "metadata": md1, }],
+                   u"unknownchild-imm": ["unknown",  {"ro_uri": unknown_immcap}],
+                   u"dirchild-imm":     ["dirnode",  {"ro_uri": immdircap}],
+                   u"dirchild-lit":     ["dirnode",  {"ro_uri": litdircap}],
+                   u"dirchild-empty":   ["dirnode",  {"ro_uri": emptydircap}],
+                   }
+        return newkids, {'filecap1': filecap1,
+                         'unknown_immcap': unknown_immcap,
+                         'immdircap': immdircap,
+                         'litdircap': litdircap,
+                         'emptydircap': emptydircap}
+
+    def test_POST_mkdir_no_parentdir_initial_children(self):
+        (newkids, caps) = self._create_initial_children()
+        d = self.POST2("/uri?t=mkdir-with-children", simplejson.dumps(newkids))
+        def _after_mkdir(res):
+            self.failUnless(res.startswith("URI:DIR"), res)
+            n = self.s.create_node_from_uri(res)
+            d2 = self.failUnlessNodeKeysAre(n, newkids.keys())
+            d2.addCallback(lambda ign:
+                           self.failUnlessROChildURIIs(n, u"child-imm",
+                                                       caps['filecap1']))
+            d2.addCallback(lambda ign:
+                           self.failUnlessRWChildURIIs(n, u"child-mutable",
+                                                       caps['filecap2']))
+            d2.addCallback(lambda ign:
+                           self.failUnlessROChildURIIs(n, u"child-mutable-ro",
+                                                       caps['filecap3']))
+            d2.addCallback(lambda ign:
+                           self.failUnlessRWChildURIIs(n, u"unknownchild-rw",
+                                                       caps['unknown_rwcap']))
+            d2.addCallback(lambda ign:
+                           self.failUnlessROChildURIIs(n, u"unknownchild-ro",
+                                                       caps['unknown_rocap']))
+            d2.addCallback(lambda ign:
+                           self.failUnlessROChildURIIs(n, u"unknownchild-imm",
+                                                       caps['unknown_immcap']))
+            d2.addCallback(lambda ign:
+                           self.failUnlessRWChildURIIs(n, u"dirchild",
+                                                       caps['dircap']))
+            return d2
+        d.addCallback(_after_mkdir)
+        return d
+
+    def test_POST_mkdir_no_parentdir_unexpected_children(self):
+        # the regular /uri?t=mkdir operation is specified to ignore its body.
+        # Only t=mkdir-with-children pays attention to it.
+        (newkids, caps) = self._create_initial_children()
+        d = self.shouldHTTPError("POST t=mkdir unexpected children",
+                                 400, "Bad Request",
+                                 "t=mkdir does not accept children=, "
+                                 "try t=mkdir-with-children instead",
+                                 self.POST2, "/uri?t=mkdir", # without children
+                                 simplejson.dumps(newkids))
+        return d
+
     def test_POST_noparent_bad(self):
         d = self.shouldHTTPError("POST /uri?t=bogus", 400, "Bad Request",
                                  "/uri accepts only PUT, PUT?t=mkdir, "
@@ -1872,11 +3102,53 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
                                  self.POST, "/uri?t=bogus")
         return d
 
+    def test_POST_mkdir_no_parentdir_immutable(self):
+        (newkids, caps) = self._create_immutable_children()
+        d = self.POST2("/uri?t=mkdir-immutable", simplejson.dumps(newkids))
+        def _after_mkdir(res):
+            self.failUnless(res.startswith("URI:DIR"), res)
+            n = self.s.create_node_from_uri(res)
+            d2 = self.failUnlessNodeKeysAre(n, newkids.keys())
+            d2.addCallback(lambda ign:
+                           self.failUnlessROChildURIIs(n, u"child-imm",
+                                                          caps['filecap1']))
+            d2.addCallback(lambda ign:
+                           self.failUnlessROChildURIIs(n, u"unknownchild-imm",
+                                                          caps['unknown_immcap']))
+            d2.addCallback(lambda ign:
+                           self.failUnlessROChildURIIs(n, u"dirchild-imm",
+                                                          caps['immdircap']))
+            d2.addCallback(lambda ign:
+                           self.failUnlessROChildURIIs(n, u"dirchild-lit",
+                                                          caps['litdircap']))
+            d2.addCallback(lambda ign:
+                           self.failUnlessROChildURIIs(n, u"dirchild-empty",
+                                                          caps['emptydircap']))
+            return d2
+        d.addCallback(_after_mkdir)
+        return d
+
+    def test_POST_mkdir_no_parentdir_immutable_bad(self):
+        (newkids, caps) = self._create_initial_children()
+        d = self.shouldFail2(error.Error,
+                             "test_POST_mkdir_no_parentdir_immutable_bad",
+                             "400 Bad Request",
+                             "needed to be immutable but was not",
+                             self.POST2,
+                             "/uri?t=mkdir-immutable",
+                             simplejson.dumps(newkids))
+        return d
+
     def test_welcome_page_mkdir_button(self):
         # Fetch the welcome page.
         d = self.GET("/")
         def _after_get_welcome_page(res):
-            MKDIR_BUTTON_RE=re.compile('<form action="([^"]*)" method="post".*<input type="hidden" name="t" value="([^"]*)" /><input type="hidden" name="([^"]*)" value="([^"]*)" /><input type="submit" value="Create Directory!" />', re.I)
+            MKDIR_BUTTON_RE = re.compile(
+                '<form action="([^"]*)" method="post".*?'
+                '<input type="hidden" name="t" value="([^"]*)" />'
+                '<input type="hidden" name="([^"]*)" value="([^"]*)" />'
+                '<input type="submit" value="Create a directory" />',
+                re.I)
             mo = MKDIR_BUTTON_RE.search(res)
             formaction = mo.group(1)
             formt = mo.group(2)
@@ -1941,7 +3213,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
                              self.POST, self.public_url + "/foo", t="BOGUS")
         return d
 
-    def test_POST_set_children(self):
+    def test_POST_set_children(self, command_name="set_children"):
         contents9, n9, newuri9 = self.makefile(9)
         contents10, n10, newuri10 = self.makefile(10)
         contents11, n11, newuri11 = self.makefile(11)
@@ -1970,42 +3242,63 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
                                                } ]
                     }""" % (newuri9, newuri10, newuri11)
 
-        url = self.webish_url + self.public_url + "/foo" + "?t=set_children"
+        url = self.webish_url + self.public_url + "/foo" + "?t=" + command_name
 
         d = client.getPage(url, method="POST", postdata=reqbody)
         def _then(res):
-            self.failUnlessURIMatchesChild(newuri9, self._foo_node, u"atomic_added_1")
-            self.failUnlessURIMatchesChild(newuri10, self._foo_node, u"atomic_added_2")
-            self.failUnlessURIMatchesChild(newuri11, self._foo_node, u"atomic_added_3")
+            self.failUnlessURIMatchesROChild(newuri9, self._foo_node, u"atomic_added_1")
+            self.failUnlessURIMatchesROChild(newuri10, self._foo_node, u"atomic_added_2")
+            self.failUnlessURIMatchesROChild(newuri11, self._foo_node, u"atomic_added_3")
 
         d.addCallback(_then)
         d.addErrback(self.dump_error)
         return d
 
-    def test_POST_put_uri(self):
+    def test_POST_set_children_with_hyphen(self):
+        return self.test_POST_set_children(command_name="set-children")
+
+    def test_POST_link_uri(self):
         contents, n, newuri = self.makefile(8)
         d = self.POST(self.public_url + "/foo", t="uri", name="new.txt", uri=newuri)
-        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"new.txt")
+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"new.txt")
         d.addCallback(lambda res:
                       self.failUnlessChildContentsAre(self._foo_node, u"new.txt",
                                                       contents))
         return d
 
-    def test_POST_put_uri_replace(self):
+    def test_POST_link_uri_replace(self):
         contents, n, newuri = self.makefile(8)
         d = self.POST(self.public_url + "/foo", t="uri", name="bar.txt", uri=newuri)
-        d.addCallback(self.failUnlessURIMatchesChild, self._foo_node, u"bar.txt")
+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"bar.txt")
         d.addCallback(lambda res:
                       self.failUnlessChildContentsAre(self._foo_node, u"bar.txt",
                                                       contents))
         return d
 
-    def test_POST_put_uri_no_replace_queryarg(self):
+    def test_POST_link_uri_unknown_bad(self):
+        d = self.POST(self.public_url + "/foo", t="uri", name="future.txt", uri=unknown_rwcap)
+        d.addBoth(self.shouldFail, error.Error,
+                  "POST_link_uri_unknown_bad",
+                  "400 Bad Request",
+                  "unknown cap in a write slot")
+        return d
+
+    def test_POST_link_uri_unknown_ro_good(self):
+        d = self.POST(self.public_url + "/foo", t="uri", name="future-ro.txt", uri=unknown_rocap)
+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"future-ro.txt")
+        return d
+
+    def test_POST_link_uri_unknown_imm_good(self):
+        d = self.POST(self.public_url + "/foo", t="uri", name="future-imm.txt", uri=unknown_immcap)
+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node, u"future-imm.txt")
+        return d
+
+    def test_POST_link_uri_no_replace_queryarg(self):
         contents, n, newuri = self.makefile(8)
         d = self.POST(self.public_url + "/foo?replace=false", t="uri",
                       name="bar.txt", uri=newuri)
         d.addBoth(self.shouldFail, error.Error,
-                  "POST_put_uri_no_replace_queryarg",
+                  "POST_link_uri_no_replace_queryarg",
                   "409 Conflict",
                   "There was already a child by that name, and you asked me "
                   "to not replace it")
@@ -2013,12 +3306,12 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d.addCallback(self.failUnlessIsBarDotTxt)
         return d
 
-    def test_POST_put_uri_no_replace_field(self):
+    def test_POST_link_uri_no_replace_field(self):
         contents, n, newuri = self.makefile(8)
         d = self.POST(self.public_url + "/foo", t="uri", replace="false",
                       name="bar.txt", uri=newuri)
         d.addBoth(self.shouldFail, error.Error,
-                  "POST_put_uri_no_replace_field",
+                  "POST_link_uri_no_replace_field",
                   "409 Conflict",
                   "There was already a child by that name, and you asked me "
                   "to not replace it")
@@ -2026,14 +3319,21 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d.addCallback(self.failUnlessIsBarDotTxt)
         return d
 
-    def test_POST_delete(self):
-        d = self.POST(self.public_url + "/foo", t="delete", name="bar.txt")
+    def test_POST_delete(self, command_name='delete'):
+        d = self._foo_node.list()
+        def _check_before(children):
+            self.failUnless(u"bar.txt" in children)
+        d.addCallback(_check_before)
+        d.addCallback(lambda res: self.POST(self.public_url + "/foo", t=command_name, name="bar.txt"))
         d.addCallback(lambda res: self._foo_node.list())
-        def _check(children):
+        def _check_after(children):
             self.failIf(u"bar.txt" in children)
-        d.addCallback(_check)
+        d.addCallback(_check_after)
         return d
 
+    def test_POST_unlink(self):
+        return self.test_POST_delete(command_name='unlink')
+
     def test_POST_rename_file(self):
         d = self.POST(self.public_url + "/foo", t="rename",
                       from_name="bar.txt", to_name='wibble.txt')
@@ -2101,7 +3401,7 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
     def failUnlessIsEmptyJSON(self, res):
         data = simplejson.loads(res)
         self.failUnlessEqual(data[0], "dirnode", data)
-        self.failUnlessEqual(len(data[1]["children"]), 0)
+        self.failUnlessReallyEqual(len(data[1]["children"]), 0)
 
     def test_POST_rename_file_slash_fail(self):
         d = self.POST(self.public_url + "/foo", t="rename",
@@ -2136,14 +3436,14 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
                       " actual page: %s" % (which, to_where, res))
         res.trap(error.PageRedirect)
         if statuscode is not None:
-            self.failUnlessEqual(res.value.status, statuscode,
-                                 "%s: not a redirect" % which)
+            self.failUnlessReallyEqual(res.value.status, statuscode,
+                                       "%s: not a redirect" % which)
         if target is not None:
             # the PageRedirect does not seem to capture the uri= query arg
             # properly, so we can't check for it.
             realtarget = self.webish_url + target
-            self.failUnlessEqual(res.value.location, realtarget,
-                                 "%s: wrong target" % which)
+            self.failUnlessReallyEqual(res.value.location, realtarget,
+                                       "%s: wrong target" % which)
         return res.value.location
 
     def test_GET_URI_form(self):
@@ -2215,23 +3515,23 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         return d
 
     def test_PUT_DIRURL_uri(self):
-        d = self.s.create_empty_dirnode()
+        d = self.s.create_dirnode()
         def _made_dir(dn):
             new_uri = dn.get_uri()
             # replace /foo with a new (empty) directory
             d = self.PUT(self.public_url + "/foo?t=uri", new_uri)
             d.addCallback(lambda res:
-                          self.failUnlessEqual(res.strip(), new_uri))
+                          self.failUnlessReallyEqual(res.strip(), new_uri))
             d.addCallback(lambda res:
-                          self.failUnlessChildURIIs(self.public_root,
-                                                    u"foo",
-                                                    new_uri))
+                          self.failUnlessRWChildURIIs(self.public_root,
+                                                      u"foo",
+                                                      new_uri))
             return d
         d.addCallback(_made_dir)
         return d
 
     def test_PUT_DIRURL_uri_noreplace(self):
-        d = self.s.create_empty_dirnode()
+        d = self.s.create_dirnode()
         def _made_dir(dn):
             new_uri = dn.get_uri()
             # replace /foo with a new (empty) directory, but ask that
@@ -2242,9 +3542,9 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
                                  self.public_url + "/foo?t=uri&replace=false",
                                  new_uri)
             d.addCallback(lambda res:
-                          self.failUnlessChildURIIs(self.public_root,
-                                                    u"foo",
-                                                    self._foo_uri))
+                          self.failUnlessRWChildURIIs(self.public_root,
+                                                      u"foo",
+                                                      self._foo_uri))
             return d
         d.addCallback(_made_dir)
         return d
@@ -2254,24 +3554,64 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
                                  "400 Bad Request", "PUT to a directory",
                                  self.PUT, self.public_url + "/foo?t=BOGUS", "")
         d.addCallback(lambda res:
-                      self.failUnlessChildURIIs(self.public_root,
-                                                u"foo",
-                                                self._foo_uri))
+                      self.failUnlessRWChildURIIs(self.public_root,
+                                                  u"foo",
+                                                  self._foo_uri))
         return d
 
     def test_PUT_NEWFILEURL_uri(self):
         contents, n, new_uri = self.makefile(8)
         d = self.PUT(self.public_url + "/foo/new.txt?t=uri", new_uri)
-        d.addCallback(lambda res: self.failUnlessEqual(res.strip(), new_uri))
+        d.addCallback(lambda res: self.failUnlessReallyEqual(res.strip(), new_uri))
         d.addCallback(lambda res:
                       self.failUnlessChildContentsAre(self._foo_node, u"new.txt",
                                                       contents))
         return d
 
+    def test_PUT_NEWFILEURL_mdmf(self):
+        new_contents = self.NEWFILE_CONTENTS * 300000
+        d = self.PUT(self.public_url + \
+                     "/foo/mdmf.txt?mutable=true&mutable-type=mdmf",
+                     new_contents)
+        d.addCallback(lambda ignored:
+            self.GET(self.public_url + "/foo/mdmf.txt?t=json"))
+        def _got_json(json):
+            data = simplejson.loads(json)
+            data = data[1]
+            self.failUnlessIn("mutable-type", data)
+            self.failUnlessEqual(data['mutable-type'], "mdmf")
+            self.failUnless(data['rw_uri'].startswith("URI:MDMF"))
+            self.failUnless(data['ro_uri'].startswith("URI:MDMF"))
+        d.addCallback(_got_json)
+        return d
+
+    def test_PUT_NEWFILEURL_sdmf(self):
+        new_contents = self.NEWFILE_CONTENTS * 300000
+        d = self.PUT(self.public_url + \
+                     "/foo/sdmf.txt?mutable=true&mutable-type=sdmf",
+                     new_contents)
+        d.addCallback(lambda ignored:
+            self.GET(self.public_url + "/foo/sdmf.txt?t=json"))
+        def _got_json(json):
+            data = simplejson.loads(json)
+            data = data[1]
+            self.failUnlessIn("mutable-type", data)
+            self.failUnlessEqual(data['mutable-type'], "sdmf")
+        d.addCallback(_got_json)
+        return d
+
+    def test_PUT_NEWFILEURL_bad_mutable_type(self):
+       new_contents = self.NEWFILE_CONTENTS * 300000
+       return self.shouldHTTPError("test bad mutable type",
+                                   400, "Bad Request", "Unknown type: foo",
+                                   self.PUT, self.public_url + \
+                                   "/foo/foo.txt?mutable=true&mutable-type=foo",
+                                   new_contents)
+
     def test_PUT_NEWFILEURL_uri_replace(self):
         contents, n, new_uri = self.makefile(8)
         d = self.PUT(self.public_url + "/foo/bar.txt?t=uri", new_uri)
-        d.addCallback(lambda res: self.failUnlessEqual(res.strip(), new_uri))
+        d.addCallback(lambda res: self.failUnlessReallyEqual(res.strip(), new_uri))
         d.addCallback(lambda res:
                       self.failUnlessChildContentsAre(self._foo_node, u"bar.txt",
                                                       contents))
@@ -2286,18 +3626,53 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
                   "to not replace it")
         return d
 
+    def test_PUT_NEWFILEURL_uri_unknown_bad(self):
+        d = self.PUT(self.public_url + "/foo/put-future.txt?t=uri", unknown_rwcap)
+        d.addBoth(self.shouldFail, error.Error,
+                  "POST_put_uri_unknown_bad",
+                  "400 Bad Request",
+                  "unknown cap in a write slot")
+        return d
+
+    def test_PUT_NEWFILEURL_uri_unknown_ro_good(self):
+        d = self.PUT(self.public_url + "/foo/put-future-ro.txt?t=uri", unknown_rocap)
+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node,
+                      u"put-future-ro.txt")
+        return d
+
+    def test_PUT_NEWFILEURL_uri_unknown_imm_good(self):
+        d = self.PUT(self.public_url + "/foo/put-future-imm.txt?t=uri", unknown_immcap)
+        d.addCallback(self.failUnlessURIMatchesROChild, self._foo_node,
+                      u"put-future-imm.txt")
+        return d
+
     def test_PUT_NEWFILE_URI(self):
         file_contents = "New file contents here\n"
         d = self.PUT("/uri", file_contents)
         def _check(uri):
             assert isinstance(uri, str), uri
             self.failUnless(uri in FakeCHKFileNode.all_contents)
-            self.failUnlessEqual(FakeCHKFileNode.all_contents[uri],
-                                 file_contents)
+            self.failUnlessReallyEqual(FakeCHKFileNode.all_contents[uri],
+                                       file_contents)
             return self.GET("/uri/%s" % uri)
         d.addCallback(_check)
         def _check2(res):
-            self.failUnlessEqual(res, file_contents)
+            self.failUnlessReallyEqual(res, file_contents)
+        d.addCallback(_check2)
+        return d
+
+    def test_PUT_NEWFILE_URI_not_mutable(self):
+        file_contents = "New file contents here\n"
+        d = self.PUT("/uri?mutable=false", file_contents)
+        def _check(uri):
+            assert isinstance(uri, str), uri
+            self.failUnless(uri in FakeCHKFileNode.all_contents)
+            self.failUnlessReallyEqual(FakeCHKFileNode.all_contents[uri],
+                                       file_contents)
+            return self.GET("/uri/%s" % uri)
+        d.addCallback(_check)
+        def _check2(res):
+            self.failUnlessReallyEqual(res, file_contents)
         d.addCallback(_check2)
         return d
 
@@ -2312,28 +3687,22 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
     def test_PUT_NEWFILE_URI_mutable(self):
         file_contents = "New file contents here\n"
         d = self.PUT("/uri?mutable=true", file_contents)
-        def _check_mutable(uri):
-            uri = uri.strip()
-            u = IURI(uri)
-            self.failUnless(IMutableFileURI.providedBy(u))
-            self.failUnless(u.storage_index in FakeMutableFileNode.all_contents)
-            n = self.s.create_node_from_uri(uri)
+        def _check1(filecap):
+            filecap = filecap.strip()
+            self.failUnless(filecap.startswith("URI:SSK:"), filecap)
+            self.filecap = filecap
+            u = uri.WriteableSSKFileURI.init_from_string(filecap)
+            self.failUnless(u.get_storage_index() in FakeMutableFileNode.all_contents)
+            n = self.s.create_node_from_uri(filecap)
             return n.download_best_version()
-        d.addCallback(_check_mutable)
-        def _check2_mutable(data):
-            self.failUnlessEqual(data, file_contents)
-        d.addCallback(_check2_mutable)
-        return d
-
-        def _check(uri):
-            self.failUnless(uri.to_string() in FakeCHKFileNode.all_contents)
-            self.failUnlessEqual(FakeCHKFileNode.all_contents[uri.to_string()],
-                                 file_contents)
-            return self.GET("/uri/%s" % uri)
-        d.addCallback(_check)
-        def _check2(res):
-            self.failUnlessEqual(res, file_contents)
+        d.addCallback(_check1)
+        def _check2(data):
+            self.failUnlessReallyEqual(data, file_contents)
+            return self.GET("/uri/%s" % urllib.quote(self.filecap))
         d.addCallback(_check2)
+        def _check3(res):
+            self.failUnlessReallyEqual(res, file_contents)
+        d.addCallback(_check3)
         return d
 
     def test_PUT_mkdir(self):
@@ -2348,6 +3717,29 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d.addCallback(self.failUnlessIsEmptyJSON)
         return d
 
+    def test_PUT_mkdir_mdmf(self):
+        d = self.PUT("/uri?t=mkdir&mutable-type=mdmf", "")
+        def _got(res):
+            u = uri.from_string(res)
+            # Check that this is an MDMF writecap
+            self.failUnlessIsInstance(u, uri.MDMFDirectoryURI)
+        d.addCallback(_got)
+        return d
+
+    def test_PUT_mkdir_sdmf(self):
+        d = self.PUT("/uri?t=mkdir&mutable-type=sdmf", "")
+        def _got(res):
+            u = uri.from_string(res)
+            self.failUnlessIsInstance(u, uri.DirectoryURI)
+        d.addCallback(_got)
+        return d
+
+    def test_PUT_mkdir_bad_mutable_type(self):
+        return self.shouldHTTPError("bad mutable type",
+                                    400, "Bad Request", "Unknown type: foo",
+                                    self.PUT, "/uri?t=mkdir&mutable-type=foo",
+                                    "")
+
     def test_POST_check(self):
         d = self.POST(self.public_url + "/foo", t="check", name="bar.txt")
         def _done(res):
@@ -2360,6 +3752,75 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
         d.addCallback(_done)
         return d
 
+
+    def test_PUT_update_at_offset(self):
+        file_contents = "test file" * 100000 # about 900 KiB
+        d = self.PUT("/uri?mutable=true", file_contents)
+        def _then(filecap):
+            self.filecap = filecap
+            new_data = file_contents[:100]
+            new = "replaced and so on"
+            new_data += new
+            new_data += file_contents[len(new_data):]
+            assert len(new_data) == len(file_contents)
+            self.new_data = new_data
+        d.addCallback(_then)
+        d.addCallback(lambda ignored:
+            self.PUT("/uri/%s?replace=True&offset=100" % self.filecap,
+                     "replaced and so on"))
+        def _get_data(filecap):
+            n = self.s.create_node_from_uri(filecap)
+            return n.download_best_version()
+        d.addCallback(_get_data)
+        d.addCallback(lambda results:
+            self.failUnlessEqual(results, self.new_data))
+        # Now try appending things to the file
+        d.addCallback(lambda ignored:
+            self.PUT("/uri/%s?offset=%d" % (self.filecap, len(self.new_data)),
+                     "puppies" * 100))
+        d.addCallback(_get_data)
+        d.addCallback(lambda results:
+            self.failUnlessEqual(results, self.new_data + ("puppies" * 100)))
+        # and try replacing the beginning of the file
+        d.addCallback(lambda ignored:
+            self.PUT("/uri/%s?offset=0" % self.filecap, "begin"))
+        d.addCallback(_get_data)
+        d.addCallback(lambda results:
+            self.failUnlessEqual(results, "begin"+self.new_data[len("begin"):]+("puppies"*100)))
+        return d
+
+    def test_PUT_update_at_invalid_offset(self):
+        file_contents = "test file" * 100000 # about 900 KiB
+        d = self.PUT("/uri?mutable=true", file_contents)
+        def _then(filecap):
+            self.filecap = filecap
+        d.addCallback(_then)
+        # Negative offsets should cause an error.
+        d.addCallback(lambda ignored:
+            self.shouldHTTPError("test mutable invalid offset negative",
+                                 400, "Bad Request",
+                                 "Invalid offset",
+                                 self.PUT,
+                                 "/uri/%s?offset=-1" % self.filecap,
+                                 "foo"))
+        return d
+
+    def test_PUT_update_at_offset_immutable(self):
+        file_contents = "Test file" * 100000
+        d = self.PUT("/uri", file_contents)
+        def _then(filecap):
+            self.filecap = filecap
+        d.addCallback(_then)
+        d.addCallback(lambda ignored:
+            self.shouldHTTPError("test immutable update",
+                                 400, "Bad Request",
+                                 "immutable",
+                                 self.PUT,
+                                 "/uri/%s?offset=50" % self.filecap,
+                                 "foo"))
+        return d
+
+
     def test_bad_method(self):
         url = self.webish_url + self.public_url + "/foo/bar.txt"
         d = self.shouldHTTPError("test_bad_method",
@@ -2418,7 +3879,8 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
             self.failUnless("finished" in data, res)
         d.addCallback(_check1)
         # the retain-for=0 will cause the handle to be expired very soon
-        d.addCallback(self.stall, 2.0)
+        d.addCallback(lambda ign:
+            self.clock.advance(2.0))
         d.addCallback(lambda ignored:
                       self.shouldHTTPError("test_ophandle_retainfor",
                                            404, "404 Not Found",
@@ -2442,6 +3904,82 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
                                            "/operations/130?t=status&output=JSON"))
         return d
 
+    def test_uncollected_ophandle_expiration(self):
+        # uncollected ophandles should expire after 4 days
+        def _make_uncollected_ophandle(ophandle):
+            d = self.POST(self.public_url +
+                          "/foo/?t=start-manifest&ophandle=%d" % ophandle,
+                          followRedirect=False)
+            # When we start the operation, the webapi server will want
+            # to redirect us to the page for the ophandle, so we get
+            # confirmation that the operation has started. If the
+            # manifest operation has finished by the time we get there,
+            # following that redirect (by setting followRedirect=True
+            # above) has the side effect of collecting the ophandle that
+            # we've just created, which means that we can't use the
+            # ophandle to test the uncollected timeout anymore. So,
+            # instead, catch the 302 here and don't follow it.
+            d.addBoth(self.should302, "uncollected_ophandle_creation")
+            return d
+        # Create an ophandle, don't collect it, then advance the clock by
+        # 4 days - 1 second and make sure that the ophandle is still there.
+        d = _make_uncollected_ophandle(131)
+        d.addCallback(lambda ign:
+            self.clock.advance((96*60*60) - 1)) # 96 hours = 4 days
+        d.addCallback(lambda ign:
+            self.GET("/operations/131?t=status&output=JSON"))
+        def _check1(res):
+            data = simplejson.loads(res)
+            self.failUnless("finished" in data, res)
+        d.addCallback(_check1)
+        # Create an ophandle, don't collect it, then try to collect it
+        # after 4 days. It should be gone.
+        d.addCallback(lambda ign:
+            _make_uncollected_ophandle(132))
+        d.addCallback(lambda ign:
+            self.clock.advance(96*60*60))
+        d.addCallback(lambda ign:
+            self.shouldHTTPError("test_uncollected_ophandle_expired_after_100_hours",
+                                 404, "404 Not Found",
+                                 "unknown/expired handle '132'",
+                                 self.GET,
+                                 "/operations/132?t=status&output=JSON"))
+        return d
+
+    def test_collected_ophandle_expiration(self):
+        # collected ophandles should expire after 1 day
+        def _make_collected_ophandle(ophandle):
+            d = self.POST(self.public_url +
+                          "/foo/?t=start-manifest&ophandle=%d" % ophandle,
+                          followRedirect=True)
+            # By following the initial redirect, we collect the ophandle
+            # we've just created.
+            return d
+        # Create a collected ophandle, then collect it after 23 hours
+        # and 59 seconds to make sure that it is still there.
+        d = _make_collected_ophandle(133)
+        d.addCallback(lambda ign:
+            self.clock.advance((24*60*60) - 1))
+        d.addCallback(lambda ign:
+            self.GET("/operations/133?t=status&output=JSON"))
+        def _check1(res):
+            data = simplejson.loads(res)
+            self.failUnless("finished" in data, res)
+        d.addCallback(_check1)
+        # Create another uncollected ophandle, then try to collect it
+        # after 24 hours to make sure that it is gone.
+        d.addCallback(lambda ign:
+            _make_collected_ophandle(134))
+        d.addCallback(lambda ign:
+            self.clock.advance(24*60*60))
+        d.addCallback(lambda ign:
+            self.shouldHTTPError("test_collected_ophandle_expired_after_1000_minutes",
+                                 404, "404 Not Found",
+                                 "unknown/expired handle '134'",
+                                 self.GET,
+                                 "/operations/134?t=status&output=JSON"))
+        return d
+
     def test_incident(self):
         d = self.POST("/report_incident", details="eek")
         def _done(res):
@@ -2458,46 +3996,75 @@ class Web(WebMixin, WebErrorMixin, testutil.StallMixin, unittest.TestCase):
 
         d = self.GET("/static/subdir/hello.txt")
         def _check(res):
-            self.failUnlessEqual(res, "hello")
+            self.failUnlessReallyEqual(res, "hello")
         d.addCallback(_check)
         return d
 
 
-class Util(unittest.TestCase):
+class Util(ShouldFailMixin, testutil.ReallyEqualMixin, unittest.TestCase):
+    def test_load_file(self):
+        # This will raise an exception unless a well-formed XML file is found under that name.
+        common.getxmlfile('directory.xhtml').load()
+
+    def test_parse_replace_arg(self):
+        self.failUnlessReallyEqual(common.parse_replace_arg("true"), True)
+        self.failUnlessReallyEqual(common.parse_replace_arg("false"), False)
+        self.failUnlessReallyEqual(common.parse_replace_arg("only-files"),
+                                   "only-files")
+        self.shouldFail(AssertionError, "test_parse_replace_arg", "",
+                        common.parse_replace_arg, "only_fles")
+
     def test_abbreviate_time(self):
-        self.failUnlessEqual(common.abbreviate_time(None), "")
-        self.failUnlessEqual(common.abbreviate_time(1.234), "1.23s")
-        self.failUnlessEqual(common.abbreviate_time(0.123), "123ms")
-        self.failUnlessEqual(common.abbreviate_time(0.00123), "1.2ms")
-        self.failUnlessEqual(common.abbreviate_time(0.000123), "123us")
+        self.failUnlessReallyEqual(common.abbreviate_time(None), "")
+        self.failUnlessReallyEqual(common.abbreviate_time(1.234), "1.23s")
+        self.failUnlessReallyEqual(common.abbreviate_time(0.123), "123ms")
+        self.failUnlessReallyEqual(common.abbreviate_time(0.00123), "1.2ms")
+        self.failUnlessReallyEqual(common.abbreviate_time(0.000123), "123us")
+        self.failUnlessReallyEqual(common.abbreviate_time(-123000), "-123000000000us")
+
+    def test_compute_rate(self):
+        self.failUnlessReallyEqual(common.compute_rate(None, None), None)
+        self.failUnlessReallyEqual(common.compute_rate(None, 1), None)
+        self.failUnlessReallyEqual(common.compute_rate(250000, None), None)
+        self.failUnlessReallyEqual(common.compute_rate(250000, 0), None)
+        self.failUnlessReallyEqual(common.compute_rate(250000, 10), 25000.0)
+        self.failUnlessReallyEqual(common.compute_rate(0, 10), 0.0)
+        self.shouldFail(AssertionError, "test_compute_rate", "",
+                        common.compute_rate, -100, 10)
+        self.shouldFail(AssertionError, "test_compute_rate", "",
+                        common.compute_rate, 100, -10)
+
+        # Sanity check
+        rate = common.compute_rate(10*1000*1000, 1)
+        self.failUnlessReallyEqual(common.abbreviate_rate(rate), "10.00MBps")
 
     def test_abbreviate_rate(self):
-        self.failUnlessEqual(common.abbreviate_rate(None), "")
-        self.failUnlessEqual(common.abbreviate_rate(1234000), "1.23MBps")
-        self.failUnlessEqual(common.abbreviate_rate(12340), "12.3kBps")
-        self.failUnlessEqual(common.abbreviate_rate(123), "123Bps")
+        self.failUnlessReallyEqual(common.abbreviate_rate(None), "")
+        self.failUnlessReallyEqual(common.abbreviate_rate(1234000), "1.23MBps")
+        self.failUnlessReallyEqual(common.abbreviate_rate(12340), "12.3kBps")
+        self.failUnlessReallyEqual(common.abbreviate_rate(123), "123Bps")
 
     def test_abbreviate_size(self):
-        self.failUnlessEqual(common.abbreviate_size(None), "")
-        self.failUnlessEqual(common.abbreviate_size(1.23*1000*1000*1000), "1.23GB")
-        self.failUnlessEqual(common.abbreviate_size(1.23*1000*1000), "1.23MB")
-        self.failUnlessEqual(common.abbreviate_size(1230), "1.2kB")
-        self.failUnlessEqual(common.abbreviate_size(123), "123B")
+        self.failUnlessReallyEqual(common.abbreviate_size(None), "")
+        self.failUnlessReallyEqual(common.abbreviate_size(1.23*1000*1000*1000), "1.23GB")
+        self.failUnlessReallyEqual(common.abbreviate_size(1.23*1000*1000), "1.23MB")
+        self.failUnlessReallyEqual(common.abbreviate_size(1230), "1.2kB")
+        self.failUnlessReallyEqual(common.abbreviate_size(123), "123B")
 
     def test_plural(self):
         def convert(s):
             return "%d second%s" % (s, status.plural(s))
-        self.failUnlessEqual(convert(0), "0 seconds")
-        self.failUnlessEqual(convert(1), "1 second")
-        self.failUnlessEqual(convert(2), "2 seconds")
+        self.failUnlessReallyEqual(convert(0), "0 seconds")
+        self.failUnlessReallyEqual(convert(1), "1 second")
+        self.failUnlessReallyEqual(convert(2), "2 seconds")
         def convert2(s):
             return "has share%s: %s" % (status.plural(s), ",".join(s))
-        self.failUnlessEqual(convert2([]), "has shares: ")
-        self.failUnlessEqual(convert2(["1"]), "has share: 1")
-        self.failUnlessEqual(convert2(["1","2"]), "has shares: 1,2")
+        self.failUnlessReallyEqual(convert2([]), "has shares: ")
+        self.failUnlessReallyEqual(convert2(["1"]), "has share: 1")
+        self.failUnlessReallyEqual(convert2(["1","2"]), "has shares: 1,2")
 
 
-class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
+class Grid(GridTestMixin, WebErrorMixin, ShouldFailMixin, testutil.ReallyEqualMixin, unittest.TestCase):
 
     def CHECK(self, ign, which, args, clientnum=0):
         fileurl = self.fileurls[which]
@@ -2523,11 +4090,14 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
         def _stash_mutable_uri(n, which):
             self.uris[which] = n.get_uri()
             assert isinstance(self.uris[which], str)
-        d.addCallback(lambda ign: c0.create_mutable_file(DATA+"3"))
+        d.addCallback(lambda ign:
+            c0.create_mutable_file(publish.MutableData(DATA+"3")))
         d.addCallback(_stash_mutable_uri, "corrupt")
         d.addCallback(lambda ign:
                       c0.upload(upload.Data("literal", convergence="")))
         d.addCallback(_stash_uri, "small")
+        d.addCallback(lambda ign: c0.create_immutable_dirnode({}))
+        d.addCallback(_stash_mutable_uri, "smalldir")
 
         def _compute_fileurls(ignored):
             self.fileurls = {}
@@ -2536,14 +4106,14 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
         d.addCallback(_compute_fileurls)
 
         def _clobber_shares(ignored):
-            good_shares = self.find_shares(self.uris["good"])
-            self.failUnlessEqual(len(good_shares), 10)
-            sick_shares = self.find_shares(self.uris["sick"])
+            good_shares = self.find_uri_shares(self.uris["good"])
+            self.failUnlessReallyEqual(len(good_shares), 10)
+            sick_shares = self.find_uri_shares(self.uris["sick"])
             os.unlink(sick_shares[0][2])
-            dead_shares = self.find_shares(self.uris["dead"])
+            dead_shares = self.find_uri_shares(self.uris["dead"])
             for i in range(1, 10):
                 os.unlink(dead_shares[i][2])
-            c_shares = self.find_shares(self.uris["corrupt"])
+            c_shares = self.find_uri_shares(self.uris["corrupt"])
             cso = CorruptShareOptions()
             cso.stdout = StringIO()
             cso.parseOptions([c_shares[0][2]])
@@ -2559,7 +4129,7 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
         def _got_html_good_return_to(res):
             self.failUnless("Healthy" in res, res)
             self.failIf("Not Healthy" in res, res)
-            self.failUnless('<a href="somewhere">Return to parent directory'
+            self.failUnless('<a href="somewhere">Return to file'
                             in res, res)
         d.addCallback(_got_html_good_return_to)
         d.addCallback(self.CHECK, "good", "t=check&output=json")
@@ -2580,7 +4150,7 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
         def _got_html_small_return_to(res):
             self.failUnless("Literal files are always healthy" in res, res)
             self.failIf("Not Healthy" in res, res)
-            self.failUnless('<a href="somewhere">Return to parent directory'
+            self.failUnless('<a href="somewhere">Return to file'
                             in res, res)
         d.addCallback(_got_html_small_return_to)
         d.addCallback(self.CHECK, "small", "t=check&output=json")
@@ -2590,6 +4160,18 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
             self.failUnless(r["results"]["healthy"])
         d.addCallback(_got_json_small)
 
+        d.addCallback(self.CHECK, "smalldir", "t=check")
+        def _got_html_smalldir(res):
+            self.failUnless("Literal files are always healthy" in res, res)
+            self.failIf("Not Healthy" in res, res)
+        d.addCallback(_got_html_smalldir)
+        d.addCallback(self.CHECK, "smalldir", "t=check&output=json")
+        def _got_json_smalldir(res):
+            r = simplejson.loads(res)
+            self.failUnlessEqual(r["storage-index"], "")
+            self.failUnless(r["results"]["healthy"])
+        d.addCallback(_got_json_smalldir)
+
         d.addCallback(self.CHECK, "sick", "t=check")
         def _got_html_sick(res):
             self.failUnless("Not Healthy" in res, res)
@@ -2629,8 +4211,8 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
                             r["summary"])
             self.failIf(r["results"]["healthy"])
             self.failUnless(r["results"]["recoverable"])
-            self.failUnlessEqual(r["results"]["count-shares-good"], 9)
-            self.failUnlessEqual(r["results"]["count-corrupt-shares"], 1)
+            self.failUnlessReallyEqual(r["results"]["count-shares-good"], 9)
+            self.failUnlessReallyEqual(r["results"]["count-corrupt-shares"], 1)
         d.addCallback(_got_json_corrupt)
 
         d.addErrback(self.explain_web_error)
@@ -2655,7 +4237,8 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
         def _stash_mutable_uri(n, which):
             self.uris[which] = n.get_uri()
             assert isinstance(self.uris[which], str)
-        d.addCallback(lambda ign: c0.create_mutable_file(DATA+"3"))
+        d.addCallback(lambda ign:
+            c0.create_mutable_file(publish.MutableData(DATA+"3")))
         d.addCallback(_stash_mutable_uri, "corrupt")
 
         def _compute_fileurls(ignored):
@@ -2665,14 +4248,14 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
         d.addCallback(_compute_fileurls)
 
         def _clobber_shares(ignored):
-            good_shares = self.find_shares(self.uris["good"])
-            self.failUnlessEqual(len(good_shares), 10)
-            sick_shares = self.find_shares(self.uris["sick"])
+            good_shares = self.find_uri_shares(self.uris["good"])
+            self.failUnlessReallyEqual(len(good_shares), 10)
+            sick_shares = self.find_uri_shares(self.uris["sick"])
             os.unlink(sick_shares[0][2])
-            dead_shares = self.find_shares(self.uris["dead"])
+            dead_shares = self.find_uri_shares(self.uris["dead"])
             for i in range(1, 10):
                 os.unlink(dead_shares[i][2])
-            c_shares = self.find_shares(self.uris["corrupt"])
+            c_shares = self.find_uri_shares(self.uris["corrupt"])
             cso = CorruptShareOptions()
             cso.stdout = StringIO()
             cso.parseOptions([c_shares[0][2]])
@@ -2733,15 +4316,15 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
         d.addCallback(_compute_fileurls)
 
         def _clobber_shares(ignored):
-            sick_shares = self.find_shares(self.uris["sick"])
+            sick_shares = self.find_uri_shares(self.uris["sick"])
             os.unlink(sick_shares[0][2])
         d.addCallback(_clobber_shares)
 
         d.addCallback(self.CHECK, "sick", "t=check&repair=true&output=json")
         def _got_json_sick(res):
             r = simplejson.loads(res)
-            self.failUnlessEqual(r["repair-attempted"], True)
-            self.failUnlessEqual(r["repair-successful"], True)
+            self.failUnlessReallyEqual(r["repair-attempted"], True)
+            self.failUnlessReallyEqual(r["repair-successful"], True)
             self.failUnlessEqual(r["pre-repair-results"]["summary"],
                                  "Not Healthy: 9 shares (enc 3-of-10)")
             self.failIf(r["pre-repair-results"]["results"]["healthy"])
@@ -2752,6 +4335,269 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
         d.addErrback(self.explain_web_error)
         return d
 
+    def test_unknown(self, immutable=False):
+        self.basedir = "web/Grid/unknown"
+        if immutable:
+            self.basedir = "web/Grid/unknown-immutable"
+
+        self.set_up_grid()
+        c0 = self.g.clients[0]
+        self.uris = {}
+        self.fileurls = {}
+
+        # the future cap format may contain slashes, which must be tolerated
+        expected_info_url = "uri/%s?t=info" % urllib.quote(unknown_rwcap,
+                                                           safe="")
+
+        if immutable:
+            name = u"future-imm"
+            future_node = UnknownNode(None, unknown_immcap, deep_immutable=True)
+            d = c0.create_immutable_dirnode({name: (future_node, {})})
+        else:
+            name = u"future"
+            future_node = UnknownNode(unknown_rwcap, unknown_rocap)
+            d = c0.create_dirnode()
+
+        def _stash_root_and_create_file(n):
+            self.rootnode = n
+            self.rooturl = "uri/" + urllib.quote(n.get_uri()) + "/"
+            self.rourl = "uri/" + urllib.quote(n.get_readonly_uri()) + "/"
+            if not immutable:
+                return self.rootnode.set_node(name, future_node)
+        d.addCallback(_stash_root_and_create_file)
+
+        # make sure directory listing tolerates unknown nodes
+        d.addCallback(lambda ign: self.GET(self.rooturl))
+        def _check_directory_html(res, expected_type_suffix):
+            pattern = re.compile(r'<td>\?%s</td>[ \t\n\r]*'
+                                  '<td>%s</td>' % (expected_type_suffix, str(name)),
+                                 re.DOTALL)
+            self.failUnless(re.search(pattern, res), res)
+            # find the More Info link for name, should be relative
+            mo = re.search(r'<a href="([^"]+)">More Info</a>', res)
+            info_url = mo.group(1)
+            self.failUnlessReallyEqual(info_url, "%s?t=info" % (str(name),))
+        if immutable:
+            d.addCallback(_check_directory_html, "-IMM")
+        else:
+            d.addCallback(_check_directory_html, "")
+
+        d.addCallback(lambda ign: self.GET(self.rooturl+"?t=json"))
+        def _check_directory_json(res, expect_rw_uri):
+            data = simplejson.loads(res)
+            self.failUnlessEqual(data[0], "dirnode")
+            f = data[1]["children"][name]
+            self.failUnlessEqual(f[0], "unknown")
+            if expect_rw_uri:
+                self.failUnlessReallyEqual(to_str(f[1]["rw_uri"]), unknown_rwcap, data)
+            else:
+                self.failIfIn("rw_uri", f[1])
+            if immutable:
+                self.failUnlessReallyEqual(to_str(f[1]["ro_uri"]), unknown_immcap, data)
+            else:
+                self.failUnlessReallyEqual(to_str(f[1]["ro_uri"]), unknown_rocap, data)
+            self.failUnless("metadata" in f[1])
+        d.addCallback(_check_directory_json, expect_rw_uri=not immutable)
+
+        def _check_info(res, expect_rw_uri, expect_ro_uri):
+            self.failUnlessIn("Object Type: <span>unknown</span>", res)
+            if expect_rw_uri:
+                self.failUnlessIn(unknown_rwcap, res)
+            if expect_ro_uri:
+                if immutable:
+                    self.failUnlessIn(unknown_immcap, res)
+                else:
+                    self.failUnlessIn(unknown_rocap, res)
+            else:
+                self.failIfIn(unknown_rocap, res)
+            self.failIfIn("Raw data as", res)
+            self.failIfIn("Directory writecap", res)
+            self.failIfIn("Checker Operations", res)
+            self.failIfIn("Mutable File Operations", res)
+            self.failIfIn("Directory Operations", res)
+
+        # FIXME: these should have expect_rw_uri=not immutable; I don't know
+        # why they fail. Possibly related to ticket #922.
+
+        d.addCallback(lambda ign: self.GET(expected_info_url))
+        d.addCallback(_check_info, expect_rw_uri=False, expect_ro_uri=False)
+        d.addCallback(lambda ign: self.GET("%s%s?t=info" % (self.rooturl, str(name))))
+        d.addCallback(_check_info, expect_rw_uri=False, expect_ro_uri=True)
+
+        def _check_json(res, expect_rw_uri):
+            data = simplejson.loads(res)
+            self.failUnlessEqual(data[0], "unknown")
+            if expect_rw_uri:
+                self.failUnlessReallyEqual(to_str(data[1]["rw_uri"]), unknown_rwcap, data)
+            else:
+                self.failIfIn("rw_uri", data[1])
+
+            if immutable:
+                self.failUnlessReallyEqual(to_str(data[1]["ro_uri"]), unknown_immcap, data)
+                self.failUnlessReallyEqual(data[1]["mutable"], False)
+            elif expect_rw_uri:
+                self.failUnlessReallyEqual(to_str(data[1]["ro_uri"]), unknown_rocap, data)
+                self.failUnlessReallyEqual(data[1]["mutable"], True)
+            else:
+                self.failUnlessReallyEqual(to_str(data[1]["ro_uri"]), unknown_rocap, data)
+                self.failIf("mutable" in data[1], data[1])
+
+            # TODO: check metadata contents
+            self.failUnless("metadata" in data[1])
+
+        d.addCallback(lambda ign: self.GET("%s%s?t=json" % (self.rooturl, str(name))))
+        d.addCallback(_check_json, expect_rw_uri=not immutable)
+
+        # and make sure that a read-only version of the directory can be
+        # rendered too. This version will not have unknown_rwcap, whether
+        # or not future_node was immutable.
+        d.addCallback(lambda ign: self.GET(self.rourl))
+        if immutable:
+            d.addCallback(_check_directory_html, "-IMM")
+        else:
+            d.addCallback(_check_directory_html, "-RO")
+
+        d.addCallback(lambda ign: self.GET(self.rourl+"?t=json"))
+        d.addCallback(_check_directory_json, expect_rw_uri=False)
+
+        d.addCallback(lambda ign: self.GET("%s%s?t=json" % (self.rourl, str(name))))
+        d.addCallback(_check_json, expect_rw_uri=False)
+
+        # TODO: check that getting t=info from the Info link in the ro directory
+        # works, and does not include the writecap URI.
+        return d
+
+    def test_immutable_unknown(self):
+        return self.test_unknown(immutable=True)
+
+    def test_mutant_dirnodes_are_omitted(self):
+        self.basedir = "web/Grid/mutant_dirnodes_are_omitted"
+
+        self.set_up_grid()
+        c = self.g.clients[0]
+        nm = c.nodemaker
+        self.uris = {}
+        self.fileurls = {}
+
+        lonely_uri = "URI:LIT:n5xgk" # LIT for "one"
+        mut_write_uri = "URI:SSK:vfvcbdfbszyrsaxchgevhmmlii:euw4iw7bbnkrrwpzuburbhppuxhc3gwxv26f6imekhz7zyw2ojnq"
+        mut_read_uri = "URI:SSK-RO:e3mdrzfwhoq42hy5ubcz6rp3o4:ybyibhnp3vvwuq2vaw2ckjmesgkklfs6ghxleztqidihjyofgw7q"
+
+        # This method tests mainly dirnode, but we'd have to duplicate code in order to
+        # test the dirnode and web layers separately.
+
+        # 'lonely' is a valid LIT child, 'ro' is a mutant child with an SSK-RO readcap,
+        # and 'write-in-ro' is a mutant child with an SSK writecap in the ro_uri field.
+        # When the directory is read, the mutants should be silently disposed of, leaving
+        # their lonely sibling.
+        # We don't test the case of a retrieving a cap from the encrypted rw_uri field,
+        # because immutable directories don't have a writecap and therefore that field
+        # isn't (and can't be) decrypted.
+        # TODO: The field still exists in the netstring. Technically we should check what
+        # happens if something is put there (_unpack_contents should raise ValueError),
+        # but that can wait.
+
+        lonely_child = nm.create_from_cap(lonely_uri)
+        mutant_ro_child = nm.create_from_cap(mut_read_uri)
+        mutant_write_in_ro_child = nm.create_from_cap(mut_write_uri)
+
+        def _by_hook_or_by_crook():
+            return True
+        for n in [mutant_ro_child, mutant_write_in_ro_child]:
+            n.is_allowed_in_immutable_directory = _by_hook_or_by_crook
+
+        mutant_write_in_ro_child.get_write_uri    = lambda: None
+        mutant_write_in_ro_child.get_readonly_uri = lambda: mut_write_uri
+
+        kids = {u"lonely":      (lonely_child, {}),
+                u"ro":          (mutant_ro_child, {}),
+                u"write-in-ro": (mutant_write_in_ro_child, {}),
+                }
+        d = c.create_immutable_dirnode(kids)
+
+        def _created(dn):
+            self.failUnless(isinstance(dn, dirnode.DirectoryNode))
+            self.failIf(dn.is_mutable())
+            self.failUnless(dn.is_readonly())
+            # This checks that if we somehow ended up calling dn._decrypt_rwcapdata, it would fail.
+            self.failIf(hasattr(dn._node, 'get_writekey'))
+            rep = str(dn)
+            self.failUnless("RO-IMM" in rep)
+            cap = dn.get_cap()
+            self.failUnlessIn("CHK", cap.to_string())
+            self.cap = cap
+            self.rootnode = dn
+            self.rooturl = "uri/" + urllib.quote(dn.get_uri()) + "/"
+            return download_to_data(dn._node)
+        d.addCallback(_created)
+
+        def _check_data(data):
+            # Decode the netstring representation of the directory to check that all children
+            # are present. This is a bit of an abstraction violation, but there's not really
+            # any other way to do it given that the real DirectoryNode._unpack_contents would
+            # strip the mutant children out (which is what we're trying to test, later).
+            position = 0
+            numkids = 0
+            while position < len(data):
+                entries, position = split_netstring(data, 1, position)
+                entry = entries[0]
+                (name_utf8, ro_uri, rwcapdata, metadata_s), subpos = split_netstring(entry, 4)
+                name = name_utf8.decode("utf-8")
+                self.failUnless(rwcapdata == "")
+                self.failUnless(name in kids)
+                (expected_child, ign) = kids[name]
+                self.failUnlessReallyEqual(ro_uri, expected_child.get_readonly_uri())
+                numkids += 1
+
+            self.failUnlessReallyEqual(numkids, 3)
+            return self.rootnode.list()
+        d.addCallback(_check_data)
+
+        # Now when we use the real directory listing code, the mutants should be absent.
+        def _check_kids(children):
+            self.failUnlessReallyEqual(sorted(children.keys()), [u"lonely"])
+            lonely_node, lonely_metadata = children[u"lonely"]
+
+            self.failUnlessReallyEqual(lonely_node.get_write_uri(), None)
+            self.failUnlessReallyEqual(lonely_node.get_readonly_uri(), lonely_uri)
+        d.addCallback(_check_kids)
+
+        d.addCallback(lambda ign: nm.create_from_cap(self.cap.to_string()))
+        d.addCallback(lambda n: n.list())
+        d.addCallback(_check_kids)  # again with dirnode recreated from cap
+
+        # Make sure the lonely child can be listed in HTML...
+        d.addCallback(lambda ign: self.GET(self.rooturl))
+        def _check_html(res):
+            self.failIfIn("URI:SSK", res)
+            get_lonely = "".join([r'<td>FILE</td>',
+                                  r'\s+<td>',
+                                  r'<a href="[^"]+%s[^"]+">lonely</a>' % (urllib.quote(lonely_uri),),
+                                  r'</td>',
+                                  r'\s+<td align="right">%d</td>' % len("one"),
+                                  ])
+            self.failUnless(re.search(get_lonely, res), res)
+
+            # find the More Info link for name, should be relative
+            mo = re.search(r'<a href="([^"]+)">More Info</a>', res)
+            info_url = mo.group(1)
+            self.failUnless(info_url.endswith(urllib.quote(lonely_uri) + "?t=info"), info_url)
+        d.addCallback(_check_html)
+
+        # ... and in JSON.
+        d.addCallback(lambda ign: self.GET(self.rooturl+"?t=json"))
+        def _check_json(res):
+            data = simplejson.loads(res)
+            self.failUnlessEqual(data[0], "dirnode")
+            listed_children = data[1]["children"]
+            self.failUnlessReallyEqual(sorted(listed_children.keys()), [u"lonely"])
+            ll_type, ll_data = listed_children[u"lonely"]
+            self.failUnlessEqual(ll_type, "filenode")
+            self.failIf("rw_uri" in ll_data)
+            self.failUnlessReallyEqual(to_str(ll_data["ro_uri"]), lonely_uri)
+        d.addCallback(_check_json)
+        return d
+
     def test_deep_check(self):
         self.basedir = "web/Grid/deep_check"
         self.set_up_grid()
@@ -2759,7 +4605,7 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
         self.uris = {}
         self.fileurls = {}
         DATA = "data" * 100
-        d = c0.create_empty_dirnode()
+        d = c0.create_dirnode()
         def _stash_root_and_create_file(n):
             self.rootnode = n
             self.fileurls["root"] = "uri/" + urllib.quote(n.get_uri()) + "/"
@@ -2780,6 +4626,11 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
                                                         convergence="")))
         d.addCallback(_stash_uri, "sick")
 
+        # this tests that deep-check and stream-manifest will ignore
+        # UnknownNode instances. Hopefully this will also cover deep-stats.
+        future_node = UnknownNode(unknown_rwcap, unknown_rocap)
+        d.addCallback(lambda ign: self.rootnode.set_node(u"future", future_node))
+
         def _clobber_shares(ignored):
             self.delete_shares_numbered(self.uris["sick"], [0,1])
         d.addCallback(_clobber_shares)
@@ -2788,40 +4639,65 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
         # root/good
         # root/small
         # root/sick
+        # root/future
 
         d.addCallback(self.CHECK, "root", "t=stream-deep-check")
         def _done(res):
-            units = [simplejson.loads(line)
-                     for line in res.splitlines()
-                     if line]
-            self.failUnlessEqual(len(units), 4+1)
+            try:
+                units = [simplejson.loads(line)
+                         for line in res.splitlines()
+                         if line]
+            except ValueError:
+                print "response is:", res
+                print "undecodeable line was '%s'" % line
+                raise
+            self.failUnlessReallyEqual(len(units), 5+1)
             # should be parent-first
             u0 = units[0]
             self.failUnlessEqual(u0["path"], [])
             self.failUnlessEqual(u0["type"], "directory")
-            self.failUnlessEqual(u0["cap"], self.rootnode.get_uri())
+            self.failUnlessReallyEqual(to_str(u0["cap"]), self.rootnode.get_uri())
             u0cr = u0["check-results"]
-            self.failUnlessEqual(u0cr["results"]["count-shares-good"], 10)
+            self.failUnlessReallyEqual(u0cr["results"]["count-shares-good"], 10)
 
             ugood = [u for u in units
                      if u["type"] == "file" and u["path"] == [u"good"]][0]
-            self.failUnlessEqual(ugood["cap"], self.uris["good"])
+            self.failUnlessReallyEqual(to_str(ugood["cap"]), self.uris["good"])
             ugoodcr = ugood["check-results"]
-            self.failUnlessEqual(ugoodcr["results"]["count-shares-good"], 10)
+            self.failUnlessReallyEqual(ugoodcr["results"]["count-shares-good"], 10)
 
             stats = units[-1]
             self.failUnlessEqual(stats["type"], "stats")
             s = stats["stats"]
-            self.failUnlessEqual(s["count-immutable-files"], 2)
-            self.failUnlessEqual(s["count-literal-files"], 1)
-            self.failUnlessEqual(s["count-directories"], 1)
+            self.failUnlessReallyEqual(s["count-immutable-files"], 2)
+            self.failUnlessReallyEqual(s["count-literal-files"], 1)
+            self.failUnlessReallyEqual(s["count-directories"], 1)
+            self.failUnlessReallyEqual(s["count-unknown"], 1)
         d.addCallback(_done)
 
+        d.addCallback(self.CHECK, "root", "t=stream-manifest")
+        def _check_manifest(res):
+            self.failUnless(res.endswith("\n"))
+            units = [simplejson.loads(t) for t in res[:-1].split("\n")]
+            self.failUnlessReallyEqual(len(units), 5+1)
+            self.failUnlessEqual(units[-1]["type"], "stats")
+            first = units[0]
+            self.failUnlessEqual(first["path"], [])
+            self.failUnlessEqual(to_str(first["cap"]), self.rootnode.get_uri())
+            self.failUnlessEqual(first["type"], "directory")
+            stats = units[-1]["stats"]
+            self.failUnlessReallyEqual(stats["count-immutable-files"], 2)
+            self.failUnlessReallyEqual(stats["count-literal-files"], 1)
+            self.failUnlessReallyEqual(stats["count-mutable-files"], 0)
+            self.failUnlessReallyEqual(stats["count-immutable-files"], 2)
+            self.failUnlessReallyEqual(stats["count-unknown"], 1)
+        d.addCallback(_check_manifest)
+
         # now add root/subdir and root/subdir/grandchild, then make subdir
         # unrecoverable, then see what happens
 
         d.addCallback(lambda ign:
-                      self.rootnode.create_empty_directory(u"subdir"))
+                      self.rootnode.create_subdirectory(u"subdir"))
         d.addCallback(_stash_uri, "subdir")
         d.addCallback(lambda subdir_node:
                       subdir_node.add_file(u"grandchild",
@@ -2837,6 +4713,7 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
         # root/good
         # root/small
         # root/sick
+        # root/future
         # root/subdir [unrecoverable]
         # root/subdir/grandchild
 
@@ -2859,7 +4736,7 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
                               error_line)
             self.failUnless(len(error_msg) > 2, error_msg_s) # some traceback
             units = [simplejson.loads(line) for line in lines[:first_error]]
-            self.failUnlessEqual(len(units), 5) # includes subdir
+            self.failUnlessReallyEqual(len(units), 6) # includes subdir
             last_unit = units[-1]
             self.failUnlessEqual(last_unit["path"], ["subdir"])
         d.addCallback(_check_broken_manifest)
@@ -2880,13 +4757,13 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
                               error_line)
             self.failUnless(len(error_msg) > 2, error_msg_s) # some traceback
             units = [simplejson.loads(line) for line in lines[:first_error]]
-            self.failUnlessEqual(len(units), 5) # includes subdir
+            self.failUnlessReallyEqual(len(units), 6) # includes subdir
             last_unit = units[-1]
             self.failUnlessEqual(last_unit["path"], ["subdir"])
             r = last_unit["check-results"]["results"]
-            self.failUnlessEqual(r["count-recoverable-versions"], 0)
-            self.failUnlessEqual(r["count-shares-good"], 1)
-            self.failUnlessEqual(r["recoverable"], False)
+            self.failUnlessReallyEqual(r["count-recoverable-versions"], 0)
+            self.failUnlessReallyEqual(r["count-shares-good"], 1)
+            self.failUnlessReallyEqual(r["recoverable"], False)
         d.addCallback(_check_broken_deepcheck)
 
         d.addErrback(self.explain_web_error)
@@ -2899,7 +4776,7 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
         self.uris = {}
         self.fileurls = {}
         DATA = "data" * 100
-        d = c0.create_empty_dirnode()
+        d = c0.create_dirnode()
         def _stash_root_and_create_file(n):
             self.rootnode = n
             self.fileurls["root"] = "uri/" + urllib.quote(n.get_uri()) + "/"
@@ -2929,15 +4806,15 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
         #d.addCallback(_stash_uri, "corrupt")
 
         def _clobber_shares(ignored):
-            good_shares = self.find_shares(self.uris["good"])
-            self.failUnlessEqual(len(good_shares), 10)
-            sick_shares = self.find_shares(self.uris["sick"])
+            good_shares = self.find_uri_shares(self.uris["good"])
+            self.failUnlessReallyEqual(len(good_shares), 10)
+            sick_shares = self.find_uri_shares(self.uris["sick"])
             os.unlink(sick_shares[0][2])
-            #dead_shares = self.find_shares(self.uris["dead"])
+            #dead_shares = self.find_uri_shares(self.uris["dead"])
             #for i in range(1, 10):
             #    os.unlink(dead_shares[i][2])
 
-            #c_shares = self.find_shares(self.uris["corrupt"])
+            #c_shares = self.find_uri_shares(self.uris["corrupt"])
             #cso = CorruptShareOptions()
             #cso.stdout = StringIO()
             #cso.parseOptions([c_shares[0][2]])
@@ -2954,38 +4831,38 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
             units = [simplejson.loads(line)
                      for line in res.splitlines()
                      if line]
-            self.failUnlessEqual(len(units), 4+1)
+            self.failUnlessReallyEqual(len(units), 4+1)
             # should be parent-first
             u0 = units[0]
             self.failUnlessEqual(u0["path"], [])
             self.failUnlessEqual(u0["type"], "directory")
-            self.failUnlessEqual(u0["cap"], self.rootnode.get_uri())
+            self.failUnlessReallyEqual(to_str(u0["cap"]), self.rootnode.get_uri())
             u0crr = u0["check-and-repair-results"]
-            self.failUnlessEqual(u0crr["repair-attempted"], False)
-            self.failUnlessEqual(u0crr["pre-repair-results"]["results"]["count-shares-good"], 10)
+            self.failUnlessReallyEqual(u0crr["repair-attempted"], False)
+            self.failUnlessReallyEqual(u0crr["pre-repair-results"]["results"]["count-shares-good"], 10)
 
             ugood = [u for u in units
                      if u["type"] == "file" and u["path"] == [u"good"]][0]
-            self.failUnlessEqual(ugood["cap"], self.uris["good"])
+            self.failUnlessEqual(to_str(ugood["cap"]), self.uris["good"])
             ugoodcrr = ugood["check-and-repair-results"]
-            self.failUnlessEqual(u0crr["repair-attempted"], False)
-            self.failUnlessEqual(u0crr["pre-repair-results"]["results"]["count-shares-good"], 10)
+            self.failUnlessReallyEqual(ugoodcrr["repair-attempted"], False)
+            self.failUnlessReallyEqual(ugoodcrr["pre-repair-results"]["results"]["count-shares-good"], 10)
 
             usick = [u for u in units
                      if u["type"] == "file" and u["path"] == [u"sick"]][0]
-            self.failUnlessEqual(usick["cap"], self.uris["sick"])
+            self.failUnlessReallyEqual(to_str(usick["cap"]), self.uris["sick"])
             usickcrr = usick["check-and-repair-results"]
-            self.failUnlessEqual(usickcrr["repair-attempted"], True)
-            self.failUnlessEqual(usickcrr["repair-successful"], True)
-            self.failUnlessEqual(usickcrr["pre-repair-results"]["results"]["count-shares-good"], 9)
-            self.failUnlessEqual(usickcrr["post-repair-results"]["results"]["count-shares-good"], 10)
+            self.failUnlessReallyEqual(usickcrr["repair-attempted"], True)
+            self.failUnlessReallyEqual(usickcrr["repair-successful"], True)
+            self.failUnlessReallyEqual(usickcrr["pre-repair-results"]["results"]["count-shares-good"], 9)
+            self.failUnlessReallyEqual(usickcrr["post-repair-results"]["results"]["count-shares-good"], 10)
 
             stats = units[-1]
             self.failUnlessEqual(stats["type"], "stats")
             s = stats["stats"]
-            self.failUnlessEqual(s["count-immutable-files"], 2)
-            self.failUnlessEqual(s["count-literal-files"], 1)
-            self.failUnlessEqual(s["count-directories"], 1)
+            self.failUnlessReallyEqual(s["count-immutable-files"], 2)
+            self.failUnlessReallyEqual(s["count-literal-files"], 1)
+            self.failUnlessReallyEqual(s["count-directories"], 1)
         d.addCallback(_done)
 
         d.addErrback(self.explain_web_error)
@@ -2993,17 +4870,11 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
 
     def _count_leases(self, ignored, which):
         u = self.uris[which]
-        shares = self.find_shares(u)
+        shares = self.find_uri_shares(u)
         lease_counts = []
         for shnum, serverid, fn in shares:
-            if u.startswith("URI:SSK") or u.startswith("URI:DIR2"):
-                sf = MutableShareFile(fn)
-                num_leases = len(sf.debug_get_leases())
-            elif u.startswith("URI:CHK"):
-                sf = ShareFile(fn)
-                num_leases = len(list(sf.iter_leases()))
-            else:
-                raise ValueError("can't count leases on %s" % u)
+            sf = get_share_file(fn)
+            num_leases = len(list(sf.get_leases()))
         lease_counts.append( (fn, num_leases) )
         return lease_counts
 
@@ -3029,7 +4900,8 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
         def _stash_mutable_uri(n, which):
             self.uris[which] = n.get_uri()
             assert isinstance(self.uris[which], str)
-        d.addCallback(lambda ign: c0.create_mutable_file(DATA+"2"))
+        d.addCallback(lambda ign:
+            c0.create_mutable_file(publish.MutableData(DATA+"2")))
         d.addCallback(_stash_mutable_uri, "mutable")
 
         def _compute_fileurls(ignored):
@@ -3112,7 +4984,7 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
         self.uris = {}
         self.fileurls = {}
         DATA = "data" * 100
-        d = c0.create_empty_dirnode()
+        d = c0.create_dirnode()
         def _stash_root_and_create_file(n):
             self.rootnode = n
             self.uris["root"] = n.get_uri()
@@ -3128,7 +5000,8 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
                                                         convergence="")))
         d.addCallback(_stash_uri, "small")
 
-        d.addCallback(lambda ign: c0.create_mutable_file("mutable"))
+        d.addCallback(lambda ign:
+            c0.create_mutable_file(publish.MutableData("mutable")))
         d.addCallback(lambda fn: self.rootnode.set_node(u"mutable", fn))
         d.addCallback(_stash_uri, "mutable")
 
@@ -3138,7 +5011,7 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
                      for line in res.splitlines()
                      if line]
             # root, one, small, mutable,   stats
-            self.failUnlessEqual(len(units), 4+1)
+            self.failUnlessReallyEqual(len(units), 4+1)
         d.addCallback(_done)
 
         d.addCallback(self._count_leases, "root")
@@ -3171,3 +5044,367 @@ class Grid(GridTestMixin, WebErrorMixin, unittest.TestCase, ShouldFailMixin):
 
         d.addErrback(self.explain_web_error)
         return d
+
+
+    def test_exceptions(self):
+        self.basedir = "web/Grid/exceptions"
+        self.set_up_grid(num_clients=1, num_servers=2)
+        c0 = self.g.clients[0]
+        c0.DEFAULT_ENCODING_PARAMETERS['happy'] = 2
+        self.fileurls = {}
+        DATA = "data" * 100
+        d = c0.create_dirnode()
+        def _stash_root(n):
+            self.fileurls["root"] = "uri/" + urllib.quote(n.get_uri()) + "/"
+            self.fileurls["imaginary"] = self.fileurls["root"] + "imaginary"
+            return n
+        d.addCallback(_stash_root)
+        d.addCallback(lambda ign: c0.upload(upload.Data(DATA, convergence="")))
+        def _stash_bad(ur):
+            self.fileurls["1share"] = "uri/" + urllib.quote(ur.uri)
+            self.delete_shares_numbered(ur.uri, range(1,10))
+
+            u = uri.from_string(ur.uri)
+            u.key = testutil.flip_bit(u.key, 0)
+            baduri = u.to_string()
+            self.fileurls["0shares"] = "uri/" + urllib.quote(baduri)
+        d.addCallback(_stash_bad)
+        d.addCallback(lambda ign: c0.create_dirnode())
+        def _mangle_dirnode_1share(n):
+            u = n.get_uri()
+            url = self.fileurls["dir-1share"] = "uri/" + urllib.quote(u) + "/"
+            self.fileurls["dir-1share-json"] = url + "?t=json"
+            self.delete_shares_numbered(u, range(1,10))
+        d.addCallback(_mangle_dirnode_1share)
+        d.addCallback(lambda ign: c0.create_dirnode())
+        def _mangle_dirnode_0share(n):
+            u = n.get_uri()
+            url = self.fileurls["dir-0share"] = "uri/" + urllib.quote(u) + "/"
+            self.fileurls["dir-0share-json"] = url + "?t=json"
+            self.delete_shares_numbered(u, range(0,10))
+        d.addCallback(_mangle_dirnode_0share)
+
+        # NotEnoughSharesError should be reported sensibly, with a
+        # text/plain explanation of the problem, and perhaps some
+        # information on which shares *could* be found.
+
+        d.addCallback(lambda ignored:
+                      self.shouldHTTPError("GET unrecoverable",
+                                           410, "Gone", "NoSharesError",
+                                           self.GET, self.fileurls["0shares"]))
+        def _check_zero_shares(body):
+            self.failIf("<html>" in body, body)
+            body = " ".join(body.strip().split())
+            exp = ("NoSharesError: no shares could be found. "
+                   "Zero shares usually indicates a corrupt URI, or that "
+                   "no servers were connected, but it might also indicate "
+                   "severe corruption. You should perform a filecheck on "
+                   "this object to learn more. The full error message is: "
+                   "no shares (need 3). Last failure: None")
+            self.failUnlessReallyEqual(exp, body)
+        d.addCallback(_check_zero_shares)
+
+
+        d.addCallback(lambda ignored:
+                      self.shouldHTTPError("GET 1share",
+                                           410, "Gone", "NotEnoughSharesError",
+                                           self.GET, self.fileurls["1share"]))
+        def _check_one_share(body):
+            self.failIf("<html>" in body, body)
+            body = " ".join(body.strip().split())
+            msgbase = ("NotEnoughSharesError: This indicates that some "
+                       "servers were unavailable, or that shares have been "
+                       "lost to server departure, hard drive failure, or disk "
+                       "corruption. You should perform a filecheck on "
+                       "this object to learn more. The full error message is:"
+                       )
+            msg1 = msgbase + (" ran out of shares:"
+                              " complete=sh0"
+                              " pending="
+                              " overdue= unused= need 3. Last failure: None")
+            msg2 = msgbase + (" ran out of shares:"
+                              " complete="
+                              " pending=Share(sh0-on-xgru5)"
+                              " overdue= unused= need 3. Last failure: None")
+            self.failUnless(body == msg1 or body == msg2, body)
+        d.addCallback(_check_one_share)
+
+        d.addCallback(lambda ignored:
+                      self.shouldHTTPError("GET imaginary",
+                                           404, "Not Found", None,
+                                           self.GET, self.fileurls["imaginary"]))
+        def _missing_child(body):
+            self.failUnless("No such child: imaginary" in body, body)
+        d.addCallback(_missing_child)
+
+        d.addCallback(lambda ignored: self.GET(self.fileurls["dir-0share"]))
+        def _check_0shares_dir_html(body):
+            self.failUnless("<html>" in body, body)
+            # we should see the regular page, but without the child table or
+            # the dirops forms
+            body = " ".join(body.strip().split())
+            self.failUnlessIn('href="?t=info">More info on this directory',
+                              body)
+            exp = ("UnrecoverableFileError: the directory (or mutable file) "
+                   "could not be retrieved, because there were insufficient "
+                   "good shares. This might indicate that no servers were "
+                   "connected, insufficient servers were connected, the URI "
+                   "was corrupt, or that shares have been lost due to server "
+                   "departure, hard drive failure, or disk corruption. You "
+                   "should perform a filecheck on this object to learn more.")
+            self.failUnlessIn(exp, body)
+            self.failUnlessIn("No upload forms: directory is unreadable", body)
+        d.addCallback(_check_0shares_dir_html)
+
+        d.addCallback(lambda ignored: self.GET(self.fileurls["dir-1share"]))
+        def _check_1shares_dir_html(body):
+            # at some point, we'll split UnrecoverableFileError into 0-shares
+            # and some-shares like we did for immutable files (since there
+            # are different sorts of advice to offer in each case). For now,
+            # they present the same way.
+            self.failUnless("<html>" in body, body)
+            body = " ".join(body.strip().split())
+            self.failUnlessIn('href="?t=info">More info on this directory',
+                              body)
+            exp = ("UnrecoverableFileError: the directory (or mutable file) "
+                   "could not be retrieved, because there were insufficient "
+                   "good shares. This might indicate that no servers were "
+                   "connected, insufficient servers were connected, the URI "
+                   "was corrupt, or that shares have been lost due to server "
+                   "departure, hard drive failure, or disk corruption. You "
+                   "should perform a filecheck on this object to learn more.")
+            self.failUnlessIn(exp, body)
+            self.failUnlessIn("No upload forms: directory is unreadable", body)
+        d.addCallback(_check_1shares_dir_html)
+
+        d.addCallback(lambda ignored:
+                      self.shouldHTTPError("GET dir-0share-json",
+                                           410, "Gone", "UnrecoverableFileError",
+                                           self.GET,
+                                           self.fileurls["dir-0share-json"]))
+        def _check_unrecoverable_file(body):
+            self.failIf("<html>" in body, body)
+            body = " ".join(body.strip().split())
+            exp = ("UnrecoverableFileError: the directory (or mutable file) "
+                   "could not be retrieved, because there were insufficient "
+                   "good shares. This might indicate that no servers were "
+                   "connected, insufficient servers were connected, the URI "
+                   "was corrupt, or that shares have been lost due to server "
+                   "departure, hard drive failure, or disk corruption. You "
+                   "should perform a filecheck on this object to learn more.")
+            self.failUnlessReallyEqual(exp, body)
+        d.addCallback(_check_unrecoverable_file)
+
+        d.addCallback(lambda ignored:
+                      self.shouldHTTPError("GET dir-1share-json",
+                                           410, "Gone", "UnrecoverableFileError",
+                                           self.GET,
+                                           self.fileurls["dir-1share-json"]))
+        d.addCallback(_check_unrecoverable_file)
+
+        d.addCallback(lambda ignored:
+                      self.shouldHTTPError("GET imaginary",
+                                           404, "Not Found", None,
+                                           self.GET, self.fileurls["imaginary"]))
+
+        # attach a webapi child that throws a random error, to test how it
+        # gets rendered.
+        w = c0.getServiceNamed("webish")
+        w.root.putChild("ERRORBOOM", ErrorBoom())
+
+        # "Accept: */*" :        should get a text/html stack trace
+        # "Accept: text/plain" : should get a text/plain stack trace
+        # "Accept: text/plain, application/octet-stream" : text/plain (CLI)
+        # no Accept header:      should get a text/html stack trace
+
+        d.addCallback(lambda ignored:
+                      self.shouldHTTPError("GET errorboom_html",
+                                           500, "Internal Server Error", None,
+                                           self.GET, "ERRORBOOM",
+                                           headers={"accept": ["*/*"]}))
+        def _internal_error_html1(body):
+            self.failUnless("<html>" in body, "expected HTML, not '%s'" % body)
+        d.addCallback(_internal_error_html1)
+
+        d.addCallback(lambda ignored:
+                      self.shouldHTTPError("GET errorboom_text",
+                                           500, "Internal Server Error", None,
+                                           self.GET, "ERRORBOOM",
+                                           headers={"accept": ["text/plain"]}))
+        def _internal_error_text2(body):
+            self.failIf("<html>" in body, body)
+            self.failUnless(body.startswith("Traceback "), body)
+        d.addCallback(_internal_error_text2)
+
+        CLI_accepts = "text/plain, application/octet-stream"
+        d.addCallback(lambda ignored:
+                      self.shouldHTTPError("GET errorboom_text",
+                                           500, "Internal Server Error", None,
+                                           self.GET, "ERRORBOOM",
+                                           headers={"accept": [CLI_accepts]}))
+        def _internal_error_text3(body):
+            self.failIf("<html>" in body, body)
+            self.failUnless(body.startswith("Traceback "), body)
+        d.addCallback(_internal_error_text3)
+
+        d.addCallback(lambda ignored:
+                      self.shouldHTTPError("GET errorboom_text",
+                                           500, "Internal Server Error", None,
+                                           self.GET, "ERRORBOOM"))
+        def _internal_error_html4(body):
+            self.failUnless("<html>" in body, "expected HTML, not '%s'" % body)
+        d.addCallback(_internal_error_html4)
+
+        def _flush_errors(res):
+            # Trial: please ignore the CompletelyUnhandledError in the logs
+            self.flushLoggedErrors(CompletelyUnhandledError)
+            return res
+        d.addBoth(_flush_errors)
+
+        return d
+
+    def test_blacklist(self):
+        # download from a blacklisted URI, get an error
+        self.basedir = "web/Grid/blacklist"
+        self.set_up_grid()
+        c0 = self.g.clients[0]
+        c0_basedir = c0.basedir
+        fn = os.path.join(c0_basedir, "access.blacklist")
+        self.uris = {}
+        DATA = "off-limits " * 50
+
+        d = c0.upload(upload.Data(DATA, convergence=""))
+        def _stash_uri_and_create_dir(ur):
+            self.uri = ur.uri
+            self.url = "uri/"+self.uri
+            u = uri.from_string_filenode(self.uri)
+            self.si = u.get_storage_index()
+            childnode = c0.create_node_from_uri(self.uri, None)
+            return c0.create_dirnode({u"blacklisted.txt": (childnode,{}) })
+        d.addCallback(_stash_uri_and_create_dir)
+        def _stash_dir(node):
+            self.dir_node = node
+            self.dir_uri = node.get_uri()
+            self.dir_url = "uri/"+self.dir_uri
+        d.addCallback(_stash_dir)
+        d.addCallback(lambda ign: self.GET(self.dir_url, followRedirect=True))
+        def _check_dir_html(body):
+            self.failUnlessIn("<html>", body)
+            self.failUnlessIn("blacklisted.txt</a>", body)
+        d.addCallback(_check_dir_html)
+        d.addCallback(lambda ign: self.GET(self.url))
+        d.addCallback(lambda body: self.failUnlessEqual(DATA, body))
+
+        def _blacklist(ign):
+            f = open(fn, "w")
+            f.write(" # this is a comment\n")
+            f.write(" \n")
+            f.write("\n") # also exercise blank lines
+            f.write("%s %s\n" % (base32.b2a(self.si), "off-limits to you"))
+            f.close()
+            # clients should be checking the blacklist each time, so we don't
+            # need to restart the client
+        d.addCallback(_blacklist)
+        d.addCallback(lambda ign: self.shouldHTTPError("get_from_blacklisted_uri",
+                                                       403, "Forbidden",
+                                                       "Access Prohibited: off-limits",
+                                                       self.GET, self.url))
+
+        # We should still be able to list the parent directory, in HTML...
+        d.addCallback(lambda ign: self.GET(self.dir_url, followRedirect=True))
+        def _check_dir_html2(body):
+            self.failUnlessIn("<html>", body)
+            self.failUnlessIn("blacklisted.txt</strike>", body)
+        d.addCallback(_check_dir_html2)
+
+        # ... and in JSON (used by CLI).
+        d.addCallback(lambda ign: self.GET(self.dir_url+"?t=json", followRedirect=True))
+        def _check_dir_json(res):
+            data = simplejson.loads(res)
+            self.failUnless(isinstance(data, list), data)
+            self.failUnlessEqual(data[0], "dirnode")
+            self.failUnless(isinstance(data[1], dict), data)
+            self.failUnlessIn("children", data[1])
+            self.failUnlessIn("blacklisted.txt", data[1]["children"])
+            childdata = data[1]["children"]["blacklisted.txt"]
+            self.failUnless(isinstance(childdata, list), data)
+            self.failUnlessEqual(childdata[0], "filenode")
+            self.failUnless(isinstance(childdata[1], dict), data)
+        d.addCallback(_check_dir_json)
+
+        def _unblacklist(ign):
+            open(fn, "w").close()
+            # the Blacklist object watches mtime to tell when the file has
+            # changed, but on windows this test will run faster than the
+            # filesystem's mtime resolution. So we edit Blacklist.last_mtime
+            # to force a reload.
+            self.g.clients[0].blacklist.last_mtime -= 2.0
+        d.addCallback(_unblacklist)
+
+        # now a read should work
+        d.addCallback(lambda ign: self.GET(self.url))
+        d.addCallback(lambda body: self.failUnlessEqual(DATA, body))
+
+        # read again to exercise the blacklist-is-unchanged logic
+        d.addCallback(lambda ign: self.GET(self.url))
+        d.addCallback(lambda body: self.failUnlessEqual(DATA, body))
+
+        # now add a blacklisted directory, and make sure files under it are
+        # refused too
+        def _add_dir(ign):
+            childnode = c0.create_node_from_uri(self.uri, None)
+            return c0.create_dirnode({u"child": (childnode,{}) })
+        d.addCallback(_add_dir)
+        def _get_dircap(dn):
+            self.dir_si_b32 = base32.b2a(dn.get_storage_index())
+            self.dir_url_base = "uri/"+dn.get_write_uri()
+            self.dir_url_json1 = "uri/"+dn.get_write_uri()+"?t=json"
+            self.dir_url_json2 = "uri/"+dn.get_write_uri()+"/?t=json"
+            self.dir_url_json_ro = "uri/"+dn.get_readonly_uri()+"/?t=json"
+            self.child_url = "uri/"+dn.get_readonly_uri()+"/child"
+        d.addCallback(_get_dircap)
+        d.addCallback(lambda ign: self.GET(self.dir_url_base, followRedirect=True))
+        d.addCallback(lambda body: self.failUnlessIn("<html>", body))
+        d.addCallback(lambda ign: self.GET(self.dir_url_json1))
+        d.addCallback(lambda res: simplejson.loads(res))  # just check it decodes
+        d.addCallback(lambda ign: self.GET(self.dir_url_json2))
+        d.addCallback(lambda res: simplejson.loads(res))  # just check it decodes
+        d.addCallback(lambda ign: self.GET(self.dir_url_json_ro))
+        d.addCallback(lambda res: simplejson.loads(res))  # just check it decodes
+        d.addCallback(lambda ign: self.GET(self.child_url))
+        d.addCallback(lambda body: self.failUnlessEqual(DATA, body))
+
+        def _block_dir(ign):
+            f = open(fn, "w")
+            f.write("%s %s\n" % (self.dir_si_b32, "dir-off-limits to you"))
+            f.close()
+            self.g.clients[0].blacklist.last_mtime -= 2.0
+        d.addCallback(_block_dir)
+        d.addCallback(lambda ign: self.shouldHTTPError("get_from_blacklisted_dir base",
+                                                       403, "Forbidden",
+                                                       "Access Prohibited: dir-off-limits",
+                                                       self.GET, self.dir_url_base))
+        d.addCallback(lambda ign: self.shouldHTTPError("get_from_blacklisted_dir json1",
+                                                       403, "Forbidden",
+                                                       "Access Prohibited: dir-off-limits",
+                                                       self.GET, self.dir_url_json1))
+        d.addCallback(lambda ign: self.shouldHTTPError("get_from_blacklisted_dir json2",
+                                                       403, "Forbidden",
+                                                       "Access Prohibited: dir-off-limits",
+                                                       self.GET, self.dir_url_json2))
+        d.addCallback(lambda ign: self.shouldHTTPError("get_from_blacklisted_dir json_ro",
+                                                       403, "Forbidden",
+                                                       "Access Prohibited: dir-off-limits",
+                                                       self.GET, self.dir_url_json_ro))
+        d.addCallback(lambda ign: self.shouldHTTPError("get_from_blacklisted_dir child",
+                                                       403, "Forbidden",
+                                                       "Access Prohibited: dir-off-limits",
+                                                       self.GET, self.child_url))
+        return d
+
+
+class CompletelyUnhandledError(Exception):
+    pass
+class ErrorBoom(rend.Page):
+    def beforeRender(self, ctx):
+        raise CompletelyUnhandledError("whoops")