
LDAP Nodes
==========

::

    >>> from bda.ldap import LDAPProps
    >>> from bda.ldap import LDAPNode
  
Create LDAP properties.
::

    >>> user = 'cn=Manager,dc=my-domain,dc=com'
    >>> pwd = 'secret'
    >>> props = LDAPProps('127.0.0.1', 12345, user, pwd, cache=False)

Create the root node. The Root node expects the initial base DN as name and
the server properties.
::

    >>> LDAPNode('dc=my-domain,dc=com')
    Traceback (most recent call last):
    ...
    ValueError: Wrong initialization.
  
    >>> LDAPNode(props=props)
    Traceback (most recent call last):
    ...
    ValueError: Wrong initialization.
  
    >>> root = LDAPNode('dc=my-domain,dc=com', props)
    >>> root
    <dc=my-domain,dc=com - False>

    The non-unicode name got decoded
    >>> root.__name__
    u'dc=my-domain,dc=com'
    
    >>> root.ldap_session
    <bda.ldap.session.LDAPSession object at ...>

Check child keys of root.
::

    >>> root.keys()
    [u'ou=customers']

Access inexistent child.
::

    >>> foo = root['foo']
    Traceback (most recent call last):
    ...
    KeyError: u'Entry not existent: foo'

Access existent child and it's attributes.
::
  
    >>> customers = root['ou=customers']
    >>> customers
    <ou=customers,dc=my-domain,dc=com:ou=customers - False>
  
    >>> customers.attributes
    {u'objectClass': [u'top', u'organizationalUnit'], u'ou': u'customers'}
  
    >>> customers.DN
    u'ou=customers,dc=my-domain,dc=com'
  
    >>> customers.__name__
    u'ou=customers'
  
    >>> customers.keys()
    [u'ou=customer1', u'ou=customer2', u'ou=nasty\\, customer']
  
    >>> customers.changed
    False

Create a new LDAPNode and add it to customers.
::

    >>> customer = LDAPNode()
    >>> customer.attributes['ou'] = 'customer3'
    >>> customer.attributes['objectClass'] = ['top', 'organizationalUnit']
    >>> customer.ldap_session is None
    True
    
    >>> customers['ou=customer3'] = customer
    >>> root.ldap_session
    <bda.ldap.session.LDAPSession object at ...>

This entry was not written yet to the directory, but marked that it has to be
written.

New entry has no childs, but was added to the parent. There
were an bug where iteration tried to load from ldap at this stage. Lets test
if this works
::
    >>> customer.keys()
    []     

Note that calling keys() of the container now, returns the already added key
anyway, since this is the sane state of the node structure, at least for
itself.
::

    >>> customers.keys()
    [u'ou=customer1', u'ou=customer2', u'ou=nasty\\, customer', u'ou=customer3']


The Container itself has changed.
::
 
    >>> customers.changed
    True

But there's no action on the container since a child was added and the
attributes of the contained has not been changed.
::

    >>> customers._action

The added child has been marked as changed as well.
::

    >>> customer.changed
    True

But there's also the action set that it has to be added.
::

    >>> from bda.ldap.node import ACTION_ADD, ACTION_MODIFY, ACTION_DELETE
  
    >>> customer._action == ACTION_ADD
    True

Check the directory state, not added yet.
::

    >>> res = customers._session.search('(objectClass=*)',
    ...                                 1,
    ...                                 baseDN=customers.DN,
    ...                                 force_reload=True)
    >>> len(res)
    3

On call the new entry is written to the directory.
::

    >>> root()

Now the new entry is present in the directory as well.
::

    >>> res = customers._session.search('(objectClass=*)',
    ...                                 1,
    ...                                 baseDN=customers.DN,
    ...                                 force_reload=True)
    >>> len(res)
    4

Add a person to test modification stuff.
::

    >>> person = LDAPNode()
    >>> person.attributes['objectClass'] = ['top', 'person']
    >>> person.attributes['sn'] = 'Mustermann'
    >>> person.attributes['cn'] = 'Max'
    >>> person.attributes['description'] = 'Initial Description'
    >>> customer['cn=max'] = person
    >>> customer.keys()
    [u'cn=max']
  
Again, not in directory yet.
::

    >>> res = customer._session.search('(objectClass=person)',
    ...                                1,
    ...                                baseDN=customer.DN,
    ...                                force_reload=True)
    >>> len(res)
    0
  
But after __call__.
::

    >>> customer()

Create Customer convenience query function. Need this more than once.
::

    >>> def queryPersonDirectly():
    ...     res = customer._session.search('(objectClass=person)',
    ...                                    1,
    ...                                    baseDN=customer.DN,
    ...                                    force_reload=True)
    ...     return res
  
    >>> pprint(queryPersonDirectly())
    [(u'cn=max,ou=customer3,ou=customers,dc=my-domain,dc=com',
      {u'cn': [u'Max'],
       u'description': [u'Initial Description'],
       u'objectClass': [u'top', u'person'],
       u'sn': [u'Mustermann']})]

Lets modify this person. First look at the flags.
::

    >>> root.changed, customer.changed, person.changed
    (False, False, False)
  
    >>> person._action

    >>> person.attributes.changed
    False

Now do modification.
::
    
    >>> person.attributes['description'] = 'Another description'
    >>> person.attributes.changed
    True
  
    >>> person._action == ACTION_MODIFY
    True

    >>> root.changed, customer.changed, person.changed
    (True, True, True)


Write changed to directory.
::

    >>> root()
    
Check the flags.
::

    >>> root.changed, customer.changed, person.changed
    (False, False, False)

And check the changes in the directory.
::

    >>> pprint(queryPersonDirectly())
    [(u'cn=max,ou=customer3,ou=customers,dc=my-domain,dc=com',
      {u'cn': [u'Max'],
       u'description': [u'Another description'],
       u'objectClass': [u'top', u'person'],
       u'sn': [u'Mustermann']})]

Check removing of an attribute.
::  
    >>> root.changed, customer.changed, person.changed, \
    ... person.attributes.changed
    (False, False, False, False)

    >>> del person.attributes['description']
    >>> root.changed, customer.changed, person.changed, \
    ... person.attributes.changed
    (True, True, True, True)

We can call a node in the middle.
::

    >>> customer()
    >>> pprint(queryPersonDirectly())
    [(u'cn=max,ou=customer3,ou=customers,dc=my-domain,dc=com',
      {u'cn': [u'Max'], u'objectClass': [u'top', u'person'], u'sn': [u'Mustermann']})]

    >>> root.changed, customer.changed, person.changed, \
    ... person.attributes.changed
    (False, False, False, False)

Check adding of an attribute.
::
    >>> person.attributes['description'] = u'Brandnew description'
    >>> root.changed, customer.changed, person.changed, \
    ... person.attributes.changed
    (True, True, True, True)

    >>> customer()
    >>> pprint(queryPersonDirectly())
    [(u'cn=max,ou=customer3,ou=customers,dc=my-domain,dc=com',
      {u'cn': [u'Max'],
       u'description': [u'Brandnew description'],
       u'objectClass': [u'top', u'person'],
       u'sn': [u'Mustermann']})]

    >>> root.changed, customer.changed, person.changed, \
    ... person.attributes.changed
    (False, False, False, False)

Attribute with non-ascii unicode returns as is.
::
    >>> person.attributes['sn'] = u'i\u0107'
    >>> person()
    >>> queryPersonDirectly()[0][1]['sn'][0]
    u'i\u0107'

Attribute with non-ascii str (utf8) returns as unicode.
::
    >>> person.attributes['sn'] = 'i\xc4\x87'
    >>> person()
    >>> queryPersonDirectly()[0][1]['sn'][0]
    u'i\u0107'

# XXX: Don't test this until we have proper binary attr support
#Attribute with utf16 str fails.
#::
#    >>> person.attributes['sn'] = '\xff\xfei\x00\x07\x01'
#    Traceback (most recent call last):
#    ...
#    UnicodeDecodeError:
#      'utf8' codec can't decode byte 0xff in position 0: unexpected code byte

Check queryNode with nasty dn.
::
    >>> from bda.ldap.node import queryNode
    >>> node = queryNode(props, \
    ...     r'ou=nasty\, customer,ou=customers,dc=my-domain,dc=com')
    >>> node.__name__
    u'ou=nasty\\, customer'
    >>> node.__parent__
    <ou=customers,dc=my-domain,dc=com - False>

Check access to attributes on a fresh but added-to-parent node. There were a bug
so we test it.
::

    >>> customerattrempty = LDAPNode()
    >>> customers['cn=customer99'] = customerattrempty
    >>> customerattrempty.attributes.keys()
    []
    
Add some attributes to make call work.
::
    
    >>> customerattrempty.attributes['objectClass'] = \
    ...     ['organizationalRole', 'simpleSecurityObject']
    >>> customerattrempty.attributes['userPassword'] = 'fooo'

Check deleting of entries.
::

    >>> [k for k in customer._keys]
    [u'cn=max']

    >>> del customer['cn=max']
    >>> root.changed, customer.changed, person.changed, \
    ... person.attributes.changed
    (True, True, True, False)
    
    >>> [k for k in customer._keys]
    []
    
    >>> root.printtree()
    <dc=my-domain,dc=com - True>
      <ou=customers,dc=my-domain,dc=com:ou=customers - True>
        <ou=customer3,ou=customers,dc=my-domain,dc=com:ou=customer3 - True>
          <cn=max,ou=customer3,ou=customers,dc=my-domain,dc=com:cn=max - True>
        <cn=customer99,ou=customers,dc=my-domain,dc=com:cn=customer99 - True>
    
    >>> customer()
    >>> queryPersonDirectly()
    []
    
    XXX This fails ATM - check ``LDAPNode._get_changed``
    >>> root.printtree()
    <dc=my-domain,dc=com - True>
      <ou=customers,dc=my-domain,dc=com:ou=customers - True>
        <ou=customer3,ou=customers,dc=my-domain,dc=com:ou=customer3 - False>
        <cn=customer99,ou=customers,dc=my-domain,dc=com:cn=customer99 - True>

    >>> root.changed, customer.changed
    (True, False)
    
    >>> customerattrempty()
    
    XXX This fails ATM - check ``LDAPNode._get_changed``
    >>> root.printtree()
    <dc=my-domain,dc=com - False>
      <ou=customers,dc=my-domain,dc=com:ou=customers - False>
        <ou=customer3,ou=customers,dc=my-domain,dc=com:ou=customer3 - False>
        <cn=customer99,ou=customers,dc=my-domain,dc=com:cn=customer99 - False>
