

# NOTE: delete all output directories (on the development site, NOT the live site) before running to make sure that
# directory creation works.

# NOTE: order of execution:  
# Subclasses of TestCase are executed first.
# See https://docs.djangoproject.com/en/3.2/topics/testing/overview/
# Then I subclass TestCase in order to make sure necessary preliminaries in TestCase are done before  the subclass is run.  See 'TestSetup'.
# Within a class, only defs starting with lower-case 'test' are executed, so the order of execution within a class
# may be controlled by making defs an uppercase 'Test...', then calling those defs with a def of lowercase 'test' in the same class.


import pdb
from pprint import pprint

import re
import time
from datetime import timedelta 
from django.utils import timezone 

from django.test import TestCase, Client, RequestFactory
from django.contrib.auth.models import User
import json
from django.dispatch import receiver
from django.contrib.auth.signals import user_logged_out
from django.contrib.sessions.middleware import SessionMiddleware
from django.conf import settings
import random, sys

from .models import Mapmember, Map_desc, WhosLoggedIn, UserProfile, Node 
from .views  import *

from ideatree.utils import myutils 




class TestMyUtils(TestCase):

  def test_rStrChop(self):
    str1 = 'test'
    chopThis = ' ,'
    result = myutils.rStrChop(str1 + chopThis , chopThis)
    self.assertEqual(result, str1)

  def test_tostr(self):
    s = None
    res = myutils.tostr(s)
    self.assertEqual(res, '')
    s = ' a string' 
    res = myutils.tostr(s)
    self.assertEqual(res, s)
    s = 123 
    res = myutils.tostr(s)
    self.assertEqual(res, '123')


  def test_millisNow(self):
    ans = myutils.millisNow(offsetSeconds=0, decimalPoints=0)
    now = int(round(time.time() * 1000))
    self.assertAlmostEqual(ans, now, delta=10)


  def test_not_guest_user(self):
    guestuser = User.objects.create_user(username=settings.GUEST_USERNAME, password=settings.GUEST_PASSWORD)
    res = not_guest_user(guestuser)
    self.assertEqual(res, False)

  def test_nodeShapes(self):
    shapes = nodeShapes(getIndexFor=False)
    self.assertTrue(len(shapes) > 0)
    # FIXME: is there an assertVariableTypeIsList?
    #shapes = nodeShapes(getIndexFor='bartsimpson')
    # FIXME; assert exception = ValueError





# See https://stackoverflow.com/questions/27841101/can-not-log-in-with-unit-test-in-django-allauth
# And https://docs.djangoproject.com/en/2.0/topics/testing/tools/
username = 'ron.newman@gmail.com'
password = 'costner3'
clnt = Client(enforce_csrf_checks=False) # using Client instead of test_client because it bypasses csrf checking by default


class TestSetup(TestCase):

  def setUp(self):
    self.myMaps = []
    self.user = User.objects.create_user(username, password=password)
    UserProfile.objects.filter(pk=self.user.id).update(accounttype=settings.FREE_ACCT)
    #logged_in = client.login(username=username, password=password)
    logged_in = clnt.login(username=username, password=password)
    self.assertTrue(logged_in)
    #self.session = client.session
    self.session = clnt.session
    self.session.save()


  def add_session_to_request(self, xtra=None):
    factory = RequestFactory()
    self.request = factory.get('/ideatree/')
    self.request.user = self.user
    """Annotate a request object with a session"""
    self.request.session = self.session
    self.request.session['username']= self.user.username
    if xtra:
      for key in xtra.keys():
        self.request.session[key]= xtra[key] 
    self.request.session.save()
    from ideatree.utils import initPermissionsTable
    initPermissionsTable.initPermsTable(self.request) # set up action permissions table



# The vast majority of tests are in this class.  See the last def: test_makeMap_then_openMap_then_map_functions()
# FIXME: break out into separate classes as much as possible given dependencies.
class TestDefsInViews(TestSetup):

  def TestCheckPermissionsWithAccess(self): # NOTE: requires at least node exists
    # With non-existent node
    kwargs = {'nodeID':1000}
    self.assertRaisesRegex(Exception, "Node matching query does not exist.", checkPermissions, self.request, self.user.id, "createNode", self.mapId, **kwargs )

    # With non-existent edge 
    kwargs = {'edgeID':1000}
    self.assertRaisesRegex(Exception, "Edge matching query does not exist.", checkPermissions, self.request, self.user.id, "createEdge", self.mapId, **kwargs )

    # FIXME write test with a map that I don't own.
    # Another possible test is with valid node and edge Ids but non-existent map.

    # With non-existent command 
    aNodeId = Node.objects.filter(status=settings.NODE_ACTIVE).values_list('id',flat=True)[0]
    kwargs = {'nodeID':aNodeId}
    self.assertRaisesRegex(Warning, settings.NOT_PERMITTED_PROMPT, checkPermissions, self.request, self.user.id, "fuckYourself", self.mapId, **kwargs )


    #FIXME def test_checkPermissionsWithNOAccess(submitterID,methodName,mapId,nodeID=None,edgeID=None,ownerID=None):

    #FIXME def test_checkPermissionsReadOnlyUser(submitterID,methodName,mapId,nodeID=None,edgeID=None,ownerID=None):


  # a helper function (FIXME: why does it have asserts?)
  def makeedge(self, originId, targetId, label=None, cost=None, returnResponse=False):
    # Make the provisional for the EDGE.
    response = clnt.post('/ideatree/map/createNewEntityProvisional/', {'provisionalIdsRequested[node]':0, 'provisionalIdsRequested[edge]':1})
    self.assertEqual(response.status_code, 200)
    res = json.loads(response.content.decode("utf-8"))
    edgeId = int(res['edge'][0])
    # And the EDGE ITSELF.
    nowTime = myutils.millisNow(offsetSeconds=0, decimalPoints=0)
    transactionId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transactionId, nowTime)
    # TODO: this leaves out the _draw_ parameter.
    edgeParams = {'origin':originId, 'target':targetId, 'owner':self.user.id, 'color':'111111', 'penwidth':2.0, 'arrowhead':2 }
    if label:
      edgeParams["label"]=label
    if cost:
      edgeParams["cost"]=cost
    cm += json.dumps(makeCreateEdgeChangeMessageJSON(edgeId, edgeParams, forGraphvizLayout=False))
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    self.assertEqual(response.status_code, 200)
    if returnResponse:
      return(response)
    else:
      return(edgeId)


  # a helper function (FIXME: why does it have asserts?)
  def makenode(self, label=None, type=settings.ORIGINAL_NODE, url=None, description='test label'):
    nowTime = myutils.millisNow(offsetSeconds=0, decimalPoints=0)
    shape = 1
    style = 'filled,rounded'
    size = '0.5,0.5'
    pos = '1,1'
    fillcolor = '00ffff'
    pencolor= '111111'
    numcomments = 0
    lwidth = 0.5
    # FIXME: test with no creation time given
    nodeParams = {'nodetype':type, 'description':description, 'shape':shape, 'nodestyle':style , 'size':size, 'pos':pos, 'fillcolor':fillcolor, 'pencolor':pencolor, 'numcomments':numcomments, 'lwidth':lwidth, 'timestamp': nowTime, 'ownerID':self.user.id}
    if label:
      nodeParams["label"] = label
    if url:
      nodeParams["url"]=url

    # The node to be 'created' must be created first in the db in 'provisional' form.
    response = clnt.post('/ideatree/map/createNewEntityProvisional/', {'provisionalIdsRequested[node]':1, 'provisionalIdsRequested[edge]':0})
    res = json.loads(response.content.decode("utf-8"))
    nodeId = int(res['node'][0])

    transactionId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transactionId, nowTime)
    cm += json.dumps(makeCreateNodeChangeMessageJSON(nodeId, nodeParams, forGraphvizLayout=False))
    cm += ']}'

    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    self.assertEqual(response.status_code, 200)
    # Read node in database and verify its parameters:
    # FIXME: move these outside this helper function.
    #nd = Node.objects.get(pk=nodeId)
    #self.assertEqual(nd.nodetype,type)
    #self.assertEqual(nd.description,description)
    # FIXME: do more parameters
    
    return(nodeId)


  def deletenode(self,mapId,nodeId):
    nowTime = myutils.millisNow(offsetSeconds=0, decimalPoints=0)
    transactionId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transactionId, nowTime)
    cm += json.dumps(["removeNode", nodeId])
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    self.assertEqual(response.status_code, 200)
    self.assertEqual(json.loads(response.content)["result"], "S")
    self.assertEqual(lookup_node_info(nodeId, "status"), settings.NODE_DELETED)
    return(response)



  def TestAddSharer(self):
    friendusername = 'friendusername'
    friendpassword = 'friendpassword'
    self.fr = User.objects.create_user(username=friendusername, password=friendpassword)
    response = clnt.post('/ideatree/map/addUserToMap/', {"friend":self.fr.id})
    self.assertEqual(response.status_code, 200)

    # SubTest: check friends_of() to see if map sharer was added to the friends table.
    frnds = friends_of(self.user.id)
    afriend = [afr for afr in frnds][0]['friend_id']
    self.assertEqual(afriend, self.fr.id)
    isMember = map_membership(self.fr.id, self.mapId)
    isMemberEntries = isMember.count()
    self.assertEqual(isMemberEntries, 1)
    isMemberStatus = isMember[0].status
    self.assertEqual(isMemberStatus, settings.MAPMEMBERSHIP_ACTIVE)

    # SubTest: that we ourselves are in the map mambership table for the current map.
    myMember = map_membership(self.user.id, self.mapId)
    myMemberStatus = myMember[0].status
    self.assertEqual(myMemberStatus, settings.MAPMEMBERSHIP_ACTIVE)


  def TestNotifySharer(self):
    response = clnt.post('/ideatree/map/notify/', {'mapId':self.mapId})
    self.assertEqual(response.status_code, 200)
    self.assertRegex(response.content.decode("utf-8"), r"\bnotifyform\b")
    friendMember = Mapmember.objects.get(member_id=self.fr.id).id
    response = clnt.post('/ideatree/map/notify/', {'submitted':True, 'mapId':self.mapId, 'emailbody':'hello', 'users':[friendMember]})
    # Should receive a warning message becasue no email provided for the person to be notified.
    self.assertRegex(response.content.decode("utf-8"), r"\bNo email address\b")

    # Do it again with email provided this time.
    testemail = 'ron.newman@gmail.com'
    User.objects.filter(pk=self.fr.id).update(email=testemail)  # FIXME: how to update after a get()?
    response = clnt.post('/ideatree/map/notify/', {'submitted':True, 'mapId':self.mapId, 'emailbody':'hello', 'users':[friendMember]})
    self.assertEqual(response.status_code, 200)


  def TestRemoveSharer(self):
    response = clnt.post('/ideatree/map/removeFromMap/', {"userId":self.fr.id, 'mapId':self.mapId})
    isMember = map_membership(self.fr.id, self.mapId)
    isMemberStatus = isMember[0].status
    self.assertEqual(isMemberStatus, settings.MAPMEMBERSHIP_DELETED)


  def TestVote(self):
    # First, create a votable node to test with.
    votableNodeId = self.makenode(label='a votable node', type=settings.VOTABLE_NODE)

    #testNodetype = settings.VOTABLE_NODE
    #votableLabel = 'a votable node'
    #nodeParams = {'nodetype':testNodetype, 'label':votableLabel, 'ownerID':self.user.id}
    # The node to be 'created' must be created first in the db in 'provisional' form.
    #response = clnt.post('/ideatree/map/createNewEntityProvisional/', {'provisionalIdsRequested[node]':1, 'provisionalIdsRequested[edge]':0})
    #res = json.loads(response.content.decode("utf-8"))
    #votableNodeId = int(res['message']['node'][0])
    # Finally, test adding a vote
    testVoteLevel = 6
    response = clnt.post('/ideatree/map/vote/', {'node':votableNodeId , 'vote':testVoteLevel})
    self.assertEqual(response.status_code, 200)
    savedVoteLevel = Vote.objects.get(node_id=votableNodeId).vote
    totalVotes = Vote.objects.filter(node__ofmap=self.mapId).count()
    self.assertEqual(totalVotes, 1)
    self.assertEqual(savedVoteLevel, testVoteLevel)


  def TestTallyVote(self):
    response = clnt.post('/ideatree/map/tallyVotes/')
    self.assertEqual(response.status_code, 200)


  def TestMapSummary(self):
    response = clnt.post('/ideatree/map/mapSummary/')
    self.assertEqual(response.status_code, 200)
    self.assertEqual(len(self.myMaps), len(response.context["maps"]))
    for i in response.context["maps"]:
      self.assertTrue(i["id"] in self.myMaps)



  def test_incrementViewers(self): # this is in this class because it requires being logged in to a map 
    # map owner must be a User instance, can't be the test client
    firstMapViewed = Map_desc.objects.create(owner=self.user, mapname='Test Map').id
    #response = client.post('/ideatree/map/incrementViewers/', json.dumps({'mapId':firstMapViewed}), content_type='application/json', secure=True)
    response = clnt.post('/ideatree/map/incrementViewers/', json.dumps({'mapId':firstMapViewed}), content_type='application/json', secure=True)
    self.assertEqual(response.status_code, 200)

    secondMapViewed = Map_desc.objects.create(owner=self.user, mapname='Test Map2').id
    #response = client.post('/ideatree/map/incrementViewers/', json.dumps({'mapId':secondMapViewed}), content_type='application/json', secure=True)
    response = clnt.post('/ideatree/map/incrementViewers/', json.dumps({'mapId':secondMapViewed}), content_type='application/json', secure=True)
    self.assertEqual(response.status_code, 200)

     # NOTE: limitation: can only be logged in to one map at a time.
    numMapsNowBeingViewed = WhosLoggedIn.objects.filter(user_id=self.user).count()
    self.assertEqual(numMapsNowBeingViewed, 1)


  @receiver(user_logged_out)
  def test_decrementViewers(sender, **kwargs):
    numViewing = WhosLoggedIn.objects.filter(user=sender.user).count()
    sender.assertEqual(numViewing, 0)


  # FIXME: lookup again after some Profile info has been added
  def test_lookup_user_info(self):
    userId = self.user.id 
    result = lookup_user_info(userId, 'id')
    self.assertEqual(result, userId)


  def test_my_account_page(self):
    newFirstName = 'John'
    newLastName = 'Doe'
    testemail = 'ron.newman@gmail.com'  # email is required by the myAccount form 
    response = clnt.post('/ideatree/map/myAccount/', {'first_name':newFirstName, 'last_name':newLastName,'email':testemail,'submitted':True})
    self.assertEqual(response.status_code, 200)
    thisUser = User.objects.get(pk=self.user.id)
    self.assertEqual(newFirstName, thisUser.first_name)
    self.assertEqual(newLastName, thisUser.last_name)
    newFirstName = ''
    newLastName = ''
    response = clnt.post('/ideatree/map/myAccount/', {'first_name':newFirstName, 'last_name':newLastName,'email':testemail,'submitted':True})
    self.assertEqual(response.status_code, 200)
    thisUser = User.objects.get(pk=self.user.id)
    self.assertEqual(newFirstName, thisUser.first_name)
    self.assertEqual(newLastName, thisUser.last_name)


  def TestMakeMap(self):
    # This sets up the request (which is invisibly send with the post) with 'username', '_auth_user_id', etc.
    self.add_session_to_request()

    # Create two scratch maps for later tunnel testing.
    # First, make sure allowable maps is high enough.
    UserProfile.objects.filter(user__id=self.user.id).update(nummapsallowed=3) 
    testMap2Name = 'test map2'
    response = clnt.post('/ideatree/map/newMap/', {'mapname':testMap2Name,'submitted':True})
    self.assertEqual(response.status_code, 200)
    self.testMap2Id = Map_desc.objects.filter(mapname=testMap2Name).values_list('id',flat=True)[0]
    self.myMaps.append(self.testMap2Id) 
    self.assertEqual(settings.MAX_FREE_ACCOUNT_NODES_PER_MAP, lookup_map_info(self.testMap2Id, "nodesleft"))

    testMap3Name = 'test map3'
    response = clnt.post('/ideatree/map/newMap/', {'mapname':testMap3Name,'submitted':True})
    self.assertEqual(response.status_code, 200)
    self.testMap3Id = Map_desc.objects.filter(mapname=testMap3Name).values_list('id',flat=True)[0]
    self.myMaps.append(self.testMap3Id) 
    # FIXME: do submit with missing and corrupt fields.



    # TEST NEWMAP ----------------------------------------------------------------------
    # The main map we will test with.
    # First, check that the map creation dialog is retrieved
    # This does not actually create a map.
    response = clnt.post('/ideatree/map/newMap/', {'mapname':'test map'})
    self.assertEqual(response.status_code, 200)

    # Check that a new map can be created, by spoofing the submission of a new map form.
    self.mainTestMapName = 'main test map'
    response = clnt.post('/ideatree/map/newMap/', {'mapname':self.mainTestMapName,'submitted':True})
    self.assertEqual(response.status_code, 200)
    numMaps = Map_desc.objects.all().count()
    self.assertEqual(numMaps, 3) 
    mainTestMapId = Map_desc.objects.filter(mapname=self.mainTestMapName).values_list('id',flat=True)[0]
    self.myMaps.append(mainTestMapId) 

    # Set numMapsAllowed to two and check that it fails if a third map is added 
    UserProfile.objects.filter(user__id=self.user.id).update(nummapsallowed=2) 
    response = clnt.post('/ideatree/map/newMap/', {'mapname':'test map 3','submitted':True})
    self.assertNotEqual(response.status_code, 200)

    # Check that owned_maps is a subset of accessible_maps
    self.owned_maps = json.loads(response.wsgi_request.session['owned_maps'])
    accessible_maps = json.loads(response.wsgi_request.session['accessible_maps'])
    for omap in self.owned_maps:
      is_subset = False 
      for amap in accessible_maps:
        if omap.get('id') == amap.get('id'):
          is_subset = True
      if not is_subset:
        break
    self.assertTrue(is_subset)


  def TestOpenMap(self):
    nowTime = timezone.now() # NOTE: depends on TZ=True in settings.py!!
    trialEndTime = nowTime - timedelta(days=settings.FREE_ACCT_TRIALPERIOD_DAYS + 1)
    UserProfile.objects.filter(pk=self.user.id).update(registerdate=trialEndTime) 

    response = clnt.post('/ideatree/map/openMap/')
    self.assertEqual(response.status_code, 200)
    showExpirationPrompt = response.context.get('showExpirationPrompt')
    self.assertTrue(showExpirationPrompt)

    mainTestMapId = Map_desc.objects.filter(mapname=self.mainTestMapName).values_list('id',flat=True)[0]
    self.mapId = mainTestMapId 
    self.session["mapname"] = self.mainTestMapName
    self.session.save()
    response = clnt.post('/ideatree/map/openMap/',{'mapId':self.mapId})  # check opening map from the session
    self.assertEqual(response.status_code, 200)

    # test def lookup_map_info()
    mapinfo = lookup_map_info(self.mapId, "mapname")
    self.assertEqual(mapinfo, self.mainTestMapName)



  def TestOpenMapForReadonlyUser(self):
    # FIXME
    # put 'readOnly' in request.session to trigger this test
    #'Open Map not available for read only accounts.'
    pass


    # test all_accessible_maps
  def TestListAllAccessibleMaps(self):
    self.add_session_to_request(xtra={'mapId':self.mapId})
    allmine = all_accessible_maps(self.request, thisUser=self.user.id, exclude_current=False)
    includesThisMap = self.mapId in allmine.values_list('id',flat=True) 
    self.assertTrue(includesThisMap)
    allmine = all_accessible_maps(self.request, thisUser=self.user.id, exclude_current=True)
    includesThisMap = self.mapId in allmine.values_list('id',flat=True) 
    self.assertFalse(includesThisMap)



  def TestEditUnopenedMapName(self):
    response = clnt.post('/ideatree/map/editMapName/')
    self.assertRegex(response.content.decode("utf-8"), settings.PLEASE_OPEN_GRAPH_PROMPT) 


  def TestEditMapName(self):
    response = clnt.post('/ideatree/map/editMapName/', {'submitted':False}) # This will return an html user form.
    self.assertEqual(response.status_code, 200)
    response = clnt.post('/ideatree/map/editMapName/', {'submitted':'0'})  # This will return an html user form.
    self.assertEqual(response.status_code, 200)
    testMapName = 'some new map name'
    response = clnt.post('/ideatree/map/editMapName/', {'mapname':testMapName ,'submitted':'1'})
    self.assertEqual(response.status_code, 200)
    mapId = re.search(r"\bdata-mapid=\"([0-9]+)\b",response.content.decode("utf-8")).group(1)
    self.assertEqual(testMapName, Map_desc.objects.get(pk=int(mapId)).mapname)
    self.assertRegex(response.content.decode("utf-8"), r"\bSuccess\b")


  def TestEditMapNameWithNoAccess(self):
    pass

  def TestDeleteUnopenedMap(self):
    #self.assertRegex(response.content.decode("utf-8"), settings.PLEASE_OPEN_GRAPH_PROMPT) 
    pass
    # FIXME
    #response = clnt.post('/ideatree/map/deleteMap/')
    #self.assertRegex(response.content.decode("utf-8"), settings.PLEASE_OPEN_GRAPH_PROMPT) 

  def TestDeleteMapWithNoAccess(self):
    #self.assertRegex(response.content.decode("utf-8"), settings.PLEASE_OPEN_GRAPH_PROMPT) 
    # FIXME
    pass

  def TestDeleteMapWithNodesPresent(self):
    # settings.DELETE_AFTER_GRAPH_EMPTY_PROMPT 
    # FIXME
    pass
    #response = clnt.post('/ideatree/map/deleteMap/')

  def TestDeleteMap(self):
    # FIXME: do submit with missing and corrupt fields.
    pass
    #response = clnt.post('/ideatree/map/deleteMap/')


  def TestDemoMap(self):
    tempuser = self.user
    self.user = User.objects.create_user(settings.GUEST_USERNAME, password=settings.GUEST_PASSWORD)
    self.add_session_to_request()  # Just to get a populated request
    response = clnt.post('/ideatree/demoMap/')
    self.assertEqual(response.status_code, 302) # redirects to open map
    self.user = tempuser  # restore state


  def TestSearchUsers(self): 
    # Just get the form:
    response = clnt.post('/ideatree/map/searchUsers/', {"doSearch":False})
    # Emulate a form submit:
    testusername = 'testusername'
    testpassword = 'testpassword'
    u = User.objects.create_user(username=testusername, password=testpassword)
    response = clnt.post('/ideatree/map/searchUsers/', {"doSearch":True, 'searchTerm':testusername, 'searchType':'username'})
    self.assertEqual(response.status_code, 200)
    self.assertRegex(response.content.decode("utf-8"), r"\btestusername\b")
    self.assertEqual(response.context["numUsersFound"], 1)


  def TestChangeMessageHandler(self):
    self.add_session_to_request(xtra={'mapId':self.mapId})
    nowTime = myutils.millisNow(offsetSeconds=0, decimalPoints=0)

    # TEST SETMAPATTRIBUTE --------------------------------------------------------------
    transactionId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transactionId, nowTime)
    testMapName = "Ron Newman's map"
    cm += json.dumps(["setMapAttribute", "mapname", testMapName])
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    savedMapName = Map_desc.objects.get(pk=self.mapId).mapname
    self.assertEqual(savedMapName, testMapName)
    # many other parameters possible to test


    # TEST SAVEMAPSETTINGS() ---------------------------------------------------------------
    testmapsettings = json.dumps({'view.nodeMaximization.smooth':True}) # Make sure this value is not the default.
    response = clnt.post('/ideatree/map/saveMapSettings/', {'mapsettings':testmapsettings})
    savedSettings = UserProfile.objects.get(user_id=self.user.id).mapsettings
    self.assertEqual(testmapsettings, savedSettings)


    # TEST CREATENODE -----------------------------------------------------------------------
    # FIXME: use helper function makeNode instead of creating it again here.
    # FIXME: test with crazy label strings
    testNodetype = settings.ORIGINAL_NODE
    firstNodeLabel = 'first node label'
    testDescription = 'a description' # FIXME: add all the other parameters for checking below
    testShape = 1
    testNodestyle = 'filled,rounded'
    testSize = '0.50,0.50'
    testPos = '1,1'
    testFillcolor = '00ffff'
    testPencolor= '111111'
    testNumcomments = 0
    testLwidth = 0.5
    testURL = 'www.ideatree.net'
    # FIXME: test with no creation time given
    nodeParams = {'nodetype':testNodetype, 'label':firstNodeLabel, 'description':testDescription, 'shape':testShape, 'nodestyle':testNodestyle , 'size':testSize, 'pos':testPos, 'fillcolor':testFillcolor, 'pencolor':testPencolor, 'numcomments':testNumcomments, 'lwidth':testLwidth, 'url':testURL, 'timestamp': nowTime, 'ownerID':self.user.id}

    # The node to be 'created' must be created first in the db in 'provisional' form.
    response = clnt.post('/ideatree/map/createNewEntityProvisional/', {'provisionalIdsRequested[node]':1, 'provisionalIdsRequested[edge]':0})
    res = json.loads(response.content.decode("utf-8"))
    testNodeId = int(res['node'][0])

    transactionId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transactionId, nowTime)
    cm += json.dumps(makeCreateNodeChangeMessageJSON(testNodeId, nodeParams, forGraphvizLayout=False))
    cm += ']}'

    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    # verify that the map isn't full:
    if "message" in json.loads(response.content.decode("utf-8")):
      self.assertNotEqual(json.loads(response.content.decode("utf-8"))["message"], settings.MAP_FULL_PROMPT ) 
    self.assertEqual(response.status_code, 200)
    self.assertRegex(json.loads(response.content.decode("utf-8"))["result"], "S") 

    savedOfmap = lookup_node_info(testNodeId, "ofmap")
    savedOwner = lookup_node_info(testNodeId, "owner")
    savedNodetype = lookup_node_info(testNodeId, "nodetype")
    savedLabel = lookup_node_info(testNodeId, "label")
    savedDescription = lookup_node_info(testNodeId, "description")
    savedShape = lookup_node_info(testNodeId, "shape")
    savedNodestyle = lookup_node_info(testNodeId, "style")
    savedSize = lookup_node_info(testNodeId, "size")
    savedXPos = lookup_node_info(testNodeId, "xpos")
    savedYPos = lookup_node_info(testNodeId, "ypos")
    testXPos = float(testPos.split(',')[0])
    testYPos = float(testPos.split(',')[1])
    savedFillcolor = lookup_node_info(testNodeId, "fillcolor")
    savedPencolor = lookup_node_info(testNodeId, "pencolor")
    savedNumcomments = lookup_node_info(testNodeId, "numcomments")
    savedLwidth = lookup_node_info(testNodeId, "lwidth")
    savedURL = lookup_node_info(testNodeId, "url")
    savedStatus = lookup_node_info(testNodeId, "status")

    self.assertEqual(savedOfmap, self.mapId)
    self.assertEqual(savedOwner, self.user.id)
    self.assertEqual(savedLabel, firstNodeLabel)
    self.assertEqual(savedDescription, testDescription)
    self.assertEqual(savedShape, testShape)
    self.assertEqual(savedNodestyle, testNodestyle )
    self.assertEqual(savedSize, testSize)
    self.assertEqual(savedXPos, testXPos)
    self.assertEqual(savedYPos, testYPos)
    self.assertEqual(savedFillcolor, testFillcolor)
    self.assertEqual(savedPencolor, testPencolor)
    self.assertEqual(savedNumcomments, testNumcomments)
    self.assertTrue(savedURL in [testURL, 'http://' + str(testURL), 'https://' + str(testURL)])
    self.assertEqual(savedStatus, settings.NODE_ACTIVE)
    self.assertEqual(savedNodetype, testNodetype)

    # NOTE: lots of other attributes could be tested here

    # FIXME: check for missing label when url is provided on new node.

    # TEST SETNODEATTRIBUTE ----------------------------------------------------------
    testPos = "0,0"
    transactionId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transactionId, nowTime)
    cm += json.dumps(makeSetNodeAttributeChangeMessage(testNodeId, testPos, "pos"))
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    self.assertEqual(response.status_code, 200)
    # Check saved pos
    savedXPos = lookup_node_info(testNodeId, "xpos")
    savedYPos = lookup_node_info(testNodeId, "ypos")
    testXPos = float(testPos.split(',')[0])
    testYPos = float(testPos.split(',')[1])
    self.assertEqual(savedXPos, testXPos)
    self.assertEqual(savedYPos, testYPos)


    # TEST CRUDNODECOMMENTS --------------------------------------------------------
    savedNumcomments = lookup_node_info(testNodeId, "numcomments")
    self.assertEqual(savedNumcomments, 0)
    testComment = "newly created comment"
    response = clnt.post('/ideatree/map/nodeComment/', {'nodeID':testNodeId, 'submitNewComment':True, 'comment':testComment})
    self.assertEqual(response.status_code, 200)
    savedNumcomments = lookup_node_info(testNodeId, "numcomments")
    self.assertEqual(savedNumcomments, 1)
    savedComment = NodeComment.objects.filter(node=testNodeId).values_list('comment', flat=True)[0]
    self.assertEqual(savedComment, testComment)
    # FIXME: do submit with missing and corrupt fields.

    # TEST UPDATE COMMENT --------------------------------------------------------
    updatedTestComment = "updated test comment"
    savedCommentId = NodeComment.objects.filter(node=testNodeId)[0].id
    response = clnt.post('/ideatree/map/nodeComment/', {'nodeID':testNodeId, 'submitToUpdate':True, 'comment':updatedTestComment, 'commentId':savedCommentId})
    self.assertEqual(response.status_code, 200)
    savedComment = NodeComment.objects.filter(node=testNodeId).values_list('comment', flat=True)[0]
    self.assertEqual(savedComment, updatedTestComment)

    # TEST COMMENTSUMMARY --------------------------------------------------------
    response = clnt.post('/ideatree/map/commentSummary/')
    self.assertEqual(response.status_code, 200)
    self.assertRegex(response.content.decode("utf-8"), r"\bupdated test comment\b")

    # TEST DELETE COMMENT --------------------------------------------------------
    response = clnt.post('/ideatree/map/nodeComment/', {'nodeID':testNodeId, 'reallyDelete':True, 'commentId':savedCommentId})
    deleteStatus = NodeComment.objects.get(pk=savedCommentId).status
    self.assertEqual(deleteStatus, settings.NODE_COMMENT_DELETED)  # FIXME: check that this was done when the node is deleted, too.


    # TEST CREATEEDGE -----------------------------------------------------
    # Prepare by creating a SECOND NODE to edge to.
    response = clnt.post('/ideatree/map/createNewEntityProvisional/', {'provisionalIdsRequested[node]':1, 'provisionalIdsRequested[edge]':0})
    self.assertEqual(response.status_code, 200)
    res = json.loads(response.content.decode("utf-8"))
    testNode2Id = int(res['node'][0])
    transactionId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transactionId, nowTime)
    cm += json.dumps(makeCreateNodeChangeMessageJSON(testNode2Id, nodeParams, forGraphvizLayout=False))
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    self.assertEqual(response.status_code, 200)
    # Now the provisional for the EDGE.
    response = clnt.post('/ideatree/map/createNewEntityProvisional/', {'provisionalIdsRequested[node]':0, 'provisionalIdsRequested[edge]':1})
    self.assertEqual(response.status_code, 200)
    res = json.loads(response.content.decode("utf-8"))
    testEdgeId = int(res['edge'][0])
    # And the EDGE ITSELF.
    transactionId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transactionId, nowTime)
    # TODO: this leaves out the _draw_ parameter.
    testEdgeLabel = 'test edge label'
    edgeParams = {'origin':testNodeId, 'target':testNode2Id, 'owner':self.user.id, 'label':testEdgeLabel, 'color':'111111', 'cost':10, 'penwidth':2.0, 'arrowhead':2 }
    cm += json.dumps(makeCreateEdgeChangeMessageJSON(testEdgeId, edgeParams, forGraphvizLayout=False))
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    self.assertEqual(response.status_code, 200)
    # See if values were saved correctly.
    # Status should automatically be set to active. 
    edgeStatus = Edge.objects.get(pk=testEdgeId).status
    self.assertEqual(edgeStatus, settings.EDGE_ACTIVE)
    edgeLabel = Edge.objects.get(pk=testEdgeId).label
    self.assertEqual(edgeLabel, testEdgeLabel)
    # checking of other parameters possible.


    # TEST SETEDGEATTRIBUTE ------------------------------------------------
    testEdgeColor = "0000eeff"
    transactionId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transactionId, nowTime)
    cm += json.dumps(["setEdgeAttribute", testEdgeId, "color", testEdgeColor])
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    self.assertEqual(response.status_code, 200)
    # And check that the moved node has its cluster parameter set.
    edgeColor = Edge.objects.get(pk=testEdgeId).color
    self.assertEqual(edgeColor, testEdgeColor)


    # TEST MINIMIZE BRANCH ------------------------------------------------------
    transactionId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transactionId, nowTime)
    cm += json.dumps(["setEdgeAttribute", testEdgeId, "hiddenbranch", 1])
    cm += ', '
    cm += json.dumps(["setNodeAttribute", testNode2Id, "hiddenbranch", 1])
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    self.assertEqual(response.status_code, 200)
    minimizedEdgeBranch = Edge.objects.get(pk=testEdgeId).hiddenbranch
    self.assertEqual(minimizedEdgeBranch, 1) # NOTE: the value is 1 only because it's the first minimized branch we've constructed
    minimizedNodeBranch = Node.objects.get(pk=testNode2Id).hiddenbranch
    self.assertEqual(minimizedNodeBranch, 1) # NOTE: the value is 1 only because it's the first minimized branch we've constructed
    # NOTE: no use to test an extended branch, since the javascript client figures it out and just sends setxxxAttribute changes.


    # TEST DUPLICATE TRANSACTION ID SHOULD FAIL ------------------------------------------------------
    firstcolor = 'eeeeee'
    cm = makeTransactionHeaderJSON(self.mapId, '1', nowTime)
    cm += json.dumps(["setNodeAttribute", testNode2Id, "fillcolor", firstcolor])
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    self.assertEqual(response.status_code, 200)
    self.assertEqual(firstcolor, lookup_node_info(testNode2Id, "fillcolor"))
    # Now do it again with the same transaction ID, it should fail.
    secondcolor = 'bbbbbb'
    cm = makeTransactionHeaderJSON(self.mapId, '1', nowTime)
    cm += json.dumps(["setNodeAttribute", testNode2Id, "fillcolor", secondcolor])
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    self.assertEqual(response.status_code, 200)
    self.assertNotEqual(secondcolor, lookup_node_info(testNode2Id, "fillcolor"))


    # TEST UNIQUE BUT OUT OF ORDER TRANSACTION SHOULD FAIL ------------------------------------------------------
    thirdcolor = 'cccccc'
    transId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transId, nowTime-1000)
    cm += json.dumps(["setNodeAttribute", testNode2Id, "fillcolor", thirdcolor ])
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    self.assertEqual(response.status_code, 200)
    self.assertNotEqual(secondcolor, lookup_node_info(testNode2Id, "fillcolor"))


    # TEST CHANGE NODE COLOR ------------------------------------------------------
    newcolor = '999999'
    transId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transId, nowTime)
    cm += json.dumps(["setNodeAttribute", testNode2Id, "fillcolor", newcolor])
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    self.assertEqual(response.status_code, 200)
    self.assertEqual(newcolor, lookup_node_info(testNode2Id, "fillcolor"))


    # TEST PUTNODEINCLUSTER -----------------------------------------------------
    # First make a node that serves as the cluster 
    clusterLabel = 'A Cluster'
    # TODO: other cluster parameters could be checked here.
    # The node to be 'created' must be created first in the db in 'provisional' form.
    response = clnt.post('/ideatree/map/createNewEntityProvisional/', {'provisionalIdsRequested[node]':1, 'provisionalIdsRequested[edge]':0})
    res = json.loads(response.content.decode("utf-8"))
    clusterNodeId = int(res['node'][0])
    clusterNodeParams = {'nodetype':settings.CLUSTER, 'label':clusterLabel, 'ownerID':self.user.id}
    transactionId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transactionId, nowTime)
    cm += json.dumps(makeCreateNodeChangeMessageJSON(clusterNodeId, clusterNodeParams, forGraphvizLayout=False))
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    self.assertEqual(response.status_code, 200)
    clusterType = lookup_node_info(clusterNodeId, "nodetype")
    self.assertEqual(clusterType, settings.CLUSTER)
    # Then move an existing node into the cluster.
    transactionId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transactionId, nowTime)
    cm += json.dumps(["putNodeInCluster", testNode2Id, clusterNodeId])
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    self.assertEqual(response.status_code, 200)
    # And check that the moved node has its cluster parameter set.
    node2pointerToCluster = lookup_node_info(testNode2Id, "clusterid")
    self.assertEqual(node2pointerToCluster, clusterNodeId)


    # TEST REMOVENODE
    deletedClusterResult= self.deletenode(self.mapId,clusterNodeId)
    # IMPORTANT NOTE: Since it was a cluster, it should also remove the enclosed node and the edge to that node,
    # but THIS IS DONE IN CLIENT JAVASCRIPT, which generates the necessary change message.
    # FIXME: seems easier and more reliable (and testable) to do it on the server, in code or through Postgres constraints.
    # Also, it's inconsistent that edges are deleted by the server when a connected node is, but nodes in clusters are not handled the same way.
    # If/When that's done, then:
    #deletedNode2Status = lookup_node_info(testNode2Id, "status")
    #self.assertEqual(deletedNode2Status, settings.NODE_DELETED)
    #deletedEdgeStatus = Edge.objects.get(pk=testEdgeId).status
    #self.assertEqual(deletedEdgeStatus, settings.EDGE_DELETED)

    # NOTE: if the above automatic server-side cluster removal is implemented the enclosed node and the edge to it
    # will have to be recreated before doing the following test.
    # TEST REMOVEEDGE 
    transactionId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transactionId, nowTime)
    cm += json.dumps(["removeEdge", testEdgeId])
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    self.assertEqual(response.status_code, 200)
    deletedEdgeStatus = Edge.objects.get(pk=testEdgeId).status
    self.assertEqual(deletedEdgeStatus, settings.EDGE_DELETED)

    # TEST AUTOMATIC REMOVEEDGE 
    # Now re-create the edge so we can see if it's automatically deleted when its target node is deleted.
    # First the provisional for the edge.
    response = clnt.post('/ideatree/map/createNewEntityProvisional/', {'provisionalIdsRequested[node]':0, 'provisionalIdsRequested[edge]':1})
    self.assertEqual(response.status_code, 200)
    res = json.loads(response.content.decode("utf-8"))
    testEdgeId = int(res['edge'][0])
    # And the edge itself.
    transactionId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transactionId, nowTime)
    testEdgeLabel = 'test edge label'
    edgeParams = {'origin':testNodeId, 'target':testNode2Id, 'owner':self.user.id, 'label':testEdgeLabel, 'color':'111111', 'cost':10, 'penwidth':2.0, 'arrowhead':2 }
    cm += json.dumps(makeCreateEdgeChangeMessageJSON(testEdgeId, edgeParams, forGraphvizLayout=False))
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    self.assertEqual(response.status_code, 200)
    # Now we can delete the target node of the edge.
    response = self.deletenode(self.mapId,testNode2Id)
    # And check that the edge to that node was automatically deleted.
    edgeStatus = Edge.objects.get(pk=testEdgeId).status
    self.assertEqual(edgeStatus, settings.EDGE_DELETED)
    # Just to be sure, check the target node, too.
    deletedNodeStatus = lookup_node_info(testNode2Id, "status")
    self.assertEqual(deletedNodeStatus, settings.NODE_DELETED)


    # TEST CREATING A TUNNEL NODE. -------------------------------------------
    testTunnelType = settings.TUNNEL_NODE
    #testTunnelLabel = 'tunnel origin'
    # FIXME: test with a really long mapname to see if tunnel label works based on that mapname.
    #tunnelNodeParams = {'nodetype':testTunnelType, 'label':testTunnelLabel, 'tunnelfarendmap':self.testMap2Id}
    tunnelNodeParams = {'nodetype':testTunnelType, 'tunnelfarendmap':self.testMap2Id}
    # The tunnel node to be 'created' must be created first in the db in 'provisional' form.
    response = clnt.post('/ideatree/map/createNewEntityProvisional/', {'provisionalIdsRequested[node]':1, 'provisionalIdsRequested[edge]':0})
    res = json.loads(response.content.decode("utf-8"))
    testTunnelId = int(res['node'][0])

    transactionId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transactionId, nowTime)
    cm += json.dumps(makeCreateNodeChangeMessageJSON(testTunnelId, tunnelNodeParams, forGraphvizLayout=False))
    cm += ']}'
    # Thsn have the provisional tunnel node's parameters filled in and status changed to ACTIVE..
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    self.assertEqual(response.status_code, 200)

    totalTunnelNodes = Node.objects.filter(nodetype=settings.TUNNEL_NODE).count() 
    self.assertEqual(totalTunnelNodes, 1) # one tunnel origin node. No tunnel target node


    # TEST REPOSITIONING A TUNNEL NODE FROM ONE TARGET MAP TO ANOTHER -------------------------------------------
    response = clnt.post('/ideatree/map/createNewEntityProvisional/', {'provisionalIdsRequested[node]':1, 'provisionalIdsRequested[edge]':0})
    res = json.loads(response.content.decode("utf-8"))
    testNodeId = int(res['node'][0])
    # Emulate a user submitting the form.  No tunnel target, should fail.
    response = clnt.post('/ideatree/map/nodeDialog/', {'nodetype':settings.TUNNEL_NODE, 'submitted':True})
    self.assertTrue(len(response.context["form"].errors["tunnelfarendmap"]) > 0) # FIXME a little awkward
    # We'll do it direct, without the form, for simplicity.  Has tunnel target this time, should succeed.
    testMaps = [i for i in self.myMaps if i != self.mapId]
    targetMapOne = testMaps[0] 
    targetMapTwo = testMaps[1] 
    self.assertTrue(len(self.myMaps) > 2)
    nodeParams = {'nodetype':settings.TUNNEL_NODE, 'tunnelfarendmap':targetMapOne}
    transactionId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transactionId, nowTime)
    cm += json.dumps(makeCreateNodeChangeMessageJSON(testNodeId, nodeParams, forGraphvizLayout=False))
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    self.assertEqual(response.status_code, 200)
    newTunnel = Node.objects.filter(id=testNodeId, status=settings.NODE_ACTIVE, nodetype=settings.TUNNEL_NODE)
    self.assertTrue(newTunnel.exists())
    # Now reposition to tunnel to point to a different target map.
    targetMapName = Map_desc.objects.get(pk=targetMapTwo).mapname
    transactionId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transactionId, nowTime)
    cm += json.dumps(["setNodeAttribute", testNodeId, "tunnelfarendmap", targetMapTwo])
    cm += ', '
    cm += json.dumps(["setNodeAttribute", testNodeId, "tunnel", targetMapTwo])
    cm += ', '
    cm += json.dumps(["setNodeAttribute", testNodeId, "targetMapName", targetMapName])
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    self.assertEqual(response.status_code, 200)


    
    # TEST UNDO OF TUNNEL DELETION:  Delete a tunnel in the origin map. Undo in the origin map should bring back the tunnel in the target map.
    # FIXME


    # TEST DOING SOMETHING COMPLICATED, INDUCING FAILURE, AND CHECKING FOR TRANSACTION ROLLBACK
    # active, in case the garbage collector, running as a cron job, happens to run during this test and removed nodes with status 'deleted'
    total_Tunnel_Nodes_BEFORE_Rollback = Node.objects.filter(status=settings.NODE_ACTIVE, nodetype=settings.TUNNEL_NODE).count()
    total_Original_Nodes_BEFORE_Rollback = Node.objects.filter(status=settings.NODE_ACTIVE, nodetype=settings.ORIGINAL_NODE).count() 
    available_Nodes_BEFORE_Rollback = Map_desc.objects.get(pk=self.mapId).nodesleft
    testTunnelType = settings.TUNNEL_NODE
    bogusUserId = 2000
    tunnelNodeParams = {'nodetype':testTunnelType, 'tunnelfarendmap':self.testMap2Id}
    # The tunnel node to be 'created' must be created first in the db in 'provisional' form.
    response = clnt.post('/ideatree/map/createNewEntityProvisional/', {'provisionalIdsRequested[node]':2, 'provisionalIdsRequested[edge]':0})
    res = json.loads(response.content.decode("utf-8"))
    testTunnelId = int(res['node'][0])
    testThrowawayNodeId = int(res['node'][1])
    transactionId = str(random.randint(1,sys.maxsize))
    # Make the last thing fail to see if complicated stuff before it is rolled back.
    cm = makeTransactionHeaderJSON(self.mapId, transactionId, nowTime)
    cm += json.dumps(makeCreateNodeChangeMessageJSON(testTunnelId, tunnelNodeParams, forGraphvizLayout=False))
    cm += json.dumps(makeCreateNodeChangeMessageJSON(testThrowawayNodeId, {'nodetype':settings.ORIGINAL_NODE, 'ownerID':bogusUserId}))
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    total_Tunnel_Nodes_AFTER_Rollback = Node.objects.filter(status=settings.NODE_ACTIVE, nodetype=settings.TUNNEL_NODE).count() 
    total_Original_Nodes_AFTER_Rollback = Node.objects.filter(status=settings.NODE_ACTIVE, nodetype=settings.ORIGINAL_NODE).count() 
    available_Nodes_AFTER_Rollback = Map_desc.objects.get(pk=self.mapId).nodesleft
    self.assertEqual(total_Tunnel_Nodes_AFTER_Rollback, total_Tunnel_Nodes_BEFORE_Rollback)
    self.assertEqual(total_Original_Nodes_BEFORE_Rollback, total_Original_Nodes_AFTER_Rollback)
    self.assertEqual(available_Nodes_AFTER_Rollback, available_Nodes_BEFORE_Rollback)

    # TEST mistakenly giving the current, originating map as the target of a tunnel.  Should fail.
    # The tunnel node to be 'created' must be created first in the db in 'provisional' form.
    response = clnt.post('/ideatree/map/createNewEntityProvisional/', {'provisionalIdsRequested[node]':1, 'provisionalIdsRequested[edge]':0})
    res = json.loads(response.content.decode("utf-8"))
    testTunnelId = int(res['node'][0])
    transactionId = str(random.randint(1,sys.maxsize))
    tunnelNodeParams = {'nodetype':testTunnelType, 'tunnelfarendmap':self.mapId}  # wrong target map Id.
    cm = makeTransactionHeaderJSON(self.mapId, transactionId, nowTime)
    cm += json.dumps(makeCreateNodeChangeMessageJSON(testTunnelId, tunnelNodeParams))
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    self.assertRegex(json.loads(response.content.decode("utf-8"))["result"], "E")  # result: error


    
    # TEST PRETTIFY
    response = clnt.post('/ideatree/map/prettify/')
    self.assertEqual(response.status_code, 200)
    prettifyResponseObj = json.loads(response.content)
    self.assertEqual(prettifyResponseObj["result"], "S")
    # FIXME: test with at least one other orientation


    # TEST MANGLED CHANGE MESSAGES --------------------------------------------------------------
    # FIXME: no assertions!
    transId = str(random.randint(1,sys.maxsize))
    nowTime = myutils.millisNow(offsetSeconds=0, decimalPoints=0)
    cm = makeTransactionHeaderJSON(self.mapId, transId, nowTime)
    cm += json.dumps(["setMapAttribute", "mapname","<script Ron Newman's map"]) # Naughty argument value # FIXME: doesn't work, value passes
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    #self.assertTrue("originalStatus" in json.loads(response.content.decode("utf-8")))
    #self.assertEqual(json.loads(response.content.decode("utf-8"))["originalStatus"], 406) 
    # Next
    transId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transId, nowTime)
    cm += json.dumps(["<setMapAttribute", "mapname", "a name"]) # Non-alpha character in command name # FIXME: doesn't work, value passes
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    #self.assertTrue("originalStatus" in json.loads(response.content.decode("utf-8")))
    #self.assertEqual(json.loads(response.content.decode("utf-8"))["originalStatus"], 406) 
    # Next
    transId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transId, nowTime)
    testMapName = "Ron Newman's map"
    cm += json.dumps(["setMapAttribute", "<mapname", "a name"]) # Non-alpha character in argument name # FIXME: doesn't work, value passes
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    #self.assertTrue("originalStatus" in json.loads(response.content.decode("utf-8")))
    #self.assertEqual(json.loads(response.content.decode("utf-8"))["originalStatus"], 406) 
    # Next
    transId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transId, nowTime)
    cm += json.dumps(["", testEdgeId]) # Missing argument name
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    #self.assertTrue("originalStatus" in json.loads(response.content.decode("utf-8"))) # FIXME: doesn't work, value passes
    #self.assertEqual(json.loads(response.content.decode("utf-8"))["originalStatus"], 406) 
    # Next
    transId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transId, nowTime)
    nodeParams = {'nodetype':'B'} # Bogus node type
    bogusNodeId = 1000 # No provisional node was made ahead of time.
    cm += json.dumps(makeCreateNodeChangeMessageJSON(bogusNodeId, nodeParams ))
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    #self.assertTrue("originalStatus" in json.loads(response.content.decode("utf-8"))) # FIXME: doesn't work, value passes
    #self.assertEqual(json.loads(response.content.decode("utf-8"))["originalStatus"], 406) 


    from shutil import copyfile
    # TEST IMPORT graphviz (dot) file.
    #dotTestPath = 'ideatree/static/ideatree/testdata/testfile.dot' 
    dotTestPath = 'ideatree/static/ideatree/testdata/bazel.dot' 
    importDir = make_absoluteImportDir(self.mapId)
    importedFile = importDir + "/uploaded." + settings.GRAPHVIZ_DOT.lower()
    copyfile(dotTestPath, importedFile)
    response = clnt.post('/ideatree/map/getImport/', {'format':settings.GRAPHVIZ_DOT, 'countNeededProvisionalElements':True})
    self.assertEqual(response.status_code, 200)
    res = json.loads(response.content.decode("utf-8"))
    provisionalElementsNeeded = {'nodeIds':res['nodes'], 'edgeIds':res['edges']}
    # For testfile.dot:
    #self.assertEqual(provisionalElementsNeeded['nodeIds'], 2)
    #self.assertEqual(provisionalElementsNeeded['edgeIds'], 1)
    # For bazel.dot:
    self.assertEqual(provisionalElementsNeeded['nodeIds'], 8)
    self.assertEqual(provisionalElementsNeeded['edgeIds'], 15)

    response = clnt.post('/ideatree/map/createNewEntityProvisional/', {'provisionalIdsRequested[node]':provisionalElementsNeeded['nodeIds'], 'provisionalIdsRequested[edge]':provisionalElementsNeeded['edgeIds']})
    self.assertEqual(response.status_code, 200)
    provisionalElementsAllocated = {}
    provisionalElementsAllocated['nodeIds'] = json.loads(response.content)['node']
    provisionalElementsAllocated['edgeIds'] = json.loads(response.content)['edge']
    response = clnt.post('/ideatree/map/getImport/', {'format':settings.GRAPHVIZ_DOT, 'provisionalElements':json.dumps(provisionalElementsAllocated)})
    self.assertEqual(response.status_code, 200)


    # TEST EXPORT Graphviz (dot) file.
    response = clnt.post('/ideatree/map/export/', {'exportFormat':settings.GRAPHVIZ_DOT})
    self.assertEqual(response.status_code, 200)
    # A JS script for submitting the form is rendered in a template.
    self.assertRegex(response.content.decode("utf-8"), r"\bdownload\b") 
    # FIXME: Emulate that client-side script running and submitting the template form. 

   

    # TEST IMPORT Excel (xlsx) file.
    # not supporting kumu at the moment:  excelTestPath = 'ideatree/static/ideatree/testdata/kumu-stw-why-nations-fail.xlsx' 
    # 119 nodes, 93 edges:
    excelTestPath2 = 'ideatree/static/ideatree/testdata/Paulus_Book_Chap_3.xlsx'
    #excelTestPath2 = 'ideatree/static/ideatree/testdata/Small_test.xlsx'

    # Emulate uploading a file, by copying it from a server test directory to the expected imports directory..
    importDir = make_absoluteImportDir(self.mapId)
    importedFile = importDir + "/uploaded." + settings.EXCEL_FILE_SUFFIX
    from shutil import copyfile
    try:
        copyfile(excelTestPath2, importedFile)
    except PermissionError as err:
        print(str(err))
        self.assertEqual(True, False)
    from openpyxl import load_workbook
    workbook = load_workbook(filename = importedFile, read_only=True)
    sheet = workbook.worksheets[0] # By convention, nodes are on sheet 0
    neededNodes = sheet.max_row  - 1  # Don't count the header row
    sheet = workbook.worksheets[1] # By convention, edges are on sheet 1
    neededEdges = sheet.max_row - 1  # Don't count the header row
    workbook.close()

    response = clnt.post('/ideatree/map/getImport/', {'format':settings.EXCEL_FILE_SUFFIX, 'countNeededProvisionalElements':True})
    self.assertEqual(response.status_code, 200)
    res = json.loads(response.content.decode("utf-8"))
    provisionalElementsNeeded = {'nodeIds':res['nodes'], 'edgeIds':res['edges']}
    self.assertEqual(provisionalElementsNeeded['nodeIds'], 119)
    self.assertEqual(provisionalElementsNeeded['edgeIds'], 93)

    response = clnt.post('/ideatree/map/createNewEntityProvisional/', {'provisionalIdsRequested[node]':provisionalElementsNeeded['nodeIds'], 'provisionalIdsRequested[edge]':provisionalElementsNeeded['edgeIds']})
    self.assertEqual(response.status_code, 200)
    provisionalElementsAllocated = {}
    provisionalElementsAllocated['nodeIds'] = json.loads(response.content)['node']
    provisionalElementsAllocated['edgeIds'] = json.loads(response.content)['edge']

    # NOTE: the path to the Excel file to import is by convention.
    response = clnt.post('/ideatree/map/getImport/', {'format':settings.EXCEL_FILE_SUFFIX, 'provisionalElements':json.dumps(provisionalElementsAllocated)})
    self.assertEqual(response.status_code, 200)
    # check warning message when invalid format is requested for import:
    response = clnt.post('/ideatree/map/getImport/', {'format':'xyz', 'provisionalElements':json.dumps(provisionalElementsAllocated)})
    self.assertRegex(json.loads(response.content)["error"], "This file format is not supported.") 

    # FIXME Read a sample node in database and verify known parameters as they were saved in the spreadsheet.
    #nd = Node.objects.get(pk=nodeId)
    #self.assertEqual(nd.nodetype,type)
    #self.assertEqual(nd.description,description)
    
    # TEST EXPORT Excel (xlsx) file.
    response = clnt.post('/ideatree/map/export/', {'exportFormat':settings.EXCEL_FILE_SUFFIX})
    self.assertEqual(response.status_code, 200)
    # A JS script for submitting the form is rendered in a template.
    self.assertRegex(response.content.decode("utf-8"), r"\bdownload\b") 
    # FIXME: Emulate that client-side script running and submitting the template form. 


    # TEST EXPORT PDF file. # FIXME: Todo? No longer a function.  PDF is now published to a read-only site, not exported as a download.
    #response = clnt.post('/ideatree/map/export/', {'format':settings.PDF_FILE_SUFFIX})
    #self.assertEqual(response.status_code, 200)
    # A JS script for submitting the form is rendered in a template.
    #self.assertRegex(response.content.decode("utf-8"), r"\bMap Successfully Published\b") 
    # FIXME: Emulate that client-side script running and submitting the template form. 




  def TestShortestPath(self):
    # FIXME some tests within that don't have to do with paths
    # Create a clean graph 
    # First, make sure allowable maps is high enough.
    UserProfile.objects.filter(user__id=self.user.id).update(nummapsallowed=4) 
    testMapName = 'shortest path map'
    response = clnt.post('/ideatree/map/newMap/', {'mapname':testMapName,'submitted':True})
    self.assertEqual(response.status_code, 200)
    self.testMapShortestPathId = Map_desc.objects.filter(mapname=testMapName).values_list('id',flat=True)[0]
    response = clnt.post('/ideatree/map/openMap/',{'mapId':self.testMapShortestPathId})
    self.assertEqual(response.status_code, 200)

    # PATH TEST: This test graph taken from https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm
    pathOrigin = self.makenode(label='B', type='B')
    pathEnd = self.makenode(label='E', type='E')
    node1 = self.makenode(label='1')
    node2 = self.makenode(label='2')
    node3 = self.makenode(label='3')
    node4 = self.makenode(label='4')
    node5 = self.makenode(label='5')
    node6 = self.makenode(label='6')
    edges=[]
    pathOriginToOne = self.makeedge(pathOrigin, node1)
    oneTo2 = self.makeedge(node1, node2, cost=7)
    oneTo6 = self.makeedge(node1, node6, cost=14)
    oneTo3 = self.makeedge(node1, node3, cost=9)
    twoTo4 = self.makeedge(node2, node4, cost=15)
    twoTo3 = self.makeedge(node2, node3, cost=10)
    threeTo6 = self.makeedge(node3, node6, cost=2)
    threeTo4 = self.makeedge(node3, node4, cost=11)
    fourTo5 = self.makeedge(node4, node5, cost=6)
    sixTo5 = self.makeedge(node6, node5, cost=9)
    fiveToPathEnd = self.makeedge(node5, pathEnd)

    # PATH TEST: Weighted
    response = clnt.post('/ideatree/map/analyze/',{'algorithm':'shortest_path', 'costed':'costed'})
    self.assertEqual(response.status_code, 200)
    rslt = json.loads(response.content.decode('utf-8'))
    path = []
    for change in rslt["changes"]:
      if (change[0] == 'setEdgeAttribute') and (change[2] == 'color') and (change[3] == settings.EDGE_ANALYSIS_COLOR):
        path.append(change[1])  # edge ID
    correctAnswer = [pathOriginToOne, oneTo3, threeTo6, sixTo5, fiveToPathEnd]
    self.assertTrue(path==correctAnswer)
    # FIXME: pdb this
    #check = [i for i,j in zip(correctAnswer, path) if i != j]
    #self.assertEqual(len(check), 0)

    # PATH TEST: Uncosted
    response = clnt.post('/ideatree/map/analyze/',{'algorithm':'shortest_path'}) # NOTE: you can't put in 'costed':False.  It has to just not be there.
    self.assertEqual(response.status_code, 200)
    rslt = json.loads(response.content.decode('utf-8'))
    path = []
    for change in rslt["changes"]:
      if (change[0] == 'setEdgeAttribute') and (change[2] == 'color') and (change[3] == settings.EDGE_ANALYSIS_COLOR):
        path.append(change[1])  # edge ID
    # FIXME: pdb this
    correctAnswer = [pathOriginToOne, oneTo6, sixTo5, fiveToPathEnd]
    self.assertTrue(path==correctAnswer)
    #check = [i for i, j in zip(correctAnswer, path) if i != j]
    #self.assertEqual(len(check), 0)

    # FIXME Check that there can only be on edge to path origin. 
    # FIXME Check that there can only be on edge to path end. 

    # PATH TEST: Check that edge to path origin cannot have a cost.
    nowTime = myutils.millisNow(offsetSeconds=0, decimalPoints=0)
    transId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transId, nowTime)
    cm += json.dumps(["setEdgeAttribute", pathOriginToOne, "cost", 10])
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    self.assertRegex(response.content.decode("utf-8"), r"\bcannot be assigned weights\b")
    # Check that edge to path end cannot have a cost.
    transId = str(random.randint(1,sys.maxsize))
    cm = makeTransactionHeaderJSON(self.mapId, transId, nowTime)
    cm += json.dumps(["setEdgeAttribute", fiveToPathEnd, "cost", 10])
    cm += ']}'
    response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
    self.assertRegex(response.content.decode("utf-8"), r"\bcannot be assigned weights\b")

    # PATH TEST: Delete both path endpoint nodes.  This should automatically delete their entries in the MapPathEndpoints table record, 
    # and when both entries deleted, the record itself.
      #def test_danglingMapPathEndpoints(self):
    response = self.deletenode(self.mapId,pathEnd)
    # And delete the endpoint
    response = self.deletenode(self.mapId,pathOrigin)

    # Now check that the MapPathEndpoint record was automatically deleted when the endpoint fields were both None.
    allmaps = Map_desc.objects.filter(status=settings.MAP_ACTIVE)
    for amap in allmaps:
      danglingPaths = MapPathEndpoints.objects.filter(ofmap=amap, pathBeginNode=None, pathEndNode=None).count()
      self.assertTrue(danglingPaths == 0)
    


    # EDGE TEST: Create an edge to a node that doesn't exist. It should fail. 
    edgeToNowhere = self.makeedge(node1, None, returnResponse=True)
    res = json.loads(edgeToNowhere.content.decode("utf-8"))
    self.assertRegex(res["originalMessage"], r"\bNot Acceptable\b")

    # EDGE TEST: Create an edge to a node that has been marked 'Deleted'. It should fail. 
    response = self.deletenode(self.mapId,node1)
    edgeToNowhere = self.makeedge(node2, node1, returnResponse=True)
    res = json.loads(edgeToNowhere.content.decode("utf-8"))
    self.assertRegex(res["message"], r"\bTarget node does not exist\b")


    # DOCKET TEST creating an 'Original' type node 
    # We're only going as far as the server's ok of the node creation.  The client will then remove the provisional node from service
    # and issue a changeMessage in a transaction to the server for the actual creation.
    testNodetype = settings.ORIGINAL_NODE
    # The node to be 'created' must be created first in the db in 'provisional' form.
    response = clnt.post('/ideatree/map/createNewEntityProvisional/', {'provisionalIdsRequested[node]':1, 'provisionalIdsRequested[edge]':0})
    res = json.loads(response.content.decode("utf-8"))
    testNodeId = int(res['node'][0])
    # Should return a form for user input.
    response = clnt.post('/ideatree/map/nodeDialog/', {'nodetype':testNodetype})
    self.assertEqual(response.status_code, 200)

    # Emulate a user submitting the form.  Should return a confirmation of a node 'created'.
    response = clnt.post('/ideatree/map/nodeDialog/', {'nodetype':testNodetype, 'submitted':True})
    res = json.loads(response.content.decode("utf-8"))
    self.assertEqual(res[0]["execute"][0]["action"], "createNode")
    # FIXME: do submit with missing and corrupt fields.


    # DOCKET TEST creating a 'Votable' type node 
    # We're only going as far as the server's ok of the node creation.  The client will then remove the provisional node from service
    # and issue a changeMessage in a transaction to the server for the actual creation.
    testNodetype = settings.VOTABLE_NODE
    # The node to be 'created' must be created first in the db in 'provisional' form.
    response = clnt.post('/ideatree/map/createNewEntityProvisional/', {'provisionalIdsRequested[node]':1, 'provisionalIdsRequested[edge]':0})
    res = json.loads(response.content.decode("utf-8"))
    testNodeId = int(res['node'][0])
    # Should return a form for user input.
    response = clnt.post('/ideatree/map/nodeDialog/', {'nodetype':testNodetype})
    self.assertEqual(response.status_code, 200)

    # Emulate a user submitting the form.  Should return a confirmation of a node 'created'.
    response = clnt.post('/ideatree/map/nodeDialog/', {'nodetype':testNodetype, 'submitted':True})
    res = json.loads(response.content.decode("utf-8"))
    self.assertEqual(res[0]["execute"][0]["action"], "createVotableNode")

    # DOCKET TEST creating a 'Tunnel' type node 
    # We're only going as far as the server's ok of the node creation.  The client will then remove the provisional node from service
    # and issue a changeMessage in a transaction to the server for the actual creation.
    testNodetype = settings.TUNNEL_NODE
    # The node to be 'created' must be created first in the db in 'provisional' form.
    response = clnt.post('/ideatree/map/createNewEntityProvisional/', {'provisionalIdsRequested[node]':1, 'provisionalIdsRequested[edge]':0})
    res = json.loads(response.content.decode("utf-8"))
    testNodeId = int(res['node'][0])
    # Should return a form for user input.
    response = clnt.post('/ideatree/map/nodeDialog/', {'nodetype':testNodetype})
    self.assertEqual(response.status_code, 200)

    # Emulate a user submitting the form.  Should return a confirmation of a node 'created'.
    response = clnt.post('/ideatree/map/nodeDialog/', {'nodetype':testNodetype, 'submitted':True})
    self.assertEqual(response.status_code, 200)



  # FIXME: repeat naughty strings as edge labels created.
  # FIXME: repeat naughty strings as edited edge labels.
  # FIXME: repeat naughty strings as node comments.
  def TestNaughtyStrings(self):
    testNodetype = settings.ORIGINAL_NODE
    badstrs = json.load(open('dev/naughtyStringsFromGitHub_minimaxir.NICE_ONES_PRUNED.json','r')) 
    #badstrs = json.load(open('dev/naughtyStringsFromGitHub_minimaxir.json','r')) # NOTE: master list of bad strings, but some are ok.  
    #realbadstrs=[]  # NOTE: used only to build list of actual bad strings from the master list.
    for i,bs in enumerate(badstrs):
      # The node to be 'created' must be created first in the db in 'provisional' form.
      response = clnt.post('/ideatree/map/createNewEntityProvisional/', {'provisionalIdsRequested[node]':1, 'provisionalIdsRequested[edge]':0})
      self.assertEqual(response.status_code, 200)
      self.assertNotRegex(response.content.decode("utf-8"), settings.PLEASE_OPEN_GRAPH_PROMPT) 
      res = json.loads(response.content.decode("utf-8"))
      bsTestNodeId = int(res['node'][0])
      nowTime = myutils.millisNow(offsetSeconds=0, decimalPoints=0)
      # FIXME: do similar with bad URLs
      nodeParams = {'nodetype':testNodetype, 'label':bs, 'description':bs, 'timestamp': nowTime, 'ownerID':self.user.id}
      transactionId = str(random.randint(1,sys.maxsize))
      cm = makeTransactionHeaderJSON(self.mapId, transactionId, nowTime)
      cm += json.dumps(makeCreateNodeChangeMessageJSON(bsTestNodeId, nodeParams, forGraphvizLayout=False))
      cm += ']}'
      response = clnt.post('/ideatree/map/changeMessageHandler/', {'chngMsgTrans':cm})
      self.assertEqual(response.status_code, 200)
      if "message" in json.loads(response.content.decode("utf-8")):
        self.assertNotRegex(json.loads(response.content.decode("utf-8"))["message"], settings.MAP_FULL_PROMPT) 

      # Used only to build initial list of actual bad strings from the master list (which includes some that aren't actually bad).
      #if "originalStatus" in json.loads(response.content.decode("utf-8")):
        #if json.loads(response.content.decode("utf-8"))["originalStatus"] == 406:
          #realbadstrs.append(i)
          #print(str(i) + ":" + str(bs))

      # NOTE: don't check for "originalStatus", because it can purposefully be 200 OK, with the "result" field set to (E)rror.
      # FIXME
      #self.assertRegex(json.loads(response.content.decode("utf-8"))["result"], "E") 
      savedLabel = Node.objects.get(pk=bsTestNodeId).label
      self.assertEqual(savedLabel, None)
      savedDescription = Node.objects.get(pk=bsTestNodeId).description
      self.assertEqual(savedDescription, None)

      # remove the test node so the map maximum of nodes isn't reached
      availableNodesBeforeDelete = lookup_map_info(self.mapId, "nodesleft")
      response = self.deletenode(self.mapId,bsTestNodeId)
      availableNodesAfterDelete = lookup_map_info(self.mapId, "nodesleft") 
      self.assertTrue(availableNodesAfterDelete > availableNodesBeforeDelete)
    #print(str(realbadstrs)) # NOTE: used only to build list of actual bad strings from the master list.  This should be copied to a utility program which prunes from the master list.



  def TestNodesLeftAccounting(self):
    thisMap = Map_desc.objects.get(pk=int(self.mapId))
    actualNodes = Node.objects.filter(ofmap_id=int(self.mapId), status=settings.NODE_ACTIVE).count()
    # Check that nodesleft equals allowed nodes minus actual nodes.
    #self.assertEqual(thisMap.nodesleft, settings.MAX_FREE_ACCOUNT_NODES_PER_MAP - actualNodes)  # FIXME fails 48,50-3
    # FIXME: and that adding a new node isn't off by one when nodesleft reaches zero.

    # Check that map won't allow a new node when map full limit reached.
    #FIXME todo: self.assertRaisesRegex(Warning, settings.MAP_FULL_PROMPT, customCheck, self.request, self.mapId)


  # Put here to control the order of tests in this class (only defs beginning with lower-case 'test' execute).
  def test_makeMap_then_openMap_then_map_functions(self):
    self.TestMakeMap()
    self.TestEditUnopenedMapName()
    self.TestDeleteUnopenedMap()
    self.TestOpenMapForReadonlyUser()
    self.TestOpenMap() # FIXME for full test, do this open with and without nodes.
    self.TestListAllAccessibleMaps()
    self.TestMapSummary()
    self.TestSearchUsers()
    self.TestAddSharer()
    self.TestNotifySharer()
    self.TestRemoveSharer()
    self.TestVote()
    self.TestTallyVote()
    self.TestChangeMessageHandler() # creates a node for use below:

    #self.TestNaughtyStrings() # FIXME

    self.TestShortestPath()
    self.TestCheckPermissionsWithAccess()
    self.TestEditMapName()

    #self.TestEditMapNameWithNoAccess()  # FIXMME

    self.TestDeleteMapWithNoAccess()
    self.TestDeleteMapWithNodesPresent()
    self.TestNodesLeftAccounting()
    self.TestDeleteMap()
    #self.TestDemoMap()  # FIXME



class TestSimpleServers(TestSetup):

  def test_index(self):
    self.add_session_to_request()  # Just to get a populated request
    response = index(self.request)
    response = clnt.get('/ideatree/')
    self.assertEqual(response.status_code, 200)

  def test_features(self):
    response = clnt.get('/ideatree/features/')
    self.assertEqual(response.status_code, 200)

  def test_videos(self):
    response = clnt.get('/ideatree/videos/')
    self.assertEqual(response.status_code, 200)

  def test_video_how_to_use(self):
    response = clnt.get('/ideatree/video_how_to_use/')
    self.assertEqual(response.status_code, 200)

  def test_video_for_project_managers(self):
    response = clnt.get('/ideatree/video_for_project_managers/')
    self.assertEqual(response.status_code, 200)

  def test_research(self):
    response = clnt.get('/ideatree/research/')
    self.assertEqual(response.status_code, 200)

  def test_press(self):
    response = clnt.get('/ideatree/press/')
    self.assertEqual(response.status_code, 200)

  # FIXME: needs front end testing, too.  This is just the first form, without submit.
  def test_contactus(self):
    # FIXME: do submit with missing and corrupt fields.
    response = clnt.get('/ideatree/map/contactus/')
    self.assertEqual(response.status_code, 200)

  def test_terms_of_service(self):
    response = clnt.get('/ideatree/terms-of-service/')
    self.assertEqual(response.status_code, 200)

  def test_privacystatement(self):
    response = clnt.get('/ideatree/privacystatement/')
    self.assertEqual(response.status_code, 200)

# FIXME
# def test_idea_selection_paper(self):
#   response = clnt.get('/ideatree/idea_selection_paper/')
#   self.assertEqual(response.status_code, 200)

  def test_presignup(self):
    response = clnt.get('/ideatree/presignup/')
    self.assertEqual(response.status_code, 200)

  def test_commandSummary(self):
    response = clnt.get('/ideatree/map/help/commandSummary/')
    self.assertEqual(response.status_code, 200)

  def test_quickStartHelp(self):
    response = clnt.get('/ideatree/map/help/quickStartHelp/')
    self.assertEqual(response.status_code, 200)

  def test_prettifyHelp(self):
    response = clnt.get('/ideatree/map/help/prettifyHelp/')
    self.assertEqual(response.status_code, 200)

  def test_clusteringHelp(self):
    response = clnt.get('/ideatree/map/help/clusteringHelp/')
    self.assertEqual(response.status_code, 200)

  def test_teamsHelp(self):
    response = clnt.get('/ideatree/map/help/teamsHelp/')
    self.assertEqual(response.status_code, 200)

  def test_notifyTeamHelp(self):
    response = clnt.get('/ideatree/map/help/notifyTeamHelp/')
    self.assertEqual(response.status_code, 200)

  def test_conceptVsMindHelp(self):
    response = clnt.get('/ideatree/map/help/conceptVsMindHelp/')
    self.assertEqual(response.status_code, 200)

  def test_nodeShapes(self):
    response = nodeShapes()
    self.assertIs(type(response), list)
    self.assertTrue(len(response) > 0)

  def test_saveExplain(self):
    response = clnt.get('/ideatree/map/saveExplain/')
    self.assertEqual(response.status_code, 200)



#--------------------- low level database stuff

class TestDatabaseIntegrity(TestCase):
  def test_no_bogus_maps_in_mapmember_table(self):
    numMapsInMapmembership = Mapmember.objects.values('ofmap').distinct().count()
    numMaps = Map_desc.objects.all().count()
    self.assertEqual(numMapsInMapmembership, numMaps)


#class QuestionViewTests(TestCase):

  #def test_myquestions_view_with_no_questions(self):
    #""" With no questions saved, just a notice should be displayed. """
    #response = client.get(reverse('askWisdom:myQuestions'))
    #self.assertEqual(response.status_code, 200)
    #self.assertContains(response, "No questions are available")
    #self.assertQuerysetEqual(response.context['latest_question_list'],[])

  #def test_myquestions_view_with_a_past_question(self):
    #""" With a past question saved, it should be displayed. """
    #create_question(question_text="Past question.", days=-30)
    #response = client.get(reverse('askWisdom:myQuestions'))
    #self.assertQuerysetEqual(response.context['latest_question_list'],['<Question: Past question.>'])

  #def test_myquestions_view_with_a_future_question(self):
    #""" With a future question saved, a not-available prompt should be displayed. """
    #create_question(question_text="Future question.", days=30)
    #response = client.get(reverse('askWisdom:myQuestions'), status_code=200)
    #self.assertContains(response, "No questions are available")


#class QuestionMethodTests(TestCase):
  #def test_was_published_recently_with_future_question(self):
    #""" With a future question saved, was_published_recently should be false. """
    #time = timezone.now() + datetime.timedelta(days=30)
    #future_question = Question(pub_date=time)
    #self.assertEqual(future_question.was_published_recently(),False)

