##############################################################################
#
# Copyright (c) 2002-2024 Nexedi SA and Contributors. All Rights Reserved.
#
# WARNING: This program as such is intended to be used by professional
# programmers who take the whole responsibility of assessing all potential
# consequences resulting from its eventual inadequacies and bugs
# End users who are looking for a ready-to-use solution with commercial
# guarantees and support are strongly adviced to contract a Free Software
# Service Company
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
##############################################################################
from cStringIO import StringIO
from httplib import NO_CONTENT
import json
import msgpack
import random
import string

from Products.ERP5Type.tests.SecurityTestCase import SecurityTestCase


def generateRandomString(length=32):
  return ''.join(
    [random.choice(string.ascii_letters + string.digits) for _ in xrange(length)]
  )

class WendelinTelecomTest(SecurityTestCase):
  """
  Wendelin Telecom Test
  """

  def getTitle(self):
    return "Wendelin Telecom Test"

  def afterSetUp(self):
    # Set up variables for ORS ingestion testing
    self.ors_enb_log_ingestion = self.portal.portal_ingestion_policies.ors_enb_log_ingestion
    self.test_ors_example_log_valid = {'log': self.portal.web_page_module.test_example_ors_enb_log_valid.getTextContent()}
    self.test_ors_example_log_invalid = {'log': self.portal.web_page_module.test_example_ors_enb_log_invalid.getTextContent()}

  def beforeTearDown(self):
    self.abort()

    # Clean up all test items
    for module, portal_type in (
      (self.portal.project_module, 'Project'),
      (self.portal.person_module, 'Person'),
      (self.portal.data_acquisition_unit_module, 'Data Acquisition Unit'),
      (self.portal.data_supply_module, 'Data Supply'),
      (self.portal.data_stream_module, 'Data Stream'),
      (self.portal.data_ingestion_module, 'Data Ingestion'),
      (self.portal.data_analysis_module, 'Data Analysis'),
      (self.portal.data_array_module, 'Data Array'),
    ):
      objects = module.objectValues(portal_type=portal_type)
      if objects:
        test_object_ids = [obj.getId() for obj in objects if ('test' in obj.getReference() and 'default' not in obj.getId())]
        if test_object_ids:
          module.manage_delObjects(ids=test_object_ids)
    self.tic()

  def createWendelinTelecomUser(self, reference, project, function):
    # Create and validate a new Person with an assignment associated to the provided project and function
    # Also generate a validated ERP5 login for the Person
    user = self.portal.person_module.newContent(
      portal_type='Person',
      reference=reference
    )
    user.newContent(portal_type='Assignment', destination_project=project, function=function).open()
    user.newContent(portal_type='ERP5 Login', reference=reference, password=reference).validate()
    user.validate()
    self.tic()

    return user
  
  def createOrsClientProject(self, reference=None, user_reference=None, ors_tag_reference=None, ors_title_reference=None):
    # Create a client project with the provided reference. The reference will be used for all items linked to the project
    # If set, user_reference overrides reference for the client user account
    # If set, ors_tag_reference overrides reference for the Data Acquisition Unit (ORS)
    # If set, ors_title_reference overrides reference for the title of the Data Acquisition Unit
    if reference is None:
      reference = generateRandomString()
    ors_tag = 'test_%s' % (ors_tag_reference or reference)
    parameter_dict = {
      'project': 'test_project_%s' % reference,
      'project_title': 'Test Project %s' % reference,
      'username': 'test_user_%s' % (user_reference or reference),
      'ors_tag': ors_tag,
      'ors_data_acquisition_unit': 'ors.%s' % ors_tag,
      'ors_title': 'Test ORS %s' % (ors_title_reference or reference)
    }

    # Call the script responsible for creating the project and all associated items and store the JSON response
    response = self.portal.Base_registerOrsClientProject(
      parameter_dict['project'],
      parameter_dict['project_title'],
      parameter_dict['username'],
      parameter_dict['ors_tag'],
      parameter_dict['ors_title']
    )
    self.tic()

    # Fetch all associated items from the catalog
    project = self.portal.portal_catalog.getResultValue(
      portal_type='Project',
      reference=parameter_dict['project'],
      title=parameter_dict['project_title'],
      validation_state='validated'
    )
    client_user = self.portal.portal_catalog.getResultValue(
      portal_type='Person',
      reference=parameter_dict['username'],
      validation_state='validated'
    )
    data_acquisition_unit = self.portal.portal_catalog.getResultValue(
      portal_type='Data Acquisition Unit',
      reference=parameter_dict['ors_data_acquisition_unit'],
      title=parameter_dict['ors_title'],
      validation_state='validated'
    )
    data_supply = None
    if data_acquisition_unit:
      data_supply = data_acquisition_unit.DataAcquisitionUnit_createOrsDataSupply(batch=1)

    # Return all associated items
    return {
      'response': response,
      'project': project,
      'client_user': client_user,
      'data_acquisition_unit': data_acquisition_unit,
      'data_supply': data_supply
    }
  
  def ingestOrsLogDataFromFluentd(self, log_data, reference):
    # Simulate a fluentd instance sending the provided log data to Wendelin for ingestion
    body = msgpack.packb([0, log_data], use_bin_type=True)
    env = {'CONTENT_TYPE': 'application/octet-stream'}
    path = self.ors_enb_log_ingestion.getPath() + '/ingest?reference=' + reference
    publish_kw = dict(user='ERP5TypeTestCase', env=env,
      request_method='POST', stdin=StringIO(body))
    return self.publish(path, **publish_kw)

  def getDataStream(self, data_acquisition_unit):
    # Get a Data Stream linked to the provided Data Acquisition Unit
    for line in data_acquisition_unit.getAggregateRelatedValueList(portal_type='Data Ingestion Line'):
      data_stream = line.getAggregateValue(portal_type='Data Stream')
      if data_stream:
        return data_stream

  def getDataAnalysis(self, data_supply):
    # Get a Data Analysis linked to the provided Data Supply
    for data_analysis in data_supply.getSpecialiseRelatedValueList(portal_type='Data Analysis'):
      if data_analysis:
        return data_analysis

  def getDataArrays(self, data_analysis):
    # Get the Data Arrays linked to the provided Data Analysis
    data_arrays = []
    for line in data_analysis.contentValues(portal_type='Data Analysis Line'):
      data_array = line.getAggregateValue(portal_type='Data Array')
      if data_array:
        data_arrays.append(data_array)
    return data_arrays
  
  def getOrsLogIngestionItems(self, log_data, reference, stop_data_analysis=False):
    # Simulate an ingestion of the provided log data
    response = self.ingestOrsLogDataFromFluentd(log_data, reference)
    self.tic()

    # Retrieve all items linked to the ingestion
    data_acquisition_unit = self.portal.portal_catalog.getResultValue(portal_type='Data Acquisition Unit', reference=reference)
    data_supply = data_acquisition_unit.DataAcquisitionUnit_createOrsDataSupply(batch=1)
    self.tic()

    data_stream = self.getDataStream(data_acquisition_unit)

    # Manually call the data analysis alarms to process the data into KPIs ASAP
    self.portal.portal_alarms.wendelin_handle_analysis.activeSense()
    self.tic()
    self.portal.portal_alarms.wendelin_handle_analysis.activeSense()
    self.tic()

    data_analysis = self.getDataAnalysis(data_supply)
    if stop_data_analysis and data_analysis.getSimulationState() == 'started':
      data_analysis.stop()
      self.tic()

    data_arrays = self.getDataArrays(data_analysis)

    # Return all associated items
    return {
      'response': response,
      'data_acquisition_unit': data_acquisition_unit,
      'data_supply': data_supply,
      'data_stream': data_stream,
      'data_analysis': data_analysis,
      'data_arrays': data_arrays
    }
  
  def checkDocumentPermissions(self, user, document, user_can_view, user_can_modify, user_can_add):
    user_id = user.Person_getUserId()

    # Check if the provided user can access and view the document
    if user_can_view:
      self.assertUserCanAccessDocument(user_id, document)
      self.assertUserCanViewDocument(user_id, document)
    else:
      self.failIfUserCanAccessDocument(user_id, document)
      self.failIfUserCanViewDocument(user_id, document)

    # Check if the provided user can edit the document
    if user_can_modify:
      self.assertUserCanModifyDocument(user_id, document)
    else:
      self.failIfUserCanModifyDocument(user_id, document)

    # Check if the provided user can add a document
    if user_can_add:
      self.assertUserCanAddDocument(user_id, document)
    else:
      self.failIfUserCanAddDocument(user_id, document)
  
  def checkIngestionDocumentsPermissions(self, user, ingestion_items, user_is_admin, user_is_assigned_to_same_project):
    # A client can only view a Data Acquisition Unit (ORS) if they are related to the same project
    # An administrator can view and edit all of them, as well as add one
    self.checkDocumentPermissions(user, ingestion_items['data_acquisition_unit'], user_is_admin or user_is_assigned_to_same_project, user_is_admin, user_is_admin)

    # Same as above for a Data Supply (required for computing security roles on users)
    self.checkDocumentPermissions(user, ingestion_items['data_supply'], user_is_admin or user_is_assigned_to_same_project, user_is_admin, user_is_admin)

    # Only an administrator can view a Data Stream, and nothing else
    self.checkDocumentPermissions(user, ingestion_items['data_stream'], user_is_admin, False, False)

    # A client can only view a Data Analysis if they are related to the same project (required for KPI graphing)
    # An administrator can view all of them
    self.checkDocumentPermissions(user, ingestion_items['data_analysis'], user_is_admin or user_is_assigned_to_same_project, False, False)

    # A client can only view a Data Array if they are related to the same project
    # An administrator can view all of them
    for data_array in ingestion_items['data_arrays']:
      self.checkDocumentPermissions(user, data_array, user_is_admin or user_is_assigned_to_same_project, False, False)
  
  def checkModulePermissions(self, user, user_is_admin):
    # Everyone can view the Data Acquisition Unit and Data Supply modules
    # Only administrators can add documents to them
    self.checkDocumentPermissions(user, self.portal.data_acquisition_unit_module, True, False, user_is_admin)
    self.checkDocumentPermissions(user, self.portal.data_supply_module, True, False, user_is_admin)

    # Everyone can view the Data Product module (required for KPI graphing)
    # Everyone can also view the two data products used in the KPI calculation process (required for KPI graphing)
    self.checkDocumentPermissions(user, self.portal.data_product_module, True, False, False)
    ors_kpi = self.portal.portal_catalog.getResultValue(portal_type='Data Product',
      reference='ors_kpi',
      validation_state='validated')
    self.checkDocumentPermissions(user, ors_kpi, True, False, False)
    ors_enb_log_data = self.portal.portal_catalog.getResultValue(portal_type='Data Product',
      reference='ors_enb_log_data',
      validation_state='validated')
    self.checkDocumentPermissions(user, ors_enb_log_data, True, False, False)

    # Everyone can view the Data Transformation module (required for KPI graphing)
    # Everyone can also view the data transformation used to produce the KPIs (required for KPI graphing)
    self.checkDocumentPermissions(user, self.portal.data_transformation_module, True, False, False)
    data_transformation = self.portal.portal_catalog.getResultValue(portal_type='Data Transformation',
      reference='ors_enb_log_data_transformation',
      validation_state='validated')
    self.checkDocumentPermissions(user, data_transformation, True, False, False)

    # Only administrators can view the Data Ingestion and Data Stream modules
    self.checkDocumentPermissions(user, self.portal.data_ingestion_module, user_is_admin, False, False)
    self.checkDocumentPermissions(user, self.portal.data_stream_module, user_is_admin, False, False)

    # Only administrators can view the Data Analysis module
    self.checkDocumentPermissions(user, self.portal.data_analysis_module, user_is_admin, False, False)

    # Everyone can view the Data Array module (required for KPI graphing)
    self.checkDocumentPermissions(user, self.portal.data_array_module, True, False, False)

    # Only administrators have access to the Person and Project modules and can add items to them for client management purposes
    self.checkDocumentPermissions(user, self.portal.project_module, user_is_admin, False, user_is_admin)
    self.checkDocumentPermissions(user, self.portal.person_module, user_is_admin, False, user_is_admin)

  def test_01_createOrsDataSupply(self):
    '''
    Test the action which creates an ORS Data Supply from a Data Acquisition Unit.
    Check that the Data Supply is indeed created and validated.
    '''
    ors_reference = 'ors.test_%s' % generateRandomString()

    # Create and validate a Data Acquisition Unit
    data_acquisition_unit = self.portal.data_acquisition_unit_module.newContent(
      portal_type='Data Acquisition Unit',
      reference=ors_reference
    )
    data_acquisition_unit.validate()
    self.tic()

    # Call the tested script which creates a related Data Supply
    # No need to assign a project to it here: that is implicitly tested later
    data_supply = data_acquisition_unit.DataAcquisitionUnit_createOrsDataSupply(batch=1)
    self.tic()

    # Check that the Data Supply exists and is validated
    self.assertTrue(data_supply is not None)
    self.assertTrue(data_supply.getValidationState() == 'validated')

  def test_02_registerOrsClientProject(self):
    '''
    Test the script called during slave instantiation in SlapOS to register a new client project.
    Check the successful case as well as all error cases.
    '''

    # Generate a random reference, call the tested script and retrieve all associated items
    # This first call should succeed
    reference = generateRandomString()
    project_items = self.createOrsClientProject(reference=reference)

    # Parse the JSON response and check that the user credentials exist and are valid
    response_dict = json.loads(project_items['response'])
    self.assertTrue(response_dict['username'] == 'test_user_%s' % reference)
    self.assertTrue(len(response_dict['init_password']) == 16)

    # Check that all items created by the script have been created
    self.assertTrue(project_items['project'] is not None)
    self.assertTrue(project_items['client_user'] is not None)
    self.assertTrue(project_items['data_acquisition_unit'] is not None)
    self.assertTrue(project_items['data_supply'] is not None)

    # Call the script a second time with the same reference
    # This should not do anything and respond with an error as the project already exists
    project_items = self.createOrsClientProject(reference=reference)

    # Parse the JSON response and check that the error message is valid
    response_dict = json.loads(project_items['response'])
    self.assertTrue('error_msg' in response_dict)
    self.assertTrue(response_dict['error_msg'] == "Client project %s already registered." % project_items['project'].getReference())

    # Create a new reference for the project, but reuse the previous reference for the client user account
    new_project_reference = generateRandomString()
    while new_project_reference == reference:
      new_project_reference = generateRandomString()
    # Call the script a third time, keeping the same reference as before ONLY for the client user account
    # This should also error out as the client user account already exists
    project_items = self.createOrsClientProject(reference=new_project_reference, user_reference=reference)

    # Parse the JSON response and check that the error message is valid
    response_dict = json.loads(project_items['response'])
    self.assertTrue('error_msg' in response_dict)
    self.assertTrue(response_dict['error_msg'] == "Client account username not available: test_user_%s." % reference)

    # Check that the new project is NOT created
    self.assertTrue(project_items['project'] is None)

    # Same thing as above, but reuse the original reference for the Data Acquisition Unit (ORS)
    new_project_reference_2 = generateRandomString()
    while (new_project_reference_2 == reference) or (new_project_reference_2 == new_project_reference):
      new_project_reference_2 = generateRandomString()
    # Call the script a fourth time, keeping the same reference as before ONLY for the Data Acquisition Unit
    # This should also error out as it already exists
    project_items = self.createOrsClientProject(reference=new_project_reference_2, ors_tag_reference=reference)

    # Parse the JSON response and check that the error message is valid
    response_dict = json.loads(project_items['response'])
    self.assertTrue('error_msg' in response_dict)
    self.assertTrue(response_dict['error_msg'] == "ORS with tag test_%s already registered." % reference)

    # Check that the new project is NOT created
    self.assertTrue(project_items['project'] is None)

    # Generate new reference again, but reuse the original reference for the Data Acquisition Unit's title
    new_project_reference_3 = generateRandomString()
    while (new_project_reference_3 == reference):
      new_project_reference_3 = generateRandomString()
    # Call the script a fifth time, using the new reference for everything except the ORS title
    # This should succeed as the title is not checked for collision
    project_items = self.createOrsClientProject(reference=new_project_reference_3, ors_title_reference=reference)

    # Parse the JSON response and check that the user credentials exist and are valid
    response_dict = json.loads(project_items['response'])
    self.assertTrue(response_dict['username'] == 'test_user_%s' % new_project_reference_3)
    self.assertTrue(len(response_dict['init_password']) == 16)

    # Check that all items created by the script have been created
    self.assertTrue(project_items['project'] is not None)
    self.assertTrue(project_items['client_user'] is not None)
    self.assertTrue(project_items['data_acquisition_unit'] is not None)
    self.assertTrue(project_items['data_supply'] is not None)
  
  def test_03_registerNewOrsToProject(self):
    '''
    Test the action which configures a Data Acquisition Unit and Data Supply representing an ORS
    assigned to a given client project.
    '''

    project_items = self.createOrsClientProject()

    # Check that all items related to the project exist
    self.assertTrue(project_items['project'] is not None)
    self.assertTrue(project_items['client_user'] is not None)
    self.assertTrue(project_items['data_acquisition_unit'] is not None)
    self.assertTrue(project_items['data_supply'] is not None)

    # Add a new ORS to the project, with a new reference
    reference = generateRandomString()
    ors_tag = 'test_%s' % reference
    ors_reference = 'ors.test_%s' % reference
    ors_title = 'ORS Test %s' % reference

    project_items['project'].Project_newOrs(ors_tag, ors_title)
    self.tic()

    # Check that the Data Acquisition Unit exists and is validated
    data_acquisition_unit = self.portal.portal_catalog.getResultValue(
      portal_type='Data Acquisition Unit',
      reference=ors_reference,
      title=ors_title,
      validation_state='validated'
    )
    self.assertTrue(data_acquisition_unit is not None)

    # Check that the Data Supply exists and is validated
    data_supply = data_acquisition_unit.DataAcquisitionUnit_createOrsDataSupply(batch=1)
    self.assertTrue(data_supply is not None)

    # Generate new references
    new_reference = generateRandomString()
    new_generic_reference = 'test_%s' % new_reference
    new_ors_reference = 'ors.test_%s' % new_reference
    new_ors_title = 'ORS Test %s' % new_reference

    # Add a new ORS to the project, with a different reference but the same title
    project_items['project'].Project_newOrs(new_generic_reference, ors_title)
    self.tic()

    # Check that the Data Acquisition Unit exists and is validated
    new_data_acquisition_unit = self.portal.portal_catalog.getResultValue(
      portal_type='Data Acquisition Unit',
      reference=new_ors_reference,
      title=ors_title,
      validation_state='validated'
    )
    self.assertTrue(new_data_acquisition_unit is not None)

    # Check that the Data Supply exists and is validated
    new_data_supply = new_data_acquisition_unit.DataAcquisitionUnit_createOrsDataSupply(batch=1)
    self.assertTrue(new_data_supply is not None)

    # Add another ORS to the project, with the same reference and a new title
    # This should fail as there can be no reference collision
    project_items['project'].Project_newOrs(ors_tag, new_ors_title)
    self.tic()

    # Check that the Data Acquisition Unit does not exist
    new_data_acquisition_unit = self.portal.portal_catalog.getResultValue(
      portal_type='Data Acquisition Unit',
      reference=ors_reference,
      title=new_ors_title,
      validation_state='validated'
    )
    self.assertTrue(new_data_acquisition_unit is None)

  def test_04_1_ingestValidOrsLogDataFromFluentd(self, valid_data=True):
    '''
    Test a simple valid ORS log ingestion: simulate a fluentd gateway forwarding valid ORS logs to a project on the platform,
    and check that all items related to the ingestion are valid.
    '''
    project_items = self.createOrsClientProject()
    ors_reference = project_items['data_acquisition_unit'].getReference()

    # Get the correct data logs according to what is being tested
    test_ors_example_log = self.test_ors_example_log_valid
    if not valid_data:
      test_ors_example_log = self.test_ors_example_log_invalid
    ingestion_items = self.getOrsLogIngestionItems(test_ors_example_log, ors_reference)

    # In all cases, check that all items related to the ingestion exist
    self.assertEqual(NO_CONTENT, ingestion_items['response'].getStatus())
    self.assertTrue(ingestion_items['data_acquisition_unit'] is not None)
    self.assertTrue(ingestion_items['data_supply'] is not None)
    self.assertTrue(ingestion_items['data_stream'].getData() is not None)
    self.assertTrue(ingestion_items['data_analysis'] is not None)
    self.assertTrue(all(data_array is not None for data_array in ingestion_items['data_arrays']))

    # Check that the data arrays containing the KPI data have correctly been initialized
    self.assertTrue(ingestion_items['data_acquisition_unit'].Base_getERabDataArrayKey() is not None)
    self.assertTrue(ingestion_items['data_acquisition_unit'].Base_getEUtranDataArrayKey() is not None)

    if valid_data:
      # If all data is valid: check that the arrays contain some data
      self.assertTrue(all(data_array.getArrayShape() is not None for data_array in ingestion_items['data_arrays']))
      self.assertTrue(all(data_array.getArrayDtype() is not None for data_array in ingestion_items['data_arrays']))
    else:
      # If some data is invalid: check that the whole log (one data chunk in this case) is ignored
      # and the data arrays are empty
      self.assertTrue(all(data_array.getArrayShape() is None for data_array in ingestion_items['data_arrays']))
      self.assertTrue(all(data_array.getArrayDtype() is None for data_array in ingestion_items['data_arrays']))

  def test_04_2_ingestInvalidOrsLogDataFromFluentd(self):
    '''
    Test an invalid ORS log ingestion: simulate a fluentd gateway forwarding invalid ORS logs to a project on the platform.
    Check that all items are valid, but that the data arrays contain no data due to the invalid data chunk being ignored.
    '''

    # Call the above test, but test with invalid data
    self.test_04_1_ingestValidOrsLogDataFromFluentd(valid_data=False)

  def test_05_wendelinTelecomSecurityModel(self):
    '''
    Test Wendelin Telecom's custom security model: check that different users have the correct permissions according to their function and project.
    '''

    # Create two distinct projects
    project_a_items = self.createOrsClientProject()
    ors_a_reference = project_a_items['data_acquisition_unit'].getReference()
    project_b_items = self.createOrsClientProject()
    ors_b_reference = project_b_items['data_acquisition_unit'].getReference()

    # Perform ingestions for the ORSs of both projects
    ingestion_a_items = self.getOrsLogIngestionItems(self.test_ors_example_log_valid, ors_a_reference, stop_data_analysis=True)
    ingestion_b_items = self.getOrsLogIngestionItems(self.test_ors_example_log_valid, ors_b_reference, stop_data_analysis=True)

    # Create a client user not associated to a project (should not happen in practice)
    client_user_n = self.createWendelinTelecomUser('test_user_%s' % generateRandomString(), None, 'user')

    # Create two administrator users: one associated to Project A and the second not associated to a project
    admin_user_a = self.createWendelinTelecomUser('test_user_%s' % generateRandomString(), project_a_items['project'].getRelativeUrl(), 'administrator')
    admin_user_n = self.createWendelinTelecomUser('test_user_%s' % generateRandomString(), None, 'administrator')

    ors_n_reference = 'ors.test_%s' % generateRandomString()

    # Generate a Data Acquisition Unit without a project (should not happen in practice)
    data_acquisition_unit = self.portal.data_acquisition_unit_module.newContent(
      portal_type='Data Acquisition Unit',
      reference=ors_n_reference
    )
    data_acquisition_unit.validate()
    self.tic()
    data_acquisition_unit.DataAcquisitionUnit_createOrsDataSupply(batch=1)
    self.tic()

    # Perform an ingestion for the ORS not associated to a project
    ingestion_n_items = self.getOrsLogIngestionItems(self.test_ors_example_log_valid, ors_n_reference, stop_data_analysis=True)

    # Check that the client of Project A only has access to Project A documents
    client_user_a = project_a_items['client_user']
    self.checkModulePermissions(client_user_a, False)
    self.checkIngestionDocumentsPermissions(client_user_a, ingestion_a_items, False, True)
    self.checkIngestionDocumentsPermissions(client_user_a, ingestion_b_items, False, False)
    self.checkIngestionDocumentsPermissions(client_user_a, ingestion_n_items, False, False)

    # Check that the client of project_B only has access to project_B documents
    client_user_b = project_b_items['client_user']
    self.checkModulePermissions(client_user_b, False)
    self.checkIngestionDocumentsPermissions(client_user_b, ingestion_a_items, False, False)
    self.checkIngestionDocumentsPermissions(client_user_b, ingestion_b_items, False, True)
    self.checkIngestionDocumentsPermissions(client_user_b, ingestion_n_items, False, False)

    # Check that the client without a project does not have access to any document
    self.checkModulePermissions(client_user_n, False)
    self.checkIngestionDocumentsPermissions(client_user_n, ingestion_a_items, False, False)
    self.checkIngestionDocumentsPermissions(client_user_n, ingestion_b_items, False, False)
    self.checkIngestionDocumentsPermissions(client_user_n, ingestion_n_items, False, False)

    # Check that both administrators, whether assigned to a project, have access to all documents
    self.checkModulePermissions(admin_user_a, True)
    self.checkIngestionDocumentsPermissions(admin_user_a, ingestion_a_items, True, True)
    self.checkIngestionDocumentsPermissions(admin_user_a, ingestion_b_items, True, False)
    self.checkIngestionDocumentsPermissions(admin_user_a, ingestion_n_items, True, False)

    self.checkModulePermissions(admin_user_a, True)
    self.checkIngestionDocumentsPermissions(admin_user_n, ingestion_a_items, True, False)
    self.checkIngestionDocumentsPermissions(admin_user_n, ingestion_b_items, True, False)
    self.checkIngestionDocumentsPermissions(admin_user_n, ingestion_n_items, True, False)