Mocking
#######

You can mock ldap3 in your project using the MockSyncStrategy or the MockAsyncStrategy. Both of these strategies are based
on the MockBaseStreategy that emulates a simple LDAP server and can be used to test the LDAP functionalities in your project.
You can even define a specific kind of LDAP server to emulate and MockBaseStrategy will provide a suitable schema and the relevant DSA info.

To populate the DIT (the Directory Information Tree, the hierarchical database that contains the LDAP data) you can provide data from an
actual LDAP server via a JSON data file or you can create at runtime only the entries and the attributes needed for your test suite.

Anyway if you want to use Simple Bind authentication you must provide users and passwords needed by the test. No ACL or rights control is done
by the MockBaseStrategy.

To mock the ldap3 library in your project you must define a fake Server object and set the client_strategy attribute to MOCK_SYNC or MOCK_ASYNC
while defining the Connection object::

    from ldap3 import Server, Connection, MOCK_SYNC
    server = Server('my_fake_server')
    connection = Connection(server, user='cn=my_user,ou=test,o=lab', password='my_password', client_strategy=MOCK_SYNC)

then you can load the json entries file to add data to the DIT::

    connection.strategy.entries_from_json('my_entries.json')

or add entries dynamically at runtime::

    connection.strategy.add_entry('cn=user0,ou=test,o=lab', {'userPassword': 'test0000', 'sn': 'user0_sn', 'revision': 0})
    connection.strategy.add_entry('cn=user1,ou=test,o=lab', {'userPassword': 'test1111', 'sn': 'user1_sn', 'revision': 0})
    connection.strategy.add_entry('cn=user2,ou=test,o=lab', {'userPassword': 'test2222', 'sn': 'user2_sn', 'revision': 0})

.. note::
    MockBaseStrategy doesn't check the validity of the added entries against the schema, so you can just add the entries and the attribute
    values needed to perform your tests.

Then you can use the mock connection as a normal connection to an LDAP server.

.. note::
    The MockBaseStrategy provides only Simple Authentication bind. You can bind to any object in the dict that has a **userPassword** attribute
    (either single or multi-valued). The password must be stored as cleartext. You cannot use the ``auto_bind`` parameter because the DIT is
    populated after the creation of the Connection object.

MockBaseStrategy supports the Bind, Unbind, Add, Modify, ModifyDn, Compare, Delete and Search operations (except for the
extensible match). Abandon and Extended are not supported.

.. note::
    You can replicate the DIT of a real server (or just the portions of the Tree that you need in your tests) using the ``response_to_file()`` method
    of the Connection object with *raw* output. Just perform a SUBTREE search with ALL_ATTRIBUTES (or the attributes needed in your tests) with
    the needed base and a filter as ``(objectclass=*)`` that captures every object in the DIT::

        from ldap3 import Server, Connection, ALL_ATTRIBUTES
        server = Server('my_real_server')
        connection = Connection(server, 'cn=my_real_user,ou=test,o=lab', 'my_real_password', auto_bind=True)
        if connection.search('ou=test,o=lab', '(objectclass=*)', attributes=ALL_ATTRIBUTES):
            connection.response_to_file('my_entries.json', raw=True)

    The *my_entries.json* file can then be used in the ``entries_from_json()`` method of the MockBaseStrategy.

While defining the mock server you can specify a predefined schema with the ``get_info`` parameter::

    from ldap3 import Server, Connection, OFFLINE_SLAPD_2_4
    server = Server('my_fake_server', get_info=OFFLINE_SLAPD_2_4)

The available offline schemas are OFFLINE_SLAPD_2_4 (OpenLDAP), OFFLINE_EDIR_8_8_8 (eDirectory8), OFFLINE_EDIR_9_1_4 (eDirectory9), OFFLINE_AD_2012_R2 (Active Directory) and
OFFLINE_DS389_1_3_3 (389 Directory Server).

You can also specify a previously saved schema and info retrieved from a real server::

    from ldap3 import Server, Connection, ALL

    # Retrieve server info and schema from a real server
    server = Server('my_real_server', get_info=ALL)
    connection = Connection(server, 'cn=my_user,ou=test,o=lab', 'my_real_password', auto_bind=True)

    # Store server info and schema to json files
    server.info.to_file('my_real_server_info.json')
    server.schema.to_file('my_real_server_schema.json')


Full-featured fake server
^^^^^^^^^^^^^^^^^^^^^^^^^

You can create a full-featured fake server with Server.from_definition(), a static method of the Server object with the following parameters:

* host: name of the fake server
* dsa_info: DsaInfo preloaded object or a json formatted string or a file name
* dsa_schema: SchemaInfo preloaded object or a json formatted string or a file name
* port: fake port
* use_ssl: fake ssl connection
* formatter: custom formatters


A complete example
^^^^^^^^^^^^^^^^^^

The following code retrieves the schema and the server info from a real server, then read the entries from a portion of the DIT and store them
in a json file. Then a fake server is created and loaded with the previoulsy saved schema, server info and entries and a fake user is defined
for simple binding::

    from ldap3 import Server, Connection, ALL, ALL_ATTRIBUTES, MOCK_SYNC
    REAL_SERVER = 'my_real_server'
    REAL_USER = 'cn=my_real_user,ou=test,o=lab'
    REAL_PASSWORD = 'my_real_password'

    # Retrieve server info and schema from a real server
    server = Server(REAL_SERVER, get_info=ALL)
    connection = Connection(server, REAL_USER, REAL_PASSWORD, auto_bind=True)

    # Store server info and schema to json files
    server.info.to_file('my_real_server_info.json')
    server.schema.to_file('my_real_server_schema.json')

    # Read entries from a portion of the DIT from real server and store them in a json file
    if connection.search('ou=test,o=lab', '(objectclass=*)', attributes=ALL_ATTRIBUTES):
        connection.response_to_file('my_real_server_entries.json', raw=True)

    # Close the connection to the real server
    connection.unbind()

    # Create a fake server from the info and schema json files
    fake_server = Server.from_definition('my_fake_server', 'my_real_server_info.json', 'my_real_server_schema.json')

    # Create a MockSyncStrategy connection to the fake server
    fake_connection = Connection(fake_server, user='cn=my_user,ou=test,o=lab', password='my_password', client_strategy=MOCK_SYNC)

    # Populate the DIT of the fake server
    fake_connection.strategy.entries_from_json('my_real_server_entries.json')

    # Add a fake user for Simple binding
    fake_connection.strategy.add_entry('cn=my_user,ou=test,o=lab', {'userPassword': 'my_password', 'sn': 'user_sn', 'revision': 0})

    # Bind to the fake server
    fake_connection.bind()

Then the connection is ready to be used in your tests.
