diff --git a/CHANGELOG.md b/CHANGELOG.md index 030edccb..f4ed7422 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Change log +## Devel + +* Restructure audits by moving them to their own sub-app (`anvil_consortium_manager.auditor`) +* Allow users to ignore a specific ManagedGroupMembershipAudit "not in app" record + ## 0.27.0 (2024-12-06) * Allow a user to link an Account that is not or has never been linked to another user. diff --git a/anvil_consortium_manager/__init__.py b/anvil_consortium_manager/__init__.py index cde6d897..df7db864 100644 --- a/anvil_consortium_manager/__init__.py +++ b/anvil_consortium_manager/__init__.py @@ -1 +1 @@ -__version__ = "0.27.0" +__version__ = "0.28.0" diff --git a/anvil_consortium_manager/audit/audit.py b/anvil_consortium_manager/audit/audit.py deleted file mode 100644 index 6f1db29e..00000000 --- a/anvil_consortium_manager/audit/audit.py +++ /dev/null @@ -1,485 +0,0 @@ -from abc import ABC - -import django_tables2 as tables - -from .. import exceptions, models -from ..anvil_api import AnVILAPIClient, AnVILAPIError404 - - -# Audit classes for individual model instances: -class ModelInstanceResult: - """Class to hold an audit result for a specific instance of a model.""" - - def __init__(self, model_instance): - self.model_instance = model_instance - self.errors = set() - - def __eq__(self, other): - return self.model_instance == other.model_instance and self.errors == other.errors - - def __str__(self): - return str(self.model_instance) - - def add_error(self, error): - """Add an error to the audit result for this model instance.""" - self.errors.add(error) - - def ok(self): - """Check whether an audit result has errors.""" - - if self.errors: - return False - else: - return True - - -class NotInAppResult: - """Class to hold an audit result for a record that is not present in the app.""" - - def __init__(self, record): - self.record = record - - def __str__(self): - return self.record - - def __eq__(self, other): - return self.record == other.record - - -# Tables for reporting audit results: -class VerifiedTable(tables.Table): - """Table for verified results.""" - - model_instance = tables.columns.Column(linkify=True, orderable=False) - - -# Tables for reporting audit results: -class ErrorTable(tables.Table): - """Table for results with errors.""" - - model_instance = tables.columns.Column(linkify=True, orderable=False) - errors = tables.columns.Column(orderable=False) - - def render_errors(self, record): - return ", ".join(sorted(record.errors)) - - -class NotInAppTable(tables.Table): - record = tables.columns.Column(orderable=False, empty_values=()) - - -# Audit classes for object classes: -class AnVILAudit(ABC): - """Abstract base class for AnVIL audit results.""" - - def __init__(self): - self._model_instance_results = [] - self._not_in_app_results = [] - - def ok(self): - model_instances_ok = all([x.ok() for x in self._model_instance_results]) - not_in_app_ok = len(self._not_in_app_results) == 0 - return model_instances_ok and not_in_app_ok - - def run_audit(self): - raise NotImplementedError("Define a run_audit method.") - - def add_result(self, result): - if isinstance(result, NotInAppResult): - self._add_not_in_app_result(result) - elif isinstance(result, ModelInstanceResult): - self._add_model_instance_result(result) - else: - raise ValueError("result must be a ModelInstanceResult or a NotInAppResult.") - - def _add_not_in_app_result(self, result): - # Check that it hasn't been added yet. - check = [x for x in self._not_in_app_results if x == result] - if len(check) > 0: - raise ValueError("Already added a result for {}.".format(result.record)) - self._not_in_app_results.append(result) - - def _add_model_instance_result(self, result): - check = [x for x in self._model_instance_results if x.model_instance == result.model_instance] - if len(check) > 0: - raise ValueError("Already added a result for {}.".format(result.model_instance)) - self._model_instance_results.append(result) - - def get_result_for_model_instance(self, model_instance): - results = [x for x in self._model_instance_results if x.model_instance == model_instance] - if len(results) != 1: - raise ValueError("model_instance is not in the results.") - return results[0] - - def get_verified_results(self): - return [x for x in self._model_instance_results if x.ok()] - - def get_error_results(self): - return [x for x in self._model_instance_results if not x.ok()] - - def get_not_in_app_results(self): - return self._not_in_app_results - - def export(self, include_verified=True, include_errors=True, include_not_in_app=True): - """Return a dictionary representation of the audit results.""" - exported_results = {} - if include_verified: - exported_results["verified"] = [ - {"id": result.model_instance.pk, "instance": result.model_instance} - for result in self.get_verified_results() - ] - if include_errors: - exported_results["errors"] = [ - { - "id": result.model_instance.pk, - "instance": result.model_instance, - "errors": list(result.errors), - } - for result in self.get_error_results() - ] - if include_not_in_app: - exported_results["not_in_app"] = list(sorted([x.record for x in self.get_not_in_app_results()])) - return exported_results - - -class BillingProjectAudit(AnVILAudit): - """Class that runs an audit for BillingProject instances.""" - - ERROR_NOT_IN_ANVIL = "Not in AnVIL" - """Error when a BillingProject in the app does not exist in AnVIL.""" - - def run_audit(self): - # Check that all billing projects exist. - for billing_project in models.BillingProject.objects.filter(has_app_as_user=True).all(): - model_instance_result = ModelInstanceResult(billing_project) - if not billing_project.anvil_exists(): - model_instance_result.add_error(self.ERROR_NOT_IN_ANVIL) - self.add_result(model_instance_result) - - -class AccountAudit(AnVILAudit): - """Class that runs an audit for Account instances.""" - - ERROR_NOT_IN_ANVIL = "Not in AnVIL" - """Error when the Account does not exist in AnVIL.""" - - def run_audit(self): - # Only checks active accounts. - for account in models.Account.objects.active(): - model_instance_result = ModelInstanceResult(account) - if not account.anvil_exists(): - model_instance_result.add_error(self.ERROR_NOT_IN_ANVIL) - self.add_result(model_instance_result) - - -class ManagedGroupAudit(AnVILAudit): - """Class to runs an audit for ManagedGroup instances.""" - - ERROR_NOT_IN_ANVIL = "Not in AnVIL" - """Error when a ManagedGroup in the app does not exist in AnVIL.""" - - ERROR_DIFFERENT_ROLE = "App has a different role in this group" - """Error when the service account running the app has a different role on AnVIL.""" - - ERROR_GROUP_MEMBERSHIP = "Group membership does not match in AnVIL" - """Error when a ManagedGroup has a different record of membership in the app compared to on AnVIL.""" - - def run_audit(self): - """Run an audit on managed groups in the app.""" - # Check the list of groups. - response = AnVILAPIClient().get_groups() - # Change from list of group dictionaries to dictionary of roles. That way we can handle being both - # a member and an admin of a group. - groups_on_anvil = {} - for group_details in response.json(): - group_name = group_details["groupName"] - role = group_details["role"].lower() - try: - groups_on_anvil[group_name] = groups_on_anvil[group_name] + [role] - except KeyError: - groups_on_anvil[group_name] = [role] - # Audit groups that exist in the app. - for group in models.ManagedGroup.objects.all(): - model_instance_result = ModelInstanceResult(group) - try: - group_roles = groups_on_anvil.pop(group.name) - except KeyError: - # Check if the group actually does exist but we're not a member of it. - try: - # If this returns a 404 error, then the group actually does not exist. - response = AnVILAPIClient().get_group_email(group.name) - if group.is_managed_by_app: - model_instance_result.add_error(self.ERROR_DIFFERENT_ROLE) - - except AnVILAPIError404: - model_instance_result.add_error(self.ERROR_NOT_IN_ANVIL) - # Perhaps we want to add has_app_as_member as a field and check that. - else: - # Check role. - if group.is_managed_by_app: - if "admin" not in group_roles: - model_instance_result.add_error(self.ERROR_DIFFERENT_ROLE) - else: - membership_audit = ManagedGroupMembershipAudit(group) - membership_audit.run_audit() - if not membership_audit.ok(): - model_instance_result.add_error(self.ERROR_GROUP_MEMBERSHIP) - elif not group.is_managed_by_app and "admin" in group_roles: - model_instance_result.add_error(self.ERROR_DIFFERENT_ROLE) - # Add the final result for this group to the class results. - self.add_result(model_instance_result) - - # Check for groups that exist on AnVIL but not the app. - for group_name in groups_on_anvil: - # Only report the ones where the app is an admin. - if "admin" in groups_on_anvil[group_name]: - self.add_result(NotInAppResult(group_name)) - - -class ManagedGroupMembershipAudit(AnVILAudit): - """Class that runs an audit for membership of a specific ManagedGroup instance.""" - - # Error strings for this class. - ERROR_ACCOUNT_ADMIN_NOT_IN_ANVIL = "Account not an admin in AnVIL" - """Error when an Account is an admin of a ManagedGroup on the app, but not in AnVIL.""" - - ERROR_ACCOUNT_MEMBER_NOT_IN_ANVIL = "Account not a member in AnVIL" - """Error when an Account is a member of a ManagedGroup on the app, but not in AnVIL.""" - - ERROR_DEACTIVATED_ACCOUNT = "Account is deactivated but still has membership records in the app." - """Error when a deactivated Account still has membership records in the app.""" - - ERROR_GROUP_ADMIN_NOT_IN_ANVIL = "Group not an admin in AnVIL" - """Error when a ManagedGroup is an admin of another ManagedGroup on the app, but not in AnVIL.""" - - ERROR_GROUP_MEMBER_NOT_IN_ANVIL = "Group not a member in AnVIL" - """Error when an ManagedGroup is a member of another ManagedGroup on the app, but not in AnVIL.""" - - def __init__(self, managed_group, *args, **kwargs): - super().__init__(*args, **kwargs) - if not managed_group.is_managed_by_app: - raise exceptions.AnVILNotGroupAdminError("group {} is not managed by app".format(managed_group.name)) - self.managed_group = managed_group - - def run_audit(self): - """Run an audit on all membership of the managed group.""" - # Get the list of members on AnVIL. - api_client = AnVILAPIClient() - # --- Members --- - response = api_client.get_group_members(self.managed_group.name) - # Convert to case insensitive emails. - members_in_anvil = [x.lower() for x in response.json()] - # Sometimes the service account is also listed as a member. Remove that too. - try: - members_in_anvil.remove(api_client.auth_session.credentials.service_account_email.lower()) - except ValueError: - # Not listed as a member -- this is ok. - pass - # -- Admins --- - response = api_client.get_group_admins(self.managed_group.name) - # Convert to case-insensitive emails. - admins_in_anvil = [x.lower() for x in response.json()] - # Remove the service account as admin. - try: - admins_in_anvil.remove(api_client.auth_session.credentials.service_account_email.lower()) - except ValueError: - # Not listed as an admin -- this is ok because it could be via group membership. - pass - - # Check group-account membership. - for membership in self.managed_group.groupaccountmembership_set.all(): - # Create an audit result instance for this model. - model_instance_result = ModelInstanceResult(membership) - # Check for deactivated account memberships. - if membership.account.status == models.Account.INACTIVE_STATUS: - model_instance_result.add_error(self.ERROR_DEACTIVATED_ACCOUNT) - # Check membership status on AnVIL. - if membership.role == models.GroupAccountMembership.ADMIN: - try: - admins_in_anvil.remove(membership.account.email.lower()) - except ValueError: - # This is an error - the email is not in the list of admins. - model_instance_result.add_error(self.ERROR_ACCOUNT_ADMIN_NOT_IN_ANVIL) - elif membership.role == models.GroupAccountMembership.MEMBER: - try: - members_in_anvil.remove(membership.account.email.lower()) - except ValueError: - # This is an error - the email is not in the list of members. - model_instance_result.add_error(self.ERROR_ACCOUNT_MEMBER_NOT_IN_ANVIL) - self.add_result(model_instance_result) - - # Check group-group membership. - for membership in self.managed_group.child_memberships.all(): - model_instance_result = ModelInstanceResult(membership) - if membership.role == models.GroupGroupMembership.ADMIN: - try: - admins_in_anvil.remove(membership.child_group.email.lower()) - except ValueError: - # This email is not in the list of members. - model_instance_result.add_error(self.ERROR_GROUP_ADMIN_NOT_IN_ANVIL) - # Also remove from members if it exists there. - try: - members_in_anvil.remove(membership.child_group.email.lower()) - except ValueError: - # The group is not directly listed as a member, so this is ok. - # It is already an admin. - pass - elif membership.role == models.GroupGroupMembership.MEMBER: - try: - members_in_anvil.remove(membership.child_group.email.lower()) - except ValueError: - # This email is not in the list of members. - model_instance_result.add_error(self.ERROR_GROUP_MEMBER_NOT_IN_ANVIL) - self.add_result(model_instance_result) - - # Add any admin that the app doesn't know about. - for member in admins_in_anvil: - self.add_result(NotInAppResult("{}: {}".format(models.GroupAccountMembership.ADMIN, member))) - # Add any members that the app doesn't know about. - for member in members_in_anvil: - self.add_result(NotInAppResult("{}: {}".format(models.GroupAccountMembership.MEMBER, member))) - - -class WorkspaceAudit(AnVILAudit): - """Class to runs an audit for Workspace instances.""" - - ERROR_NOT_IN_ANVIL = "Not in AnVIL" - """Error when a Workspace in the app does not exist on AnVIL.""" - - ERROR_NOT_OWNER_ON_ANVIL = "Not an owner on AnVIL" - """Error when the service account running the app is not an owner of the Workspace on AnVIL.""" - - ERROR_DIFFERENT_AUTH_DOMAINS = "Has different auth domains on AnVIL" - """Error when the Workspace has different auth domains in the app and on AnVIL.""" - - ERROR_WORKSPACE_SHARING = "Workspace sharing does not match on AnVIL" - """Error when a Workspace is shared with different ManagedGroups in the app and on AnVIL.""" - - ERROR_DIFFERENT_LOCK = "Workspace lock status does not match on AnVIL" - """Error when the workspace.is_locked status does not match the lock status on AnVIL.""" - - ERROR_DIFFERENT_REQUESTER_PAYS = "Workspace bucket requester_pays status does not match on AnVIL" - """Error when the workspace.is_locked status does not match the lock status on AnVIL.""" - - def run_audit(self): - """Run an audit on Workspaces in the app.""" - # Check the list of workspaces. - fields = [ - "workspace.namespace", - "workspace.name", - "workspace.authorizationDomain", - "workspace.isLocked", - "accessLevel", - ] - response = AnVILAPIClient().list_workspaces(fields=",".join(fields)) - workspaces_on_anvil = response.json() - for workspace in models.Workspace.objects.all(): - model_instance_result = ModelInstanceResult(workspace) - try: - i = next( - idx - for idx, x in enumerate(workspaces_on_anvil) - if ( - x["workspace"]["name"] == workspace.name - and x["workspace"]["namespace"] == workspace.billing_project.name - ) - ) - except StopIteration: - model_instance_result.add_error(self.ERROR_NOT_IN_ANVIL) - else: - # Check role. - workspace_details = workspaces_on_anvil.pop(i) - if workspace_details["accessLevel"] != "OWNER": - model_instance_result.add_error(self.ERROR_NOT_OWNER_ON_ANVIL) - else: - # Since we're the owner, check workspace access. - sharing_audit = WorkspaceSharingAudit(workspace) - sharing_audit.run_audit() - if not sharing_audit.ok(): - model_instance_result.add_error(self.ERROR_WORKSPACE_SHARING) - # Check auth domains. - auth_domains_on_anvil = [ - x["membersGroupName"] for x in workspace_details["workspace"]["authorizationDomain"] - ] - auth_domains_in_app = workspace.authorization_domains.all().values_list("name", flat=True) - if set(auth_domains_on_anvil) != set(auth_domains_in_app): - model_instance_result.add_error(self.ERROR_DIFFERENT_AUTH_DOMAINS) - # Check lock status. - if workspace.is_locked != workspace_details["workspace"]["isLocked"]: - model_instance_result.add_error(self.ERROR_DIFFERENT_LOCK) - # Check is_requester_pays status. Unfortunately we have to make a separate API call. - response = AnVILAPIClient().get_workspace( - workspace.billing_project.name, - workspace.name, - fields=["bucketOptions"], - ) - if workspace.is_requester_pays != response.json()["bucketOptions"]["requesterPays"]: - model_instance_result.add_error(self.ERROR_DIFFERENT_REQUESTER_PAYS) - - self.add_result(model_instance_result) - - # Check for remaining workspaces on AnVIL where we are OWNER. - not_in_app = [ - "{}/{}".format(x["workspace"]["namespace"], x["workspace"]["name"]) - for x in workspaces_on_anvil - if x["accessLevel"] == "OWNER" - ] - for workspace_name in not_in_app: - self.add_result(NotInAppResult(workspace_name)) - - -class WorkspaceSharingAudit(AnVILAudit): - """Class that runs an audit for sharing of a specific Workspace instance.""" - - ERROR_NOT_SHARED_IN_ANVIL = "Not shared in AnVIL" - """Error when a ManagedGroup has access to a workspace in the app but not on AnVIL.""" - - ERROR_DIFFERENT_ACCESS = "Different access level in AnVIL" - """Error when a ManagedGroup has a different access level for workspace in the app and on AnVIL.""" - - ERROR_DIFFERENT_CAN_SHARE = "can_share value does not match in AnVIL" - """Error when the can_share value for a ManagedGroup does not match what's on AnVIL.""" - - ERROR_DIFFERENT_CAN_COMPUTE = "can_compute value does not match in AnVIL" - """Error when the can_compute value for a ManagedGroup does not match what's on AnVIL.""" - - def __init__(self, workspace, *args, **kwargs): - super().__init__(*args, **kwargs) - self.workspace = workspace - - def run_audit(self): - """Run the audit for all workspace instances.""" - api_client = AnVILAPIClient() - response = api_client.get_workspace_acl(self.workspace.billing_project.name, self.workspace.name) - acl_in_anvil = {k.lower(): v for k, v in response.json()["acl"].items()} - # Remove the service account. - try: - acl_in_anvil.pop(api_client.auth_session.credentials.service_account_email.lower()) - except KeyError: - # In some cases, the workspace is shared with a group we are part of instead of directly with us. - pass - for access in self.workspace.workspacegroupsharing_set.all(): - # Create an audit result instance for this model. - model_instance_result = ModelInstanceResult(access) - try: - access_details = acl_in_anvil.pop(access.group.email.lower()) - except KeyError: - model_instance_result.add_error(self.ERROR_NOT_SHARED_IN_ANVIL) - else: - # Check access level. - if access.access != access_details["accessLevel"]: - model_instance_result.add_error(self.ERROR_DIFFERENT_ACCESS) - # Check can_compute value. - if access.can_compute != access_details["canCompute"]: - model_instance_result.add_error(self.ERROR_DIFFERENT_CAN_COMPUTE) - # Check can_share value -- the app never grants this, so it should always be false. - # Can share should be True for owners and false for others. - can_share = access.access == "OWNER" - if access_details["canShare"] != can_share: - model_instance_result.add_error(self.ERROR_DIFFERENT_CAN_SHARE) - # Save the results for this model instance. - self.add_result(model_instance_result) - - # Add any access that the app doesn't know about. - for key in acl_in_anvil: - self.add_result(NotInAppResult("{}: {}".format(acl_in_anvil[key]["accessLevel"], key))) diff --git a/anvil_consortium_manager/audit/__init__.py b/anvil_consortium_manager/auditor/__init__.py similarity index 100% rename from anvil_consortium_manager/audit/__init__.py rename to anvil_consortium_manager/auditor/__init__.py diff --git a/anvil_consortium_manager/auditor/admin.py b/anvil_consortium_manager/auditor/admin.py new file mode 100644 index 00000000..45bf7733 --- /dev/null +++ b/anvil_consortium_manager/auditor/admin.py @@ -0,0 +1,22 @@ +"""Admin classes for the anvil_consortium_manager.auditor app.""" + +from django.contrib import admin +from simple_history.admin import SimpleHistoryAdmin + +from . import models + + +@admin.register(models.IgnoredManagedGroupMembership) +class IgnoredManagedGroupMembershipAdmin(SimpleHistoryAdmin): + """Admin class for the IgnoredManagedGroupMembership model.""" + + list_display = ( + "pk", + "group", + "ignored_email", + "added_by", + ) + search_fields = ( + "group", + "ignored_email", + ) diff --git a/anvil_consortium_manager/auditor/apps.py b/anvil_consortium_manager/auditor/apps.py new file mode 100644 index 00000000..468592d1 --- /dev/null +++ b/anvil_consortium_manager/auditor/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class AuditorConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "anvil_consortium_manager.auditor" diff --git a/anvil_consortium_manager/auditor/audit/__init__.py b/anvil_consortium_manager/auditor/audit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/anvil_consortium_manager/auditor/audit/accounts.py b/anvil_consortium_manager/auditor/audit/accounts.py new file mode 100644 index 00000000..ac55f7c2 --- /dev/null +++ b/anvil_consortium_manager/auditor/audit/accounts.py @@ -0,0 +1,18 @@ +from anvil_consortium_manager.models import Account + +from .base import AnVILAudit, ModelInstanceResult + + +class AccountAudit(AnVILAudit): + """Class that runs an audit for Account instances.""" + + ERROR_NOT_IN_ANVIL = "Not in AnVIL" + """Error when the Account does not exist in AnVIL.""" + + def run_audit(self): + # Only checks active accounts. + for account in Account.objects.active(): + model_instance_result = ModelInstanceResult(account) + if not account.anvil_exists(): + model_instance_result.add_error(self.ERROR_NOT_IN_ANVIL) + self.add_result(model_instance_result) diff --git a/anvil_consortium_manager/auditor/audit/base.py b/anvil_consortium_manager/auditor/audit/base.py new file mode 100644 index 00000000..2e15258e --- /dev/null +++ b/anvil_consortium_manager/auditor/audit/base.py @@ -0,0 +1,201 @@ +from abc import ABC + +import django_tables2 as tables + + +# Audit classes for individual model instances: +class ModelInstanceResult: + """Class to hold an audit result for a specific instance of a model.""" + + def __init__(self, model_instance): + self.model_instance = model_instance + self.errors = set() + + def __eq__(self, other): + return self.model_instance == other.model_instance and self.errors == other.errors + + def __str__(self): + return str(self.model_instance) + + def add_error(self, error): + """Add an error to the audit result for this model instance.""" + self.errors.add(error) + + def ok(self): + """Check whether an audit result has errors.""" + + if self.errors: + return False + else: + return True + + +class NotInAppResult: + """Class to hold an audit result for a record that is not present in the app.""" + + def __init__(self, record): + self.record = record + + def __str__(self): + return self.record + + def __eq__(self, other): + return self.record == other.record + + +class IgnoredResult: + """Class to hold an audit result for a specific record in an Ignore table.""" + + def __init__(self, model_instance, record=None): + self.record = record + self.model_instance = model_instance + + def __eq__(self, other): + return self.model_instance == other.model_instance and self.record == other.record + + def __str__(self): + return str(self.record) + + +# Tables for reporting audit results: +class VerifiedTable(tables.Table): + """Table for verified results.""" + + model_instance = tables.columns.Column(linkify=True, orderable=False) + + +# Tables for reporting audit results: +class ErrorTable(tables.Table): + """Table for results with errors.""" + + model_instance = tables.columns.Column(linkify=True, orderable=False) + errors = tables.columns.Column(orderable=False) + + def render_errors(self, record): + return ", ".join(sorted(record.errors)) + + +class NotInAppTable(tables.Table): + record = tables.columns.Column(orderable=False, empty_values=()) + + +class IgnoredTable(tables.Table): + model_instance = tables.columns.Column(orderable=False, verbose_name="Details") + record = tables.columns.Column(orderable=False) + + def render_model_instance(self, record): + return "See details" + + +# Audit classes for object classes: +class AnVILAudit(ABC): + """Abstract base class for AnVIL audit results.""" + + verified_table_class = VerifiedTable + error_table_class = ErrorTable + not_in_app_table_class = NotInAppTable + ignored_table_class = IgnoredTable + + def __init__(self): + self._model_instance_results = [] + self._not_in_app_results = [] + self._ignored_results = [] + + def ok(self): + model_instances_ok = all([x.ok() for x in self._model_instance_results]) + not_in_app_ok = len(self._not_in_app_results) == 0 + return model_instances_ok and not_in_app_ok + + def run_audit(self): + raise NotImplementedError("Define a run_audit method.") + + def add_result(self, result): + if isinstance(result, NotInAppResult): + self._add_not_in_app_result(result) + elif isinstance(result, IgnoredResult): + self._add_ignored_result(result) + elif isinstance(result, ModelInstanceResult): + self._add_model_instance_result(result) + else: + raise ValueError("result must be ModelInstanceResult, NotInAppResult or IgnoredResult.") + + def _add_not_in_app_result(self, result): + # Check that it hasn't been added yet. + check = [x for x in self._not_in_app_results if x == result] + if len(check) > 0: + raise ValueError("Already added a result for {}.".format(result.record)) + self._not_in_app_results.append(result) + + def _add_model_instance_result(self, result): + check = [x for x in self._model_instance_results if x.model_instance == result.model_instance] + if len(check) > 0: + raise ValueError("Already added a result for {}.".format(result.model_instance)) + self._model_instance_results.append(result) + + def _add_ignored_result(self, result): + check = [x for x in self._ignored_results if x.model_instance == result.model_instance] + if len(check) > 0: + raise ValueError("Already added a result for {}.".format(result.model_instance)) + self._ignored_results.append(result) + + def get_result_for_model_instance(self, model_instance): + results = [x for x in self._model_instance_results if x.model_instance == model_instance] + if len(results) != 1: + raise ValueError("model_instance is not in the results.") + return results[0] + + def get_verified_results(self): + return [x for x in self._model_instance_results if x.ok()] + + def get_error_results(self): + return [x for x in self._model_instance_results if not x.ok()] + + def get_ignored_results(self): + return self._ignored_results + + def get_not_in_app_results(self): + return self._not_in_app_results + + def get_verified_table(self): + return self.verified_table_class(self.get_verified_results()) + + def get_error_table(self): + return self.error_table_class(self.get_error_results()) + + def get_not_in_app_table(self): + return self.not_in_app_table_class(self.get_not_in_app_results()) + + def get_ignored_table(self): + return self.ignored_table_class(self.get_ignored_results()) + + def export( + self, + include_verified=True, + include_errors=True, + include_not_in_app=True, + include_ignored=True, + ): + """Return a dictionary representation of the audit results.""" + exported_results = {} + if include_verified: + exported_results["verified"] = [ + {"id": result.model_instance.pk, "instance": result.model_instance} + for result in self.get_verified_results() + ] + if include_errors: + exported_results["errors"] = [ + { + "id": result.model_instance.pk, + "instance": result.model_instance, + "errors": list(result.errors), + } + for result in self.get_error_results() + ] + if include_not_in_app: + exported_results["not_in_app"] = list(sorted([x.record for x in self.get_not_in_app_results()])) + if include_ignored: + exported_results["ignored"] = [ + {"id": result.model_instance.pk, "instance": result.model_instance, "record": result.record} + for result in self.get_ignored_results() + ] + return exported_results diff --git a/anvil_consortium_manager/auditor/audit/billing_projects.py b/anvil_consortium_manager/auditor/audit/billing_projects.py new file mode 100644 index 00000000..38fa3e97 --- /dev/null +++ b/anvil_consortium_manager/auditor/audit/billing_projects.py @@ -0,0 +1,18 @@ +from anvil_consortium_manager.models import BillingProject + +from .base import AnVILAudit, ModelInstanceResult + + +class BillingProjectAudit(AnVILAudit): + """Class that runs an audit for BillingProject instances.""" + + ERROR_NOT_IN_ANVIL = "Not in AnVIL" + """Error when a BillingProject in the app does not exist in AnVIL.""" + + def run_audit(self): + # Check that all billing projects exist. + for billing_project in BillingProject.objects.filter(has_app_as_user=True).all(): + model_instance_result = ModelInstanceResult(billing_project) + if not billing_project.anvil_exists(): + model_instance_result.add_error(self.ERROR_NOT_IN_ANVIL) + self.add_result(model_instance_result) diff --git a/anvil_consortium_manager/auditor/audit/managed_groups.py b/anvil_consortium_manager/auditor/audit/managed_groups.py new file mode 100644 index 00000000..47759449 --- /dev/null +++ b/anvil_consortium_manager/auditor/audit/managed_groups.py @@ -0,0 +1,250 @@ +import django_tables2 as tables + +from anvil_consortium_manager.anvil_api import AnVILAPIClient, AnVILAPIError404 +from anvil_consortium_manager.exceptions import AnVILNotGroupAdminError +from anvil_consortium_manager.models import Account, GroupAccountMembership, GroupGroupMembership, ManagedGroup + +from .. import models +from . import base + + +class ManagedGroupAudit(base.AnVILAudit): + """Class to runs an audit for ManagedGroup instances.""" + + ERROR_NOT_IN_ANVIL = "Not in AnVIL" + """Error when a ManagedGroup in the app does not exist in AnVIL.""" + + ERROR_DIFFERENT_ROLE = "App has a different role in this group" + """Error when the service account running the app has a different role on AnVIL.""" + + ERROR_GROUP_MEMBERSHIP = "Group membership does not match in AnVIL" + """Error when a ManagedGroup has a different record of membership in the app compared to on AnVIL.""" + + def run_audit(self): + """Run an audit on managed groups in the app.""" + # Check the list of groups. + response = AnVILAPIClient().get_groups() + # Change from list of group dictionaries to dictionary of roles. That way we can handle being both + # a member and an admin of a group. + groups_on_anvil = {} + for group_details in response.json(): + group_name = group_details["groupName"] + role = group_details["role"].lower() + try: + groups_on_anvil[group_name] = groups_on_anvil[group_name] + [role] + except KeyError: + groups_on_anvil[group_name] = [role] + # Audit groups that exist in the app. + for group in ManagedGroup.objects.all(): + model_instance_result = base.ModelInstanceResult(group) + try: + group_roles = groups_on_anvil.pop(group.name) + except KeyError: + # Check if the group actually does exist but we're not a member of it. + try: + # If this returns a 404 error, then the group actually does not exist. + response = AnVILAPIClient().get_group_email(group.name) + if group.is_managed_by_app: + model_instance_result.add_error(self.ERROR_DIFFERENT_ROLE) + + except AnVILAPIError404: + model_instance_result.add_error(self.ERROR_NOT_IN_ANVIL) + # Perhaps we want to add has_app_as_member as a field and check that. + else: + # Check role. + if group.is_managed_by_app: + if "admin" not in group_roles: + model_instance_result.add_error(self.ERROR_DIFFERENT_ROLE) + else: + membership_audit = ManagedGroupMembershipAudit(group) + membership_audit.run_audit() + if not membership_audit.ok(): + model_instance_result.add_error(self.ERROR_GROUP_MEMBERSHIP) + elif not group.is_managed_by_app and "admin" in group_roles: + model_instance_result.add_error(self.ERROR_DIFFERENT_ROLE) + # Add the final result for this group to the class results. + self.add_result(model_instance_result) + + # Check for groups that exist on AnVIL but not the app. + for group_name in groups_on_anvil: + # Only report the ones where the app is an admin. + if "admin" in groups_on_anvil[group_name]: + self.add_result(base.NotInAppResult(group_name)) + + +class ManagedGroupMembershipNotInAppResult(base.NotInAppResult): + """Class to store a not in app audit result for a specific ManagedGroupMembership record.""" + + def __init__(self, *args, group=None, email=None, role=None, **kwargs): + super().__init__(*args, **kwargs) + self.group = group + self.email = email + self.role = role + + +class ManagedGroupMembershipNotInAppTable(base.NotInAppTable): + group = tables.Column() + email = tables.Column() + role = tables.Column() + ignore = tables.TemplateColumn( + template_name="anvil_consortium_manager/snippets/audit_managedgroupmembership_notinapp_ignore_button.html", + orderable=False, + verbose_name="Ignore?", + ) + + class Meta: + fields = ( + "group", + "email", + "role", + ) + exclude = ("record",) + + +class ManagedGroupMembershipIgnoredTable(base.IgnoredTable): + """A table specific to the IgnoredManagedGroupMembership model.""" + + model_instance = tables.columns.Column(linkify=True, verbose_name="Details") + model_instance__group = tables.columns.Column(linkify=True, verbose_name="Managed group", orderable=False) + model_instance__ignored_email = tables.columns.Column(orderable=False, verbose_name="Ignored email") + model_instance__added_by = tables.columns.Column(orderable=False, verbose_name="Ignored by") + + class Meta: + fields = ( + "model_instance", + "model_instance__group", + "model_instance__ignored_email", + "model_instance__added_by", + "record", + ) + + +class ManagedGroupMembershipAudit(base.AnVILAudit): + """Class that runs an audit for membership of a specific ManagedGroup instance.""" + + # Error strings for this class. + ERROR_ACCOUNT_ADMIN_NOT_IN_ANVIL = "Account not an admin in AnVIL" + """Error when an Account is an admin of a ManagedGroup on the app, but not in AnVIL.""" + + ERROR_ACCOUNT_MEMBER_NOT_IN_ANVIL = "Account not a member in AnVIL" + """Error when an Account is a member of a ManagedGroup on the app, but not in AnVIL.""" + + ERROR_DEACTIVATED_ACCOUNT = "Account is deactivated but still has membership records in the app." + """Error when a deactivated Account still has membership records in the app.""" + + ERROR_GROUP_ADMIN_NOT_IN_ANVIL = "Group not an admin in AnVIL" + """Error when a ManagedGroup is an admin of another ManagedGroup on the app, but not in AnVIL.""" + + ERROR_GROUP_MEMBER_NOT_IN_ANVIL = "Group not a member in AnVIL" + """Error when an ManagedGroup is a member of another ManagedGroup on the app, but not in AnVIL.""" + + not_in_app_table_class = ManagedGroupMembershipNotInAppTable + ignored_table_class = ManagedGroupMembershipIgnoredTable + + def __init__(self, managed_group, *args, **kwargs): + super().__init__(*args, **kwargs) + if not managed_group.is_managed_by_app: + raise AnVILNotGroupAdminError("group {} is not managed by app".format(managed_group.name)) + self.managed_group = managed_group + + def run_audit(self): + """Run an audit on all membership of the managed group.""" + # Get the list of members on AnVIL. + api_client = AnVILAPIClient() + # --- Members --- + response = api_client.get_group_members(self.managed_group.name) + # Convert to case insensitive emails. + members_in_anvil = [x.lower() for x in response.json()] + # Sometimes the service account is also listed as a member. Remove that too. + try: + members_in_anvil.remove(api_client.auth_session.credentials.service_account_email.lower()) + except ValueError: + # Not listed as a member -- this is ok. + pass + # -- Admins --- + response = api_client.get_group_admins(self.managed_group.name) + # Convert to case-insensitive emails. + admins_in_anvil = [x.lower() for x in response.json()] + # Remove the service account as admin. + try: + admins_in_anvil.remove(api_client.auth_session.credentials.service_account_email.lower()) + except ValueError: + # Not listed as an admin -- this is ok because it could be via group membership. + pass + + # Check group-account membership. + for membership in self.managed_group.groupaccountmembership_set.all(): + # Create an audit result instance for this model. + model_instance_result = base.ModelInstanceResult(membership) + # Check for deactivated account memberships. + if membership.account.status == Account.INACTIVE_STATUS: + model_instance_result.add_error(self.ERROR_DEACTIVATED_ACCOUNT) + # Check membership status on AnVIL. + if membership.role == GroupAccountMembership.ADMIN: + try: + admins_in_anvil.remove(membership.account.email.lower()) + except ValueError: + # This is an error - the email is not in the list of admins. + model_instance_result.add_error(self.ERROR_ACCOUNT_ADMIN_NOT_IN_ANVIL) + elif membership.role == GroupAccountMembership.MEMBER: + try: + members_in_anvil.remove(membership.account.email.lower()) + except ValueError: + # This is an error - the email is not in the list of members. + model_instance_result.add_error(self.ERROR_ACCOUNT_MEMBER_NOT_IN_ANVIL) + self.add_result(model_instance_result) + + # Check group-group membership. + for membership in self.managed_group.child_memberships.all(): + model_instance_result = base.ModelInstanceResult(membership) + if membership.role == GroupGroupMembership.ADMIN: + try: + admins_in_anvil.remove(membership.child_group.email.lower()) + except ValueError: + # This email is not in the list of members. + model_instance_result.add_error(self.ERROR_GROUP_ADMIN_NOT_IN_ANVIL) + # Also remove from members if it exists there. + try: + members_in_anvil.remove(membership.child_group.email.lower()) + except ValueError: + # The group is not directly listed as a member, so this is ok. + # It is already an admin. + pass + elif membership.role == GroupGroupMembership.MEMBER: + try: + members_in_anvil.remove(membership.child_group.email.lower()) + except ValueError: + # This email is not in the list of members. + model_instance_result.add_error(self.ERROR_GROUP_MEMBER_NOT_IN_ANVIL) + self.add_result(model_instance_result) + + # Add any admin that the app doesn't know about. + for obj in models.IgnoredManagedGroupMembership.objects.filter(group=self.managed_group): + try: + admins_in_anvil.remove(obj.ignored_email) + record = "{}: {}".format(GroupAccountMembership.ADMIN, obj.ignored_email) + self.add_result(base.IgnoredResult(obj, record=record)) + except ValueError: + try: + members_in_anvil.remove(obj.ignored_email) + record = "{}: {}".format(GroupAccountMembership.MEMBER, obj.ignored_email) + self.add_result(base.IgnoredResult(obj, record=record)) + except ValueError: + # This email is not in the list of members or admins. + self.add_result(base.IgnoredResult(obj, record=None)) + + for member in admins_in_anvil: + record = "{}: {}".format(GroupAccountMembership.ADMIN, member) + self.add_result( + ManagedGroupMembershipNotInAppResult( + record, group=self.managed_group, email=member, role=GroupAccountMembership.ADMIN + ) + ) + # Add any members that the app doesn't know about. + for member in members_in_anvil: + record = "{}: {}".format(GroupAccountMembership.MEMBER, member) + self.add_result( + ManagedGroupMembershipNotInAppResult( + record, group=self.managed_group, email=member, role=GroupAccountMembership.MEMBER + ) + ) diff --git a/anvil_consortium_manager/auditor/audit/workspaces.py b/anvil_consortium_manager/auditor/audit/workspaces.py new file mode 100644 index 00000000..67cf46f1 --- /dev/null +++ b/anvil_consortium_manager/auditor/audit/workspaces.py @@ -0,0 +1,149 @@ +from anvil_consortium_manager.anvil_api import AnVILAPIClient +from anvil_consortium_manager.models import Workspace + +from .base import AnVILAudit, ModelInstanceResult, NotInAppResult + + +class WorkspaceAudit(AnVILAudit): + """Class to runs an audit for Workspace instances.""" + + ERROR_NOT_IN_ANVIL = "Not in AnVIL" + """Error when a Workspace in the app does not exist on AnVIL.""" + + ERROR_NOT_OWNER_ON_ANVIL = "Not an owner on AnVIL" + """Error when the service account running the app is not an owner of the Workspace on AnVIL.""" + + ERROR_DIFFERENT_AUTH_DOMAINS = "Has different auth domains on AnVIL" + """Error when the Workspace has different auth domains in the app and on AnVIL.""" + + ERROR_WORKSPACE_SHARING = "Workspace sharing does not match on AnVIL" + """Error when a Workspace is shared with different ManagedGroups in the app and on AnVIL.""" + + ERROR_DIFFERENT_LOCK = "Workspace lock status does not match on AnVIL" + """Error when the workspace.is_locked status does not match the lock status on AnVIL.""" + + ERROR_DIFFERENT_REQUESTER_PAYS = "Workspace bucket requester_pays status does not match on AnVIL" + """Error when the workspace.is_locked status does not match the lock status on AnVIL.""" + + def run_audit(self): + """Run an audit on Workspaces in the app.""" + # Check the list of workspaces. + fields = [ + "workspace.namespace", + "workspace.name", + "workspace.authorizationDomain", + "workspace.isLocked", + "accessLevel", + ] + response = AnVILAPIClient().list_workspaces(fields=",".join(fields)) + workspaces_on_anvil = response.json() + for workspace in Workspace.objects.all(): + model_instance_result = ModelInstanceResult(workspace) + try: + i = next( + idx + for idx, x in enumerate(workspaces_on_anvil) + if ( + x["workspace"]["name"] == workspace.name + and x["workspace"]["namespace"] == workspace.billing_project.name + ) + ) + except StopIteration: + model_instance_result.add_error(self.ERROR_NOT_IN_ANVIL) + else: + # Check role. + workspace_details = workspaces_on_anvil.pop(i) + if workspace_details["accessLevel"] != "OWNER": + model_instance_result.add_error(self.ERROR_NOT_OWNER_ON_ANVIL) + else: + # Since we're the owner, check workspace access. + sharing_audit = WorkspaceSharingAudit(workspace) + sharing_audit.run_audit() + if not sharing_audit.ok(): + model_instance_result.add_error(self.ERROR_WORKSPACE_SHARING) + # Check auth domains. + auth_domains_on_anvil = [ + x["membersGroupName"] for x in workspace_details["workspace"]["authorizationDomain"] + ] + auth_domains_in_app = workspace.authorization_domains.all().values_list("name", flat=True) + if set(auth_domains_on_anvil) != set(auth_domains_in_app): + model_instance_result.add_error(self.ERROR_DIFFERENT_AUTH_DOMAINS) + # Check lock status. + if workspace.is_locked != workspace_details["workspace"]["isLocked"]: + model_instance_result.add_error(self.ERROR_DIFFERENT_LOCK) + # Check is_requester_pays status. Unfortunately we have to make a separate API call. + response = AnVILAPIClient().get_workspace( + workspace.billing_project.name, + workspace.name, + fields=["bucketOptions"], + ) + if workspace.is_requester_pays != response.json()["bucketOptions"]["requesterPays"]: + model_instance_result.add_error(self.ERROR_DIFFERENT_REQUESTER_PAYS) + + self.add_result(model_instance_result) + + # Check for remaining workspaces on AnVIL where we are OWNER. + not_in_app = [ + "{}/{}".format(x["workspace"]["namespace"], x["workspace"]["name"]) + for x in workspaces_on_anvil + if x["accessLevel"] == "OWNER" + ] + for workspace_name in not_in_app: + self.add_result(NotInAppResult(workspace_name)) + + +class WorkspaceSharingAudit(AnVILAudit): + """Class that runs an audit for sharing of a specific Workspace instance.""" + + ERROR_NOT_SHARED_IN_ANVIL = "Not shared in AnVIL" + """Error when a ManagedGroup has access to a workspace in the app but not on AnVIL.""" + + ERROR_DIFFERENT_ACCESS = "Different access level in AnVIL" + """Error when a ManagedGroup has a different access level for workspace in the app and on AnVIL.""" + + ERROR_DIFFERENT_CAN_SHARE = "can_share value does not match in AnVIL" + """Error when the can_share value for a ManagedGroup does not match what's on AnVIL.""" + + ERROR_DIFFERENT_CAN_COMPUTE = "can_compute value does not match in AnVIL" + """Error when the can_compute value for a ManagedGroup does not match what's on AnVIL.""" + + def __init__(self, workspace, *args, **kwargs): + super().__init__(*args, **kwargs) + self.workspace = workspace + + def run_audit(self): + """Run the audit for all workspace instances.""" + api_client = AnVILAPIClient() + response = api_client.get_workspace_acl(self.workspace.billing_project.name, self.workspace.name) + acl_in_anvil = {k.lower(): v for k, v in response.json()["acl"].items()} + # Remove the service account. + try: + acl_in_anvil.pop(api_client.auth_session.credentials.service_account_email.lower()) + except KeyError: + # In some cases, the workspace is shared with a group we are part of instead of directly with us. + pass + for access in self.workspace.workspacegroupsharing_set.all(): + # Create an audit result instance for this model. + model_instance_result = ModelInstanceResult(access) + try: + access_details = acl_in_anvil.pop(access.group.email.lower()) + except KeyError: + model_instance_result.add_error(self.ERROR_NOT_SHARED_IN_ANVIL) + else: + # Check access level. + if access.access != access_details["accessLevel"]: + model_instance_result.add_error(self.ERROR_DIFFERENT_ACCESS) + # Check can_compute value. + if access.can_compute != access_details["canCompute"]: + model_instance_result.add_error(self.ERROR_DIFFERENT_CAN_COMPUTE) + # Check can_share value -- the app never grants this, so it should always be false. + # Can share should be True for owners and false for others. + can_share = access.access == "OWNER" + if access_details["canShare"] != can_share: + model_instance_result.add_error(self.ERROR_DIFFERENT_CAN_SHARE) + # Save the results for this model instance. + self.add_result(model_instance_result) + + # Add any access that the app doesn't know about. + for key in acl_in_anvil: + self.add_result(NotInAppResult("{}: {}".format(acl_in_anvil[key]["accessLevel"], key))) diff --git a/anvil_consortium_manager/auditor/filters.py b/anvil_consortium_manager/auditor/filters.py new file mode 100644 index 00000000..30992a0b --- /dev/null +++ b/anvil_consortium_manager/auditor/filters.py @@ -0,0 +1,12 @@ +from django_filters import FilterSet + +from anvil_consortium_manager.forms import FilterForm + +from . import models + + +class IgnoredManagedGroupMembershipFilter(FilterSet): + class Meta: + model = models.IgnoredManagedGroupMembership + fields = {"group__name": ["icontains"], "ignored_email": ["icontains"]} + form = FilterForm diff --git a/anvil_consortium_manager/auditor/forms.py b/anvil_consortium_manager/auditor/forms.py new file mode 100644 index 00000000..34ba4555 --- /dev/null +++ b/anvil_consortium_manager/auditor/forms.py @@ -0,0 +1,33 @@ +"""Forms classes for the anvil_consortium_manager app.""" + +from dal import autocomplete, forward +from django import forms + +from anvil_consortium_manager.forms import Bootstrap5MediaFormMixin +from anvil_consortium_manager.models import ManagedGroup + +from . import models + + +class IgnoredManagedGroupMembershipForm(Bootstrap5MediaFormMixin, forms.ModelForm): + """Form for the IgnoredManagedGroupMembership model.""" + + group = forms.ModelChoiceField( + queryset=ManagedGroup.objects.filter(is_managed_by_app=True), + help_text="Only groups managed by this app can be selected.", + widget=autocomplete.ModelSelect2( + url="anvil_consortium_manager:managed_groups:autocomplete", + attrs={ + "data-theme": "bootstrap-5", + }, + forward=(forward.Const(True, "only_managed_by_app"),), + ), + ) + + class Meta: + model = models.IgnoredManagedGroupMembership + fields = ( + "group", + "ignored_email", + "note", + ) diff --git a/anvil_consortium_manager/management/commands/run_anvil_audit.py b/anvil_consortium_manager/auditor/management/commands/run_anvil_audit.py similarity index 69% rename from anvil_consortium_manager/management/commands/run_anvil_audit.py rename to anvil_consortium_manager/auditor/management/commands/run_anvil_audit.py index 1832abaf..15b79549 100644 --- a/anvil_consortium_manager/management/commands/run_anvil_audit.py +++ b/anvil_consortium_manager/auditor/management/commands/run_anvil_audit.py @@ -6,11 +6,17 @@ from django.core.management.base import BaseCommand, CommandError from django.template.loader import render_to_string -from ...anvil_api import AnVILAPIError -from ...audit import audit +from anvil_consortium_manager.anvil_api import AnVILAPIError +from ... import models +from ...audit import accounts as account_audit +from ...audit import base as base_audit +from ...audit import billing_projects as billing_project_audit +from ...audit import managed_groups as managed_group_audit +from ...audit import workspaces as workspace_audit -class ErrorTableWithLink(audit.ErrorTable): + +class ErrorTableWithLink(base_audit.ErrorTable): model_instance = tables.Column( orderable=False, linkify=lambda value, table: "https://{domain}{url}".format( @@ -47,11 +53,14 @@ def add_arguments(self, parser): help="If specified, run audit on a subset of models. Otherwise, the audit will be run on all models.", ) - def _run_audit(self, audit_results, **options): + def _run_audit(self, audit_results, ignore_model=None, **options): """Run the audit for a specific model class.""" email = options["email"] errors_only = options["errors_only"] + # Track the number of ignored records. + n_ignored = 0 + audit_name = audit_results.__class__.__name__ self.stdout.write("Running on {}... ".format(audit_name), ending="") try: @@ -64,19 +73,25 @@ def _run_audit(self, audit_results, **options): self.stdout.write(self.style.ERROR("problems found.")) self.stdout.write(pprint.pformat(audit_results.export(include_verified=False))) else: - self.stdout.write(self.style.SUCCESS("ok!")) + msg = "ok!" + if ignore_model: + n_ignored = ignore_model.objects.all().count() + if n_ignored: + msg += " (ignoring {n_ignored} records)".format(n_ignored=n_ignored) + self.stdout.write(self.style.SUCCESS(msg)) if email and (not errors_only) or (errors_only and not audit_results.ok()): # Set up the email message. subject = "AnVIL audit {} -- {}".format(audit_name, "ok" if audit_results.ok() else "errors!") exported_results = audit_results.export() html_body = render_to_string( - "anvil_consortium_manager/email_audit_report.html", + "auditor/email_audit_report.html", context={ "model_name": audit_name, - "verified_results": audit_results.get_verified_results(), + "audit_results": audit_results, + "n_ignored": n_ignored, "errors_table": ErrorTableWithLink(audit_results.get_error_results()), - "not_in_app_table": audit.NotInAppTable(audit_results.get_not_in_app_results()), + "not_in_app_table": base_audit.NotInAppTable(audit_results.get_not_in_app_results()), }, ) send_mail( @@ -95,13 +110,15 @@ def handle(self, *args, **options): models_to_audit = ["BillingProject", "Account", "ManagedGroup", "Workspace"] if "BillingProject" in models_to_audit: - self._run_audit(audit.BillingProjectAudit(), **options) + self._run_audit(billing_project_audit.BillingProjectAudit(), **options) if "Account" in models_to_audit: - self._run_audit(audit.AccountAudit(), **options) + self._run_audit(account_audit.AccountAudit(), **options) if "ManagedGroup" in models_to_audit: - self._run_audit(audit.ManagedGroupAudit(), **options) + self._run_audit( + managed_group_audit.ManagedGroupAudit(), ignore_model=models.IgnoredManagedGroupMembership, **options + ) if "Workspace" in models_to_audit: - self._run_audit(audit.WorkspaceAudit(), **options) + self._run_audit(workspace_audit.WorkspaceAudit(), **options) diff --git a/anvil_consortium_manager/auditor/migrations/0001_initial.py b/anvil_consortium_manager/auditor/migrations/0001_initial.py new file mode 100644 index 00000000..ede7b8c1 --- /dev/null +++ b/anvil_consortium_manager/auditor/migrations/0001_initial.py @@ -0,0 +1,60 @@ +# Generated by Django 5.0 on 2024-12-17 23:13 + +import django.db.models.deletion +import django_extensions.db.fields +import simple_history.models +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('anvil_consortium_manager', '0019_accountuserarchive'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='HistoricalIgnoredManagedGroupMembership', + fields=[ + ('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')), + ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')), + ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), + ('ignored_email', models.EmailField(help_text='Email address to ignore.', max_length=254)), + ('note', models.TextField(help_text='Note about why this email is being ignored.')), + ('history_id', models.AutoField(primary_key=True, serialize=False)), + ('history_date', models.DateTimeField(db_index=True)), + ('history_change_reason', models.CharField(max_length=100, null=True)), + ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), + ('added_by', models.ForeignKey(blank=True, db_constraint=False, help_text='User who added the record to this table.', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)), + ('group', models.ForeignKey(blank=True, db_constraint=False, help_text='Group where email should be ignored.', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='anvil_consortium_manager.managedgroup')), + ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'historical ignored managed group membership', + 'verbose_name_plural': 'historical ignored managed group memberships', + 'ordering': ('-history_date', '-history_id'), + 'get_latest_by': ('history_date', 'history_id'), + }, + bases=(simple_history.models.HistoricalChanges, models.Model), + ), + migrations.CreateModel( + name='IgnoredManagedGroupMembership', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, verbose_name='created')), + ('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, verbose_name='modified')), + ('ignored_email', models.EmailField(help_text='Email address to ignore.', max_length=254)), + ('note', models.TextField(help_text='Note about why this email is being ignored.')), + ('added_by', models.ForeignKey(help_text='User who added the record to this table.', on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ('group', models.ForeignKey(help_text='Group where email should be ignored.', on_delete=django.db.models.deletion.CASCADE, to='anvil_consortium_manager.managedgroup')), + ], + ), + migrations.AddConstraint( + model_name='ignoredmanagedgroupmembership', + constraint=models.UniqueConstraint(fields=('group', 'ignored_email'), name='unique_group_ignored_email'), + ), + ] diff --git a/anvil_consortium_manager/auditor/migrations/__init__.py b/anvil_consortium_manager/auditor/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/anvil_consortium_manager/auditor/models.py b/anvil_consortium_manager/auditor/models.py new file mode 100644 index 00000000..b20940dc --- /dev/null +++ b/anvil_consortium_manager/auditor/models.py @@ -0,0 +1,47 @@ +from django.conf import settings +from django.db import models +from django.urls import reverse +from django_extensions.db.models import TimeStampedModel +from simple_history.models import HistoricalRecords + + +class IgnoredManagedGroupMembership(TimeStampedModel): + """A model to store audit records that can be ignored during a ManagedGroupMembership audit. + + Right now this model is intended to track "not in app" records that can be ignored.""" + + group = models.ForeignKey( + "anvil_consortium_manager.ManagedGroup", + on_delete=models.CASCADE, + help_text="Group where email should be ignored.", + ) + ignored_email = models.EmailField(help_text="Email address to ignore.") + added_by = models.ForeignKey( + settings.AUTH_USER_MODEL, on_delete=models.PROTECT, help_text="User who added the record to this table." + ) + note = models.TextField(help_text="Note about why this email is being ignored.") + history = HistoricalRecords() + + class Meta: + constraints = [models.UniqueConstraint(fields=["group", "ignored_email"], name="unique_group_ignored_email")] + + def __str__(self): + return "{group} membership: ignoring {email}".format(group=self.group, email=self.ignored_email) + + def save(self, *args, **kwargs): + """Save method to set the email address to lowercase before saving.""" + self.ignored_email = self.ignored_email.lower() + return super().save(*args, **kwargs) + + def get_absolute_url(self): + """Get the absolute url for this object. + + Returns: + str: The absolute url for the object.""" + return reverse( + "anvil_consortium_manager:auditor:managed_groups:membership:by_group:ignored:detail", + kwargs={ + "slug": self.group.name, + "email": self.ignored_email, + }, + ) diff --git a/anvil_consortium_manager/auditor/tables.py b/anvil_consortium_manager/auditor/tables.py new file mode 100644 index 00000000..e1819ab0 --- /dev/null +++ b/anvil_consortium_manager/auditor/tables.py @@ -0,0 +1,23 @@ +import django_tables2 as tables + +from . import models + + +class IgnoredManagedGroupMembershipTable(tables.Table): + """Class to display a IgnoredManagedGroupMembership table.""" + + pk = tables.Column(linkify=True, verbose_name="Details", orderable=False) + group = tables.Column(linkify=True) + + class Meta: + model = models.IgnoredManagedGroupMembership + fields = ( + "pk", + "group", + "ignored_email", + "added_by", + "created", + ) + + def render_pk(self): + return "See details" diff --git a/anvil_consortium_manager/auditor/tests/__init__.py b/anvil_consortium_manager/auditor/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/anvil_consortium_manager/auditor/tests/factories.py b/anvil_consortium_manager/auditor/tests/factories.py new file mode 100644 index 00000000..5b8c455e --- /dev/null +++ b/anvil_consortium_manager/auditor/tests/factories.py @@ -0,0 +1,21 @@ +from django.contrib.auth import get_user_model +from factory import Faker, SubFactory +from factory.django import DjangoModelFactory + +from anvil_consortium_manager.tests.factories import ManagedGroupFactory, UserFactory + +from .. import models + +User = get_user_model() + + +class IgnoredManagedGroupMembershipFactory(DjangoModelFactory): + """A factory for the Account model.""" + + group = SubFactory(ManagedGroupFactory) + ignored_email = Faker("email") + added_by = SubFactory(UserFactory) + note = Faker("sentence") + + class Meta: + model = models.IgnoredManagedGroupMembership diff --git a/anvil_consortium_manager/auditor/tests/test_audit.py b/anvil_consortium_manager/auditor/tests/test_audit.py new file mode 100644 index 00000000..e69de29b diff --git a/anvil_consortium_manager/auditor/tests/test_audit_accounts.py b/anvil_consortium_manager/auditor/tests/test_audit_accounts.py new file mode 100644 index 00000000..e49727a6 --- /dev/null +++ b/anvil_consortium_manager/auditor/tests/test_audit_accounts.py @@ -0,0 +1,175 @@ +import responses +from django.test import TestCase +from faker import Faker + +from anvil_consortium_manager.tests.factories import AccountFactory +from anvil_consortium_manager.tests.utils import AnVILAPIMockTestMixin + +from ..audit import accounts + +fake = Faker() + + +class AccountAuditTest(AnVILAPIMockTestMixin, TestCase): + """Tests for the Account.anvil_audit method.""" + + def get_api_url(self, email): + return self.api_client.sam_entry_point + "/api/users/v1/" + email + + def get_api_json_response(self, email): + id = fake.bothify(text="#" * 21) + return { + "googleSubjectId": id, + "userEmail": email, + "userSubjectId": id, + } + + def test_anvil_audit_no_accounts(self): + """anvil_audit works correct if there are no Accounts in the app.""" + audit_results = accounts.AccountAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + + def test_anvil_audit_one_account_no_errors(self): + """anvil_audit works correct if there is one account in the app and it exists on AnVIL.""" + account = AccountFactory.create() + api_url = self.get_api_url(account.email) + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=self.get_api_json_response(account.email), + ) + audit_results = accounts.AccountAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(account) + self.assertTrue(record_result.ok()) + + def test_anvil_audit_one_account_not_on_anvil(self): + """anvil_audit raises exception if one billing project exists in the app but not on AnVIL.""" + account = AccountFactory.create() + api_url = self.get_api_url(account.email) + self.anvil_response_mock.add( + responses.GET, + api_url, + status=404, + json={"message": "other error"}, + ) + audit_results = accounts.AccountAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(account) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) + + def test_anvil_audit_two_accounts_no_errors(self): + """anvil_audit returns None if if two accounts exist in both the app and AnVIL.""" + account_1 = AccountFactory.create() + api_url_1 = self.get_api_url(account_1.email) + self.anvil_response_mock.add( + responses.GET, + api_url_1, + status=200, + json=self.get_api_json_response(account_1.email), + ) + account_2 = AccountFactory.create() + api_url_2 = self.get_api_url(account_2.email) + self.anvil_response_mock.add( + responses.GET, + api_url_2, + status=200, + json=self.get_api_json_response(account_2.email), + ) + audit_results = accounts.AccountAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 2) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(account_1) + self.assertTrue(record_result.ok()) + record_result = audit_results.get_result_for_model_instance(account_2) + self.assertTrue(record_result.ok()) + + def test_anvil_audit_two_accounts_first_not_on_anvil(self): + """anvil_audit raises exception if two accounts exist in the app but the first is not not on AnVIL.""" + account_1 = AccountFactory.create() + api_url_1 = self.get_api_url(account_1.email) + self.anvil_response_mock.add( + responses.GET, + api_url_1, + status=404, + json={"message": "other error"}, + ) + account_2 = AccountFactory.create() + api_url_2 = self.get_api_url(account_2.email) + self.anvil_response_mock.add( + responses.GET, + api_url_2, + status=200, + json=self.get_api_json_response(account_2.email), + ) + audit_results = accounts.AccountAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(account_1) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) + record_result = audit_results.get_result_for_model_instance(account_2) + self.assertTrue(record_result.ok()) + + def test_anvil_audit_two_accounts_both_missing(self): + """anvil_audit raises exception if there are two accounts that exist in the app but not in AnVIL.""" + account_1 = AccountFactory.create() + api_url_1 = self.get_api_url(account_1.email) + self.anvil_response_mock.add( + responses.GET, + api_url_1, + status=404, + json={"message": "other error"}, + ) + account_2 = AccountFactory.create() + api_url_2 = self.get_api_url(account_2.email) + self.anvil_response_mock.add( + responses.GET, + api_url_2, + status=404, + json={"message": "other error"}, + ) + audit_results = accounts.AccountAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 2) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(account_1) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) + record_result = audit_results.get_result_for_model_instance(account_2) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) + + def test_anvil_audit_deactivated_account(self): + """anvil_audit does not check AnVIL about accounts that are deactivated.""" + account = AccountFactory.create() + account.deactivate() + # No API calls made. + audit_results = accounts.AccountAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) diff --git a/anvil_consortium_manager/auditor/tests/test_audit_base.py b/anvil_consortium_manager/auditor/tests/test_audit_base.py new file mode 100644 index 00000000..01515234 --- /dev/null +++ b/anvil_consortium_manager/auditor/tests/test_audit_base.py @@ -0,0 +1,730 @@ +from django.test import TestCase +from django_tables2 import Table +from faker import Faker + +from anvil_consortium_manager.tests.factories import AccountFactory + +from ..audit import base +from . import factories + +fake = Faker() + + +class ModelInstanceResultTest(TestCase): + def test_init(self): + """Constructor works as expected.""" + obj = AccountFactory.create() + result = base.ModelInstanceResult(obj) + self.assertEqual(result.model_instance, obj) + self.assertEqual(result.errors, set()) + + def test_str(self): + """__str__ method works as expected.""" + obj = AccountFactory.create() + result = base.ModelInstanceResult(obj) + self.assertEqual(str(result), (str(obj))) + + def test_eq_no_errors(self): + """__eq__ method works as expected when there are no errors.""" + obj = AccountFactory.create() + result_1 = base.ModelInstanceResult(obj) + result_2 = base.ModelInstanceResult(obj) + self.assertEqual(result_1, result_2) + + def test_eq_errors(self): + """__eq__ method works as expected when there are errors.""" + obj = AccountFactory.create() + result_1 = base.ModelInstanceResult(obj) + result_1.add_error("foo") + result_2 = base.ModelInstanceResult(obj) + self.assertNotEqual(result_1, result_2) + result_2.add_error("foo") + self.assertEqual(result_1, result_2) + + def test_add_error(self): + """add_error method works as expected.""" + obj = AccountFactory.create() + result = base.ModelInstanceResult(obj) + result.add_error("foo") + self.assertEqual(result.errors, set(["foo"])) + result.add_error("bar") + self.assertEqual(result.errors, set(["foo", "bar"])) + + def test_add_error_duplicate(self): + """can add a second, duplicate error without error.""" + obj = AccountFactory.create() + result = base.ModelInstanceResult(obj) + result.add_error("foo") + self.assertEqual(result.errors, set(["foo"])) + result.add_error("foo") + self.assertEqual(result.errors, set(["foo"])) + + def test_ok_no_errors(self): + """ok method returns True when there are no errors.""" + obj = AccountFactory.create() + result = base.ModelInstanceResult(obj) + self.assertTrue(result.ok()) + + def test_ok_errors(self): + """ok method returns False when there are errors.""" + obj = AccountFactory.create() + result = base.ModelInstanceResult(obj) + result.add_error("foo") + self.assertFalse(result.ok()) + + +class NotInAppResultTest(TestCase): + def test_init(self): + """Constructor works as expected.""" + result = base.NotInAppResult("foo bar") + self.assertEqual(result.record, "foo bar") + + def test_str(self): + """__str__ method works as expected.""" + result = base.NotInAppResult("foo bar") + self.assertEqual(str(result), "foo bar") + + def test_eq(self): + """__eq__ method works as expected.""" + result = base.NotInAppResult("foo") + self.assertEqual(base.NotInAppResult("foo"), result) + self.assertNotEqual(base.NotInAppResult("bar"), result) + + +class IgnoredResultTest(TestCase): + def test_init(self): + """Constructor works as expected.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create() + result = base.IgnoredResult(obj, record="foo") + self.assertEqual(result.model_instance, obj) + self.assertEqual(result.record, "foo") + + def test_str(self): + """__str__ method works as expected.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create() + result = base.IgnoredResult(obj, record="foo") + self.assertEqual(str(result), "foo") + + def test_eq(self): + """__eq__ method works as expected when there are no errors.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create() + result_1 = base.IgnoredResult(obj, record="foo") + result_2 = base.IgnoredResult(obj, record="foo") + self.assertEqual(result_1, result_2) + + def test_eq_not_equal_obj(self): + """__eq__ method works as expected when there are no errors.""" + obj_1 = factories.IgnoredManagedGroupMembershipFactory.create() + result_1 = base.IgnoredResult(obj_1, record="foo") + obj_2 = factories.IgnoredManagedGroupMembershipFactory.create() + result_2 = base.IgnoredResult(obj_2, record="foo") + self.assertNotEqual(result_1, result_2) + + def test_eq_not_equal_record(self): + """__eq__ method works as expected when there are no errors.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create() + result_1 = base.IgnoredResult(obj, record="foo") + result_2 = base.IgnoredResult(obj, record="bar") + self.assertNotEqual(result_1, result_2) + + +class VerifiedTableTest(TestCase): + def test_zero_rows(self): + results = [] + table = base.VerifiedTable(results) + self.assertEqual(len(table.rows), 0) + + def test_one_row(self): + results = [base.ModelInstanceResult(AccountFactory())] + table = base.VerifiedTable(results) + self.assertEqual(len(table.rows), 1) + + def test_two_rows(self): + results = [ + base.ModelInstanceResult(AccountFactory()), + base.ModelInstanceResult(AccountFactory()), + ] + table = base.VerifiedTable(results) + self.assertEqual(len(table.rows), 2) + + +class ErrorTableTest(TestCase): + def test_zero_rows(self): + results = [] + table = base.ErrorTable(results) + self.assertEqual(len(table.rows), 0) + + def test_one_row(self): + results = [base.ModelInstanceResult(AccountFactory())] + table = base.ErrorTable(results) + self.assertEqual(len(table.rows), 1) + + def test_two_rows(self): + result_1 = base.ModelInstanceResult(AccountFactory()) + result_1.add_error("foo") + result_2 = base.ModelInstanceResult(AccountFactory()) + result_2.add_error("bar") + results = [result_1, result_2] + table = base.ErrorTable(results) + self.assertEqual(len(table.rows), 2) + + +class AnVILAuditTest(TestCase): + """Tests for the AnVILAudit abstract base class.""" + + def setUp(self): + super().setUp() + + class GenericAudit(base.AnVILAudit): + TEST_ERROR_1 = "Test error 1" + TEST_ERROR_2 = "Test error 2" + + self.audit_results = GenericAudit() + # It doesn't matter what model we use at this point, so just pick Account. + self.model_factory = AccountFactory + + def test_init(self): + """Init method works as expected.""" + self.assertEqual(len(self.audit_results._model_instance_results), 0) + self.assertEqual(len(self.audit_results._not_in_app_results), 0) + + def test_ok_no_results(self): + """ok() returns True when there are no results.""" + self.assertTrue(self.audit_results.ok()) + + def test_ok_one_result_ok(self): + """ok() returns True when there is one ok result.""" + model_instance_result = base.ModelInstanceResult(self.model_factory()) + self.audit_results.add_result(model_instance_result) + self.assertTrue(self.audit_results.ok()) + + def test_ok_two_results_ok(self): + """ok() returns True when there is one ok result.""" + model_instance_result_1 = base.ModelInstanceResult(self.model_factory()) + self.audit_results.add_result(model_instance_result_1) + model_instance_result_2 = base.ModelInstanceResult(self.model_factory()) + self.audit_results.add_result(model_instance_result_2) + self.assertTrue(self.audit_results.ok()) + + def test_ok_one_result_with_errors(self): + """ok() returns True when there is one ok result.""" + model_instance_result = base.ModelInstanceResult(self.model_factory()) + model_instance_result.add_error("foo") + self.audit_results.add_result(model_instance_result) + self.assertFalse(self.audit_results.ok()) + + def test_ok_one_not_in_app(self): + """ok() returns True when there are no results.""" + self.audit_results.add_result(base.NotInAppResult("foo")) + self.assertFalse(self.audit_results.ok()) + + def test_ok_one_ignored(self): + """ok() returns True when there is one ignored result.""" + self.audit_results.add_result( + base.IgnoredResult(factories.IgnoredManagedGroupMembershipFactory.create(), "foo"), + ) + self.assertTrue(self.audit_results.ok()) + + def test_run_audit_not_implemented(self): + with self.assertRaises(NotImplementedError): + self.audit_results.run_audit() + + def test_add_result_not_in_app(self): + """Can add a NotInAppResult.""" + not_in_app_result = base.NotInAppResult("foo") + self.audit_results.add_result(not_in_app_result) + self.assertEqual(len(self.audit_results._not_in_app_results), 1) + + def test_add_result_ignored(self): + """Can add an IgnoredResult.""" + ignored_result = base.IgnoredResult(factories.IgnoredManagedGroupMembershipFactory.create(), record="foo") + self.audit_results.add_result(ignored_result) + self.assertEqual(len(self.audit_results._ignored_results), 1) + + def test_add_result_wrong_class(self): + """Can add a NotInAppResult.""" + with self.assertRaises(ValueError): + self.audit_results.add_result("foo") + + def test_add_result_duplicate_not_in_app(self): + """Cannot add a duplicate NotInAppResult.""" + not_in_app_result = base.NotInAppResult("foo") + self.audit_results.add_result(not_in_app_result) + # import ipdb; ipdb.set_trace() + with self.assertRaises(ValueError): + self.audit_results.add_result(not_in_app_result) + self.assertEqual(len(self.audit_results._not_in_app_results), 1) + + def test_add_result_not_in_app_same_record(self): + """Cannot add a duplicate NotInAppResult.""" + not_in_app_result = base.NotInAppResult("foo") + self.audit_results.add_result(not_in_app_result) + # import ipdb; ipdb.set_trace() + with self.assertRaises(ValueError): + self.audit_results.add_result(base.NotInAppResult("foo")) + self.assertEqual(len(self.audit_results._not_in_app_results), 1) + + def test_add_result_ignored_duplicate(self): + """Can add an IgnoredResult.""" + ignored_result = base.IgnoredResult(factories.IgnoredManagedGroupMembershipFactory.create(), record="foo") + self.audit_results.add_result(ignored_result) + with self.assertRaises(ValueError): + self.audit_results.add_result(ignored_result) + + def test_add_result_ignored_equal(self): + """Can add an IgnoredResult.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create() + ignored_result_1 = base.IgnoredResult(obj, record="foo") + ignored_result_2 = base.IgnoredResult(obj, record="foo") + self.audit_results.add_result(ignored_result_1) + with self.assertRaises(ValueError): + self.audit_results.add_result(ignored_result_2) + + def test_add_result_model_instance(self): + """Can add a model instance result.""" + model_instance_result = base.ModelInstanceResult(self.model_factory()) + self.audit_results.add_result(model_instance_result) + self.assertEqual(len(self.audit_results._model_instance_results), 1) + + def test_add_result_duplicate_model_instance_result(self): + """Cannot add a duplicate model instance result.""" + model_instance_result = base.ModelInstanceResult(self.model_factory()) + self.audit_results.add_result(model_instance_result) + # import ipdb; ipdb.set_trace() + with self.assertRaises(ValueError): + self.audit_results.add_result(model_instance_result) + self.assertEqual(len(self.audit_results._model_instance_results), 1) + + def test_add_result_second_result_for_same_model_instance(self): + obj = self.model_factory() + model_instance_result_1 = base.ModelInstanceResult(obj) + self.audit_results.add_result(model_instance_result_1) + model_instance_result_2 = base.ModelInstanceResult(obj) + # import ipdb; ipdb.set_trace() + with self.assertRaises(ValueError): + self.audit_results.add_result(model_instance_result_2) + self.assertEqual(len(self.audit_results._model_instance_results), 1) + self.assertEqual(self.audit_results._model_instance_results, [model_instance_result_1]) + + def test_add_result_second_result_for_same_model_instance_with_error(self): + obj = self.model_factory() + model_instance_result_1 = base.ModelInstanceResult(obj) + self.audit_results.add_result(model_instance_result_1) + model_instance_result_2 = base.ModelInstanceResult(obj) + model_instance_result_2.add_error("Foo") + with self.assertRaises(ValueError): + self.audit_results.add_result(model_instance_result_2) + self.assertEqual(len(self.audit_results._model_instance_results), 1) + self.assertEqual(self.audit_results._model_instance_results, [model_instance_result_1]) + + def test_get_result_for_model_instance_no_matches(self): + obj = self.model_factory() + base.ModelInstanceResult(obj) + with self.assertRaises(ValueError): + self.audit_results.get_result_for_model_instance(obj) + + def test_get_result_for_model_instance_one_match(self): + obj = self.model_factory() + model_instance_result = base.ModelInstanceResult(obj) + self.audit_results.add_result(model_instance_result) + result = self.audit_results.get_result_for_model_instance(obj) + self.assertIs(result, model_instance_result) + + def test_get_verified_results_no_results(self): + """get_verified_results returns an empty list when there are no results.""" + self.assertEqual(len(self.audit_results.get_verified_results()), 0) + + def test_get_verified_results_one_verified_result(self): + """get_verified_results returns a list when there is one result.""" + model_instance_result = base.ModelInstanceResult(self.model_factory()) + self.audit_results.add_result(model_instance_result) + self.assertEqual(len(self.audit_results.get_verified_results()), 1) + self.assertIn(model_instance_result, self.audit_results.get_verified_results()) + + def test_get_error_results_two_verified_result(self): + """get_verified_results returns a list of lenght two when there are two verified results.""" + model_instance_result_1 = base.ModelInstanceResult(self.model_factory()) + self.audit_results.add_result(model_instance_result_1) + model_instance_result_2 = base.ModelInstanceResult(self.model_factory()) + self.audit_results.add_result(model_instance_result_2) + self.assertEqual(len(self.audit_results.get_verified_results()), 2) + self.assertIn(model_instance_result_1, self.audit_results.get_verified_results()) + self.assertIn(model_instance_result_2, self.audit_results.get_verified_results()) + + def test_get_verified_results_one_error_result(self): + """get_verified_results returns a list of lenght zero when there is one error result.""" + model_instance_result = base.ModelInstanceResult(self.model_factory()) + model_instance_result.add_error("foo") + self.audit_results.add_result(model_instance_result) + self.assertEqual(len(self.audit_results.get_verified_results()), 0) + + def test_get_verified_results_one_not_in_app_result(self): + """get_verified_results returns a list of lenght zero when there is one not_in_app result.""" + self.audit_results.add_result(base.NotInAppResult("foo")) + self.assertEqual(len(self.audit_results.get_verified_results()), 0) + + def test_get_verified_results_one_ignored_result(self): + """get_verified_results returns a list of lenght zero when there is one ignored result.""" + self.audit_results.add_result( + base.IgnoredResult(factories.IgnoredManagedGroupMembershipFactory.create(), record="foo") + ) + self.assertEqual(len(self.audit_results.get_verified_results()), 0) + + def test_get_error_results_no_results(self): + """get_error_results returns an empty list when there are no results.""" + self.assertEqual(len(self.audit_results.get_error_results()), 0) + + def test_get_error_results_one_verified_result(self): + """get_error_results returns a list of length zero when there is one verified result.""" + model_instance_result = base.ModelInstanceResult(self.model_factory()) + self.audit_results.add_result(model_instance_result) + self.assertEqual(len(self.audit_results.get_error_results()), 0) + + def test_get_error_results_one_error_result(self): + """get_error_results returns a list of lenght one when there is one error result.""" + model_instance_result = base.ModelInstanceResult(self.model_factory()) + model_instance_result.add_error("foo") + self.audit_results.add_result(model_instance_result) + self.assertEqual(len(self.audit_results.get_error_results()), 1) + self.assertIn(model_instance_result, self.audit_results.get_error_results()) + + def test_get_error_results_two_error_result(self): + """get_error_results returns a list of lenght two when there is one result.""" + model_instance_result_1 = base.ModelInstanceResult(self.model_factory()) + model_instance_result_1.add_error("foo") + self.audit_results.add_result(model_instance_result_1) + model_instance_result_2 = base.ModelInstanceResult(self.model_factory()) + model_instance_result_2.add_error("foo") + self.audit_results.add_result(model_instance_result_2) + self.assertEqual(len(self.audit_results.get_error_results()), 2) + self.assertIn(model_instance_result_1, self.audit_results.get_error_results()) + self.assertIn(model_instance_result_2, self.audit_results.get_error_results()) + + def test_get_error_results_one_not_in_app_result(self): + """get_error_results returns a list of length zero when there is one not_in_app result.""" + self.audit_results.add_result(base.NotInAppResult("foo")) + self.assertEqual(len(self.audit_results.get_error_results()), 0) + + def test_get_error_results_one_ignored_result(self): + """get_verified_results returns a list of lenght zero when there is one ignored result.""" + self.audit_results.add_result( + base.IgnoredResult(factories.IgnoredManagedGroupMembershipFactory.create(), record="foo") + ) + self.assertEqual(len(self.audit_results.get_error_results()), 0) + + def test_get_not_in_app_results_no_results(self): + """get_not_in_app_results returns an empty list when there are no results.""" + self.assertEqual(len(self.audit_results.get_not_in_app_results()), 0) + + def test_get_not_in_app_results_one_verified_result(self): + """get_not_in_app_results returns a list of length zero when there is one verified result.""" + model_instance_result = base.ModelInstanceResult(self.model_factory()) + self.audit_results.add_result(model_instance_result) + self.assertEqual(len(self.audit_results.get_not_in_app_results()), 0) + + def test_get_not_in_app_results_one_error_result(self): + """get_not_in_app_results returns a list of lenght one when there is one error result.""" + model_instance_result = base.ModelInstanceResult(self.model_factory()) + model_instance_result.add_error("foo") + self.audit_results.add_result(model_instance_result) + self.assertEqual(len(self.audit_results.get_not_in_app_results()), 0) + + def test_get_not_in_app_results_one_not_in_app_result(self): + """get_not_in_app_results returns a list of length zero when there is one not_in_app result.""" + result = base.NotInAppResult("foo") + self.audit_results.add_result(result) + self.assertEqual(len(self.audit_results.get_not_in_app_results()), 1) + self.assertIn(result, self.audit_results.get_not_in_app_results()) + + def test_get_not_in_app_results_two_not_in_app_results(self): + """get_not_in_app_results returns a list of lenght two when there is one result.""" + result_1 = base.NotInAppResult("foo") + self.audit_results.add_result(result_1) + result_2 = base.NotInAppResult("bar") + self.audit_results.add_result(result_2) + self.assertEqual(len(self.audit_results.get_not_in_app_results()), 2) + self.assertIn(result_1, self.audit_results.get_not_in_app_results()) + self.assertIn(result_2, self.audit_results.get_not_in_app_results()) + + def test_get_not_in_app_results_one_ignored_result(self): + """get_verified_results returns a list of lenght zero when there is one ignored result.""" + self.audit_results.add_result( + base.IgnoredResult(factories.IgnoredManagedGroupMembershipFactory.create(), record="foo") + ) + self.assertEqual(len(self.audit_results.get_not_in_app_results()), 0) + + def test_get_ignored_results_no_results(self): + """get_ignored_results returns an empty list when there are no results.""" + self.assertEqual(len(self.audit_results.get_ignored_results()), 0) + + def test_get_ignored_results_one_result(self): + """get_ignored_results returns a list when there is one result.""" + result = base.IgnoredResult(factories.IgnoredManagedGroupMembershipFactory.create(), record="foo") + self.audit_results.add_result(result) + self.assertEqual(len(self.audit_results.get_ignored_results()), 1) + self.assertIn(result, self.audit_results.get_ignored_results()) + + def test_get_ignored_results_two_results(self): + """get_ignored_results returns a list of lenght two when there are two verified results.""" + result_1 = base.IgnoredResult(factories.IgnoredManagedGroupMembershipFactory.create(), record="foo") + self.audit_results.add_result(result_1) + result_2 = base.IgnoredResult(factories.IgnoredManagedGroupMembershipFactory.create(), record="bar") + self.audit_results.add_result(result_2) + self.assertEqual(len(self.audit_results.get_ignored_results()), 2) + self.assertIn(result_1, self.audit_results.get_ignored_results()) + self.assertIn(result_2, self.audit_results.get_ignored_results()) + + def test_get_ignored_results_one_verified_result(self): + """get_ignored_results returns a list of lenght zero when there is one verified result.""" + self.audit_results.add_result(base.ModelInstanceResult(self.model_factory())) + self.assertEqual(len(self.audit_results.get_ignored_results()), 0) + + def test_get_ignored_results_one_error_result(self): + """get_ignored_results returns a list of lenght zero when there is one error result.""" + model_instance_result = base.ModelInstanceResult(self.model_factory()) + model_instance_result.add_error("foo") + self.audit_results.add_result(model_instance_result) + self.assertEqual(len(self.audit_results.get_ignored_results()), 0) + + def test_get_ignored_results_one_not_in_app_result(self): + """get_ignored_results returns a list of length zero when there is one not_in_app result.""" + self.audit_results.add_result(base.NotInAppResult("foo")) + self.assertEqual(len(self.audit_results.get_ignored_results()), 0) + + def test_get_verified_table_no_results(self): + table = self.audit_results.get_verified_table() + self.assertIsInstance(table, base.VerifiedTable) + self.assertEqual(len(table.rows), 0) + + def test_get_verified_table_one_result(self): + model_instance_result = base.ModelInstanceResult(self.model_factory()) + self.audit_results.add_result(model_instance_result) + table = self.audit_results.get_verified_table() + self.assertEqual(len(table.rows), 1) + self.assertIn(model_instance_result, table.data) + + def test_get_verified_table_two_results(self): + model_instance_result_1 = base.ModelInstanceResult(self.model_factory()) + self.audit_results.add_result(model_instance_result_1) + model_instance_result_2 = base.ModelInstanceResult(self.model_factory()) + self.audit_results.add_result(model_instance_result_2) + table = self.audit_results.get_verified_table() + self.assertEqual(len(table.rows), 2) + self.assertIn(model_instance_result_1, table.data) + self.assertIn(model_instance_result_2, table.data) + + def test_get_verified_table_custom_class(self): + class CustomTable(Table): + pass + + class CustomAudit(base.AnVILAudit): + verified_table_class = CustomTable + + audit_results = CustomAudit() + model_instance_result = base.ModelInstanceResult(self.model_factory()) + audit_results.add_result(model_instance_result) + table = audit_results.get_verified_table() + self.assertIsInstance(table, CustomTable) + self.assertEqual(len(table.rows), 1) + self.assertIn(model_instance_result, table.data) + + def test_get_error_table_no_results(self): + table = self.audit_results.get_error_table() + self.assertIsInstance(table, base.ErrorTable) + self.assertEqual(len(table.rows), 0) + + def test_get_error_table_one_result(self): + model_instance_result = base.ModelInstanceResult(self.model_factory()) + model_instance_result.add_error("foo") + self.audit_results.add_result(model_instance_result) + table = self.audit_results.get_error_table() + self.assertEqual(len(table.rows), 1) + self.assertIn(model_instance_result, table.data) + + def test_get_error_table_two_results(self): + model_instance_result_1 = base.ModelInstanceResult(self.model_factory()) + model_instance_result_1.add_error("foo") + self.audit_results.add_result(model_instance_result_1) + model_instance_result_2 = base.ModelInstanceResult(self.model_factory()) + model_instance_result_2.add_error("bar") + self.audit_results.add_result(model_instance_result_2) + table = self.audit_results.get_error_table() + self.assertEqual(len(table.rows), 2) + self.assertIn(model_instance_result_1, table.data) + self.assertIn(model_instance_result_2, table.data) + + def test_get_error_table_custom_class(self): + class CustomTable(Table): + pass + + class CustomAudit(base.AnVILAudit): + error_table_class = CustomTable + + audit_results = CustomAudit() + model_instance_result = base.ModelInstanceResult(self.model_factory()) + model_instance_result.add_error("foo") + audit_results.add_result(model_instance_result) + table = audit_results.get_error_table() + self.assertIsInstance(table, CustomTable) + self.assertEqual(len(table.rows), 1) + self.assertIn(model_instance_result, table.data) + + def test_get_not_in_app_table_no_results(self): + table = self.audit_results.get_not_in_app_table() + self.assertIsInstance(table, base.NotInAppTable) + self.assertEqual(len(table.rows), 0) + + def test_get_not_in_app_table_one_result(self): + result = base.NotInAppResult("foo") + self.audit_results.add_result(result) + table = self.audit_results.get_not_in_app_table() + self.assertEqual(len(table.rows), 1) + self.assertIn(result, table.data) + + def test_get_not_in_app_table_two_results(self): + result_1 = base.NotInAppResult("foo") + self.audit_results.add_result(result_1) + result_2 = base.NotInAppResult("bar") + self.audit_results.add_result(result_2) + table = self.audit_results.get_not_in_app_table() + self.assertEqual(len(table.rows), 2) + self.assertIn(result_1, table.data) + self.assertIn(result_2, table.data) + + def test_get_not_in_app_table_custom_class(self): + class CustomTable(Table): + pass + + class CustomAudit(base.AnVILAudit): + not_in_app_table_class = CustomTable + + audit_results = CustomAudit() + result = base.NotInAppResult("foo") + audit_results.add_result(result) + table = audit_results.get_not_in_app_table() + self.assertIsInstance(table, CustomTable) + self.assertEqual(len(table.rows), 1) + self.assertIn(result, table.data) + + def test_get_ignored_table_no_results(self): + table = self.audit_results.get_ignored_table() + self.assertIsInstance(table, base.IgnoredTable) + self.assertEqual(len(table.rows), 0) + + def test_get_ignored_table_one_result(self): + result = base.IgnoredResult(factories.IgnoredManagedGroupMembershipFactory.create()) + self.audit_results.add_result(result) + table = self.audit_results.get_ignored_table() + self.assertEqual(len(table.rows), 1) + self.assertIn(result, table.data) + + def test_get_ignored_table_two_results(self): + result_1 = base.IgnoredResult(factories.IgnoredManagedGroupMembershipFactory.create()) + self.audit_results.add_result(result_1) + result_2 = base.IgnoredResult(factories.IgnoredManagedGroupMembershipFactory.create()) + self.audit_results.add_result(result_2) + table = self.audit_results.get_ignored_table() + self.assertEqual(len(table.rows), 2) + self.assertIn(result_1, table.data) + self.assertIn(result_2, table.data) + + def test_get_ignored_table_custom_class(self): + class CustomTable(Table): + pass + + class CustomAudit(base.AnVILAudit): + ignored_table_class = CustomTable + + audit_results = CustomAudit() + result = base.IgnoredResult(factories.IgnoredManagedGroupMembershipFactory.create()) + audit_results.add_result(result) + table = audit_results.get_ignored_table() + self.assertIsInstance(table, CustomTable) + self.assertEqual(len(table.rows), 1) + self.assertIn(result, table.data) + + def test_export(self): + # One Verified result. + verified_result = base.ModelInstanceResult(self.model_factory()) + self.audit_results.add_result(verified_result) + # One error result. + error_result = base.ModelInstanceResult(self.model_factory()) + error_result.add_error("foo") + self.audit_results.add_result(error_result) + # Not in app result. + not_in_app_result = base.NotInAppResult("bar") + self.audit_results.add_result(not_in_app_result) + # Ignored result + ignored_result = base.IgnoredResult(factories.IgnoredManagedGroupMembershipFactory.create(), record="foobar") + self.audit_results.add_result(ignored_result) + # Check export. + exported_data = self.audit_results.export() + self.assertIn("verified", exported_data) + self.assertEqual( + exported_data["verified"], + [ + { + "id": verified_result.model_instance.pk, + "instance": verified_result.model_instance, + } + ], + ) + self.assertIn("errors", exported_data) + self.assertEqual( + exported_data["errors"], + [ + { + "id": error_result.model_instance.pk, + "instance": error_result.model_instance, + "errors": ["foo"], + } + ], + ) + self.assertIn("not_in_app", exported_data) + self.assertEqual(exported_data["not_in_app"], ["bar"]) + self.assertIn("ignored", exported_data) + self.assertEqual( + exported_data["ignored"], + [ + { + "id": ignored_result.model_instance.pk, + "instance": ignored_result.model_instance, + "record": "foobar", + } + ], + ) + + def test_export_include_verified_false(self): + exported_data = self.audit_results.export(include_verified=False) + self.assertNotIn("verified", exported_data) + self.assertIn("errors", exported_data) + self.assertIn("not_in_app", exported_data) + self.assertIn("ignored", exported_data) + + def test_export_include_errors_false(self): + exported_data = self.audit_results.export(include_errors=False) + self.assertIn("verified", exported_data) + self.assertNotIn("errors", exported_data) + self.assertIn("not_in_app", exported_data) + self.assertIn("ignored", exported_data) + + def test_export_include_not_in_app_false(self): + exported_data = self.audit_results.export(include_not_in_app=False) + self.assertIn("verified", exported_data) + self.assertIn("errors", exported_data) + self.assertNotIn("not_in_app", exported_data) + self.assertIn("ignored", exported_data) + + def test_export_include_ignored_false(self): + exported_data = self.audit_results.export(include_ignored=False) + self.assertIn("verified", exported_data) + self.assertIn("errors", exported_data) + self.assertIn("not_in_app", exported_data) + self.assertNotIn("ignored", exported_data) + + def test_export_not_in_app_sorted(self): + """export sorts the not_in_app results.""" + self.audit_results.add_result(base.NotInAppResult("foo")) + self.audit_results.add_result(base.NotInAppResult("bar")) + exported_data = self.audit_results.export() + self.assertEqual(exported_data["not_in_app"], ["bar", "foo"]) diff --git a/anvil_consortium_manager/auditor/tests/test_audit_billing_projects.py b/anvil_consortium_manager/auditor/tests/test_audit_billing_projects.py new file mode 100644 index 00000000..561ceae3 --- /dev/null +++ b/anvil_consortium_manager/auditor/tests/test_audit_billing_projects.py @@ -0,0 +1,171 @@ +import responses +from django.test import TestCase +from faker import Faker + +from anvil_consortium_manager.tests.factories import BillingProjectFactory +from anvil_consortium_manager.tests.utils import AnVILAPIMockTestMixin + +from ..audit import billing_projects + +fake = Faker() + + +class BillingProjectAuditTest(AnVILAPIMockTestMixin, TestCase): + """Tests for the BillingProject.anvil_audit method.""" + + def get_api_url(self, billing_project_name): + return self.api_client.rawls_entry_point + "/api/billing/v2/" + billing_project_name + + def get_api_json_response(self): + return { + "roles": ["User"], + } + + def test_anvil_audit_no_billing_projects(self): + """anvil_audit works correct if there are no billing projects in the app.""" + audit_results = billing_projects.BillingProjectAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + + def test_anvil_audit_one_billing_project_no_errors(self): + """anvil_audit works correct if one billing project exists in the app and in AnVIL.""" + billing_project = BillingProjectFactory.create(has_app_as_user=True) + api_url = self.get_api_url(billing_project.name) + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=self.get_api_json_response(), + ) + audit_results = billing_projects.BillingProjectAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(billing_project) + self.assertTrue(record_result.ok()) + + def test_anvil_audit_one_billing_project_not_on_anvil(self): + """anvil_audit raises exception with one billing project exists in the app but not on AnVIL.""" + billing_project = BillingProjectFactory.create(has_app_as_user=True) + api_url = self.get_api_url(billing_project.name) + self.anvil_response_mock.add( + responses.GET, + api_url, + status=404, + json={"message": "other error"}, + ) + audit_results = billing_projects.BillingProjectAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(billing_project) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) + + def test_anvil_audit_two_billing_projects_no_errors(self): + """anvil_audit returns None if there are two billing projects and both exist on AnVIL.""" + billing_project_1 = BillingProjectFactory.create(has_app_as_user=True) + api_url_1 = self.get_api_url(billing_project_1.name) + self.anvil_response_mock.add( + responses.GET, + api_url_1, + status=200, + json=self.get_api_json_response(), + ) + billing_project_2 = BillingProjectFactory.create(has_app_as_user=True) + api_url_2 = self.get_api_url(billing_project_2.name) + self.anvil_response_mock.add( + responses.GET, + api_url_2, + status=200, + json=self.get_api_json_response(), + ) + audit_results = billing_projects.BillingProjectAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 2) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(billing_project_1) + self.assertTrue(record_result.ok()) + record_result = audit_results.get_result_for_model_instance(billing_project_2) + self.assertTrue(record_result.ok()) + + def test_anvil_audit_two_billing_projects_first_not_on_anvil(self): + """anvil_audit raises exception if two billing projects exist in the app but the first is not on AnVIL.""" + billing_project_1 = BillingProjectFactory.create(has_app_as_user=True) + api_url_1 = self.get_api_url(billing_project_1.name) + self.anvil_response_mock.add( + responses.GET, + api_url_1, + status=404, + json={"message": "other error"}, + ) + billing_project_2 = BillingProjectFactory.create(has_app_as_user=True) + api_url_2 = self.get_api_url(billing_project_2.name) + self.anvil_response_mock.add( + responses.GET, + api_url_2, + status=200, + json=self.get_api_json_response(), + ) + audit_results = billing_projects.BillingProjectAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(billing_project_1) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) + record_result = audit_results.get_result_for_model_instance(billing_project_2) + self.assertTrue(record_result.ok()) + + def test_anvil_audit_two_billing_projects_both_missing(self): + """anvil_audit raises exception if there are two billing projects that exist in the app but not in AnVIL.""" + billing_project_1 = BillingProjectFactory.create(has_app_as_user=True) + api_url_1 = self.get_api_url(billing_project_1.name) + self.anvil_response_mock.add( + responses.GET, + api_url_1, + status=404, + json={"message": "other error"}, + ) + billing_project_2 = BillingProjectFactory.create(has_app_as_user=True) + api_url_2 = self.get_api_url(billing_project_2.name) + self.anvil_response_mock.add( + responses.GET, + api_url_2, + status=404, + json={"message": "other error"}, + ) + audit_results = billing_projects.BillingProjectAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 2) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(billing_project_1) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) + record_result = audit_results.get_result_for_model_instance(billing_project_2) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) + + def test_anvil_audit_ignore_not_has_app_has_user(self): + """anvil_audit does not check AnVIL about billing projects that do not have the app as a user.""" + BillingProjectFactory.create(has_app_as_user=False) + # No API calls made. + audit_results = billing_projects.BillingProjectAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) diff --git a/anvil_consortium_manager/auditor/tests/test_audit_managed_groups.py b/anvil_consortium_manager/auditor/tests/test_audit_managed_groups.py new file mode 100644 index 00000000..3f8e244e --- /dev/null +++ b/anvil_consortium_manager/auditor/tests/test_audit_managed_groups.py @@ -0,0 +1,2205 @@ +import responses +from django.test import TestCase +from faker import Faker + +from anvil_consortium_manager.exceptions import AnVILNotGroupAdminError +from anvil_consortium_manager.models import ( + Account, + GroupAccountMembership, + GroupGroupMembership, +) +from anvil_consortium_manager.tests.api_factories import ( + ErrorResponseFactory, + GetGroupMembershipAdminResponseFactory, + GetGroupMembershipResponseFactory, + GetGroupsResponseFactory, + GroupDetailsAdminFactory, + GroupDetailsFactory, + GroupDetailsMemberFactory, +) +from anvil_consortium_manager.tests.factories import ( + GroupAccountMembershipFactory, + GroupGroupMembershipFactory, + ManagedGroupFactory, +) +from anvil_consortium_manager.tests.utils import AnVILAPIMockTestMixin + +from ..audit import base, managed_groups +from . import factories + +fake = Faker() + + +class ManagedGroupAuditTest(AnVILAPIMockTestMixin, TestCase): + """Tests forthe ManagedGroup.anvil_audit method.""" + + def get_api_groups_url(self): + """Return the API url being called by the method.""" + return self.api_client.sam_entry_point + "/api/groups/v1" + + def get_api_url_members(self, group_name): + """Return the API url being called by the method.""" + return self.api_client.sam_entry_point + "/api/groups/v1/" + group_name + "/member" + + def get_api_url_admins(self, group_name): + """Return the API url being called by the method.""" + return self.api_client.sam_entry_point + "/api/groups/v1/" + group_name + "/admin" + + def test_anvil_audit_no_groups(self): + """anvil_audit works correct if there are no ManagedGroups in the app.""" + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=GetGroupsResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + + def test_anvil_audit_one_group_managed_by_app_no_errors(self): + """anvil_audit works correct if there is one group in the app and it exists on AnVIL.""" + group = ManagedGroupFactory.create(is_managed_by_app=True) + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=GetGroupsResponseFactory(response=[GroupDetailsAdminFactory(groupName=group.name)]).response, + ) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(group) + self.assertTrue(record_result.ok()) + + def test_anvil_audit_one_group_managed_by_app_lowercase_role(self): + """anvil_audit works correct if there is one account in the app and it exists on AnVIL.""" + group = ManagedGroupFactory.create(is_managed_by_app=True) + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=GetGroupsResponseFactory(response=[GroupDetailsAdminFactory(groupName=group.name)]).response, + ) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(group) + self.assertTrue(record_result.ok()) + + def test_anvil_audit_one_group_not_managed_by_app_no_errors(self): + """anvil_audit works correct if there is one account in the app and it exists on AnVIL.""" + group = ManagedGroupFactory.create(is_managed_by_app=False) + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=GetGroupsResponseFactory(response=[GroupDetailsMemberFactory(groupName=group.name)]).response, + ) + audit_results = managed_groups.ManagedGroupAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(group) + self.assertTrue(record_result.ok()) + + def test_anvil_audit_one_group_not_managed_by_app_no_errors_uppercase_role(self): + """anvil_audit works correct if there is one account in the app and it exists on AnVIL.""" + group = ManagedGroupFactory.create(is_managed_by_app=False) + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=GetGroupsResponseFactory(response=[GroupDetailsFactory(groupName=group.name, role="Member")]).response, + ) + audit_results = managed_groups.ManagedGroupAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(group) + self.assertTrue(record_result.ok()) + + def test_anvil_audit_one_group_not_on_anvil(self): + """anvil_audit raises exception if one group exists in the app but not on AnVIL.""" + group = ManagedGroupFactory.create() + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=GetGroupsResponseFactory(n_groups=0).response, + ) + self.anvil_response_mock.add( + responses.GET, + "https://sam.dsde-prod.broadinstitute.org/api/groups/v1/" + group.name, + status=404, + json=ErrorResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(group) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) + + def test_anvil_audit_one_group_on_anvil_but_app_not_in_group_not_managed_by_app( + self, + ): + """anvil_audit is correct if the group is not managed by the app.""" + group = ManagedGroupFactory.create(is_managed_by_app=False) + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=GetGroupsResponseFactory(n_groups=0).response, + ) + # Add the response. + self.anvil_response_mock.add( + responses.GET, + "https://sam.dsde-prod.broadinstitute.org/api/groups/v1/" + group.name, + status=200, + json="FOO@BAR.COM", + ) + audit_results = managed_groups.ManagedGroupAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(group) + self.assertTrue(record_result.ok()) + + def test_anvil_audit_one_group_managed_by_app_on_anvil_but_app_not_in_group(self): + """anvil_audit raises exception if one group exists in the app but not on AnVIL.""" + group = ManagedGroupFactory.create(is_managed_by_app=True) + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=GetGroupsResponseFactory(n_groups=0).response, + ) + # Add the response. + self.anvil_response_mock.add( + responses.GET, + "https://sam.dsde-prod.broadinstitute.org/api/groups/v1/" + group.name, + status=200, + json="FOO@BAR.COM", + ) + audit_results = managed_groups.ManagedGroupAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(group) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_ROLE])) + + def test_anvil_audit_one_group_admin_in_app_member_on_anvil(self): + """anvil_audit raises exception if one group exists in the app as an admin but the role on AnVIL is member.""" + group = ManagedGroupFactory.create(is_managed_by_app=True) + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=GetGroupsResponseFactory(response=[GroupDetailsMemberFactory(groupName=group.name)]).response, + ) + audit_results = managed_groups.ManagedGroupAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(group) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_ROLE])) + + def test_anvil_audit_one_group_member_in_app_admin_on_anvil(self): + """anvil_audit raises exception if one group exists in the app as an member but the role on AnVIL is admin.""" + group = ManagedGroupFactory.create(is_managed_by_app=False) + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=GetGroupsResponseFactory(response=[GroupDetailsAdminFactory(groupName=group.name)]).response, + ) + audit_results = managed_groups.ManagedGroupAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(group) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_ROLE])) + + def test_anvil_audit_two_groups_no_errors(self): + """anvil_audit works correctly if if two groups exist in both the app and AnVIL.""" + group_1 = ManagedGroupFactory.create() + group_2 = ManagedGroupFactory.create(is_managed_by_app=False) + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=GetGroupsResponseFactory( + response=[ + GroupDetailsAdminFactory(groupName=group_1.name), + GroupDetailsMemberFactory(groupName=group_2.name), + ] + ).response, + ) + api_url_members = self.get_api_url_members(group_1.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group_1.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 2) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(group_1) + self.assertTrue(record_result.ok()) + record_result = audit_results.get_result_for_model_instance(group_2) + self.assertTrue(record_result.ok()) + + def test_anvil_audit_two_groups_json_response_order_does_not_matter(self): + """Order of groups in the json response does not matter.""" + group_1 = ManagedGroupFactory.create() + group_2 = ManagedGroupFactory.create(is_managed_by_app=False) + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=GetGroupsResponseFactory( + response=[ + GroupDetailsMemberFactory(groupName=group_2.name), + GroupDetailsAdminFactory(groupName=group_1.name), + ] + ).response, + ) + api_url_members = self.get_api_url_members(group_1.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group_1.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 2) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(group_1) + self.assertTrue(record_result.ok()) + record_result = audit_results.get_result_for_model_instance(group_2) + self.assertTrue(record_result.ok()) + + def test_anvil_audit_two_groups_first_not_on_anvil(self): + """anvil_audit raises exception if two groups exist in the app but the first is not not on AnVIL.""" + group_1 = ManagedGroupFactory.create() + group_2 = ManagedGroupFactory.create() + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=GetGroupsResponseFactory( + response=[ + GroupDetailsAdminFactory(groupName=group_2.name), + ] + ).response, + ) + # Add response for the group that is not in the app. + self.anvil_response_mock.add( + responses.GET, + "https://sam.dsde-prod.broadinstitute.org/api/groups/v1/" + group_1.name, + status=404, + json=ErrorResponseFactory().response, + ) + # Add responses for the group that is in the app. + api_url_members = self.get_api_url_members(group_2.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group_2.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(group_1) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) + record_result = audit_results.get_result_for_model_instance(group_2) + self.assertTrue(record_result.ok()) + + def test_anvil_audit_two_groups_both_missing(self): + """anvil_audit raises exception if there are two groups that exist in the app but not in AnVIL.""" + group_1 = ManagedGroupFactory.create() + group_2 = ManagedGroupFactory.create() + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=GetGroupsResponseFactory().response, + ) + # Add response for the group that is not in the app. + self.anvil_response_mock.add( + responses.GET, + "https://sam.dsde-prod.broadinstitute.org/api/groups/v1/" + group_1.name, + status=404, + json=ErrorResponseFactory().response, + ) + # Add response for the group that is not in the app. + self.anvil_response_mock.add( + responses.GET, + "https://sam.dsde-prod.broadinstitute.org/api/groups/v1/" + group_2.name, + status=404, + json=ErrorResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 2) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(group_1) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) + record_result = audit_results.get_result_for_model_instance(group_2) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) + + def test_anvil_audit_one_group_member_missing_in_app(self): + """Groups that the app is a member of are not reported in the app.""" + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=GetGroupsResponseFactory(response=[GroupDetailsMemberFactory(groupName="test-group")]).response, + ) + audit_results = managed_groups.ManagedGroupAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + + def test_anvil_audit_one_group_admin_missing_in_app(self): + """anvil_audit works correctly if the service account is an admin of a group not in the app.""" + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=GetGroupsResponseFactory(response=[GroupDetailsAdminFactory(groupName="test-group")]).response, + ) + audit_results = managed_groups.ManagedGroupAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 1) + record_result = audit_results.get_not_in_app_results()[0] + self.assertEqual(record_result.record, "test-group") + + def test_anvil_audit_two_groups_admin_missing_in_app(self): + """anvil_audit works correctly if there are two groups in AnVIL that aren't in the app.""" + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=GetGroupsResponseFactory( + response=[ + GroupDetailsAdminFactory(groupName="test-group-1"), + GroupDetailsAdminFactory(groupName="test-group-2"), + ] + ).response, + ) + audit_results = managed_groups.ManagedGroupAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 2) + record_result = audit_results.get_not_in_app_results()[0] + self.assertEqual(record_result.record, "test-group-1") + record_result = audit_results.get_not_in_app_results()[1] + self.assertEqual(record_result.record, "test-group-2") + + def test_fails_membership_audit(self): + """Error is reported when a group fails the membership audit.""" + group = ManagedGroupFactory.create() + GroupAccountMembershipFactory.create(group=group) + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=GetGroupsResponseFactory(response=[GroupDetailsAdminFactory(groupName=group.name)]).response, + ) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(group) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_GROUP_MEMBERSHIP])) + + def test_admin_in_app_both_member_and_admin_on_anvil(self): + """anvil_audit works correctly when the app is an admin and AnVIL returns both a member and admin record.""" + group = ManagedGroupFactory.create(is_managed_by_app=True) + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=GetGroupsResponseFactory( + response=[ + GroupDetailsAdminFactory(groupName=group.name), + GroupDetailsMemberFactory(groupName=group.name), + ] + ).response, + ) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(group) + self.assertTrue(record_result.ok()) + + def test_admin_in_app_both_member_and_admin_different_order_on_anvil(self): + """anvil_audit works correctly when the app is an admin and AnVIL returns both a member and admin record.""" + group = ManagedGroupFactory.create(is_managed_by_app=True) + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=GetGroupsResponseFactory( + response=[ + GroupDetailsMemberFactory(groupName=group.name), + GroupDetailsAdminFactory(groupName=group.name), + ] + ).response, + ) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(group) + self.assertTrue(record_result.ok()) + + def test_member_in_app_both_member_and_admin_on_anvil(self): + """anvil_audit works correctly when the app is a member and AnVIL returns both a member and admin record.""" + group = ManagedGroupFactory.create(is_managed_by_app=False) + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=GetGroupsResponseFactory( + response=[ + GroupDetailsMemberFactory(groupName=group.name), + GroupDetailsAdminFactory(groupName=group.name), + ] + ).response, + ) + audit_results = managed_groups.ManagedGroupAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(group) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_ROLE])) + + def test_member_in_app_both_member_and_admin_different_order_on_anvil(self): + """anvil_audit works correctly when the app is a member and AnVIL returns both a member and admin record.""" + group = ManagedGroupFactory.create(is_managed_by_app=False) + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=GetGroupsResponseFactory( + response=[ + GroupDetailsAdminFactory(groupName=group.name), + GroupDetailsMemberFactory(groupName=group.name), + ] + ).response, + ) + audit_results = managed_groups.ManagedGroupAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(group) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_ROLE])) + + +class ManagedGroupMembershipAuditTest(AnVILAPIMockTestMixin, TestCase): + """Tests forthe ManagedGroupMembershipAudit class.""" + + def get_api_url_members(self, group_name): + """Return the API url being called by the method.""" + return self.api_client.sam_entry_point + "/api/groups/v1/" + group_name + "/member" + + def get_api_url_admins(self, group_name): + """Return the API url being called by the method.""" + return self.api_client.sam_entry_point + "/api/groups/v1/" + group_name + "/admin" + + def test_group_not_managed_by_app(self): + group = ManagedGroupFactory.create(is_managed_by_app=False) + with self.assertRaises(AnVILNotGroupAdminError): + managed_groups.ManagedGroupMembershipAudit(group) + + def test_no_members(self): + """audit works correctly if this group has no members.""" + group = ManagedGroupFactory.create() + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + + def test_one_account_members(self): + """audit works correctly if this group has one account member.""" + group = ManagedGroupFactory.create() + membership = GroupAccountMembershipFactory.create(group=group) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory(response=[membership.account.email]).response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + model_result = audit_results.get_result_for_model_instance(membership) + self.assertIsInstance(model_result, base.ModelInstanceResult) + self.assertTrue(model_result.ok()) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + + def test_two_account_members(self): + """audit works correctly if this group has two account members.""" + group = ManagedGroupFactory.create() + membership_1 = GroupAccountMembershipFactory.create(group=group) + membership_2 = GroupAccountMembershipFactory.create(group=group) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory( + response=[membership_1.account.email, membership_2.account.email] + ).response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 2) + model_result = audit_results.get_result_for_model_instance(membership_1) + self.assertIsInstance(model_result, base.ModelInstanceResult) + self.assertTrue(model_result.ok()) + model_result = audit_results.get_result_for_model_instance(membership_2) + self.assertIsInstance(model_result, base.ModelInstanceResult) + self.assertTrue(model_result.ok()) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + + def test_one_account_members_not_in_anvil(self): + """audit works correctly if this group has one account member not in anvil.""" + group = ManagedGroupFactory.create() + membership = GroupAccountMembershipFactory.create(group=group) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + model_result = audit_results.get_result_for_model_instance(membership) + self.assertFalse(model_result.ok()) + self.assertEqual(model_result.errors, set([audit_results.ERROR_ACCOUNT_MEMBER_NOT_IN_ANVIL])) + + def test_two_account_members_not_in_anvil(self): + """anvil_audit works correctly if this group has two account member not in anvil.""" + group = ManagedGroupFactory.create() + membership_1 = GroupAccountMembershipFactory.create(group=group) + membership_2 = GroupAccountMembershipFactory.create(group=group) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 2) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + model_result = audit_results.get_result_for_model_instance(membership_1) + self.assertIsInstance(model_result, base.ModelInstanceResult) + self.assertFalse(model_result.ok()) + self.assertEqual(model_result.errors, set([audit_results.ERROR_ACCOUNT_MEMBER_NOT_IN_ANVIL])) + model_result = audit_results.get_result_for_model_instance(membership_2) + self.assertIsInstance(model_result, base.ModelInstanceResult) + self.assertFalse(model_result.ok()) + self.assertEqual(model_result.errors, set([audit_results.ERROR_ACCOUNT_MEMBER_NOT_IN_ANVIL])) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + + def test_one_account_members_not_in_app(self): + """anvil_audit works correctly if this group has one account member not in the app.""" + group = ManagedGroupFactory.create() + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory(response=["test-member@example.com"]).response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 1) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + # Check individual records. + record_result = audit_results.get_not_in_app_results()[0] + self.assertIsInstance(record_result, managed_groups.ManagedGroupMembershipNotInAppResult) + self.assertEqual(record_result.record, "MEMBER: test-member@example.com") + self.assertEqual(record_result.group, group) + self.assertEqual(record_result.email, "test-member@example.com") + self.assertEqual(record_result.role, "MEMBER") + + def test_two_account_members_not_in_app(self): + """anvil_audit works correctly if this group has two account member not in the app.""" + group = ManagedGroupFactory.create() + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory( + response=["test-member-1@example.com", "test-member-2@example.com"] + ).response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 2) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + # Check individual records. + record_result = audit_results.get_not_in_app_results()[0] + self.assertIsInstance(record_result, managed_groups.ManagedGroupMembershipNotInAppResult) + self.assertEqual(record_result.record, "MEMBER: test-member-1@example.com") + self.assertEqual(record_result.group, group) + self.assertEqual(record_result.email, "test-member-1@example.com") + self.assertEqual(record_result.role, "MEMBER") + record_result = audit_results.get_not_in_app_results()[1] + self.assertIsInstance(record_result, managed_groups.ManagedGroupMembershipNotInAppResult) + self.assertEqual(record_result.record, "MEMBER: test-member-2@example.com") + self.assertEqual(record_result.group, group) + self.assertEqual(record_result.email, "test-member-2@example.com") + self.assertEqual(record_result.role, "MEMBER") + + def test_one_account_members_case_insensitive(self): + """anvil_audit works correctly if this group has one account member not in the app.""" + group = ManagedGroupFactory.create() + membership = GroupAccountMembershipFactory.create(group=group, account__email="tEsT-mEmBeR@example.com") + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory(response=["Test-Member@example.com"]).response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership) + self.assertTrue(record_result.ok()) + + def test_one_account_admin(self): + """anvil_audit works correctly if this group has one account admin.""" + group = ManagedGroupFactory.create() + membership = GroupAccountMembershipFactory.create(group=group, role=GroupAccountMembership.ADMIN) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory(response=[membership.account.email]).response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership) + self.assertTrue(record_result.ok()) + + def test_two_account_admin(self): + """anvil_audit works correctly if this group has two account members.""" + group = ManagedGroupFactory.create() + membership_1 = GroupAccountMembershipFactory.create(group=group, role=GroupAccountMembership.ADMIN) + membership_2 = GroupAccountMembershipFactory.create(group=group, role=GroupAccountMembership.ADMIN) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory( + response=[membership_1.account.email, membership_2.account.email] + ).response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 2) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership_1) + self.assertTrue(record_result.ok()) + record_result = audit_results.get_result_for_model_instance(membership_2) + self.assertTrue(record_result.ok()) + + def test_one_account_admin_not_in_anvil(self): + """anvil_audit works correctly if this group has one account member not in anvil.""" + group = ManagedGroupFactory.create() + membership = GroupAccountMembershipFactory.create(group=group, role=GroupAccountMembership.ADMIN) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_ACCOUNT_ADMIN_NOT_IN_ANVIL])) + + def test_two_account_admins_not_in_anvil(self): + """anvil_audit works correctly if this group has two account member not in anvil.""" + group = ManagedGroupFactory.create() + membership_1 = GroupAccountMembershipFactory.create(group=group, role=GroupAccountMembership.ADMIN) + membership_2 = GroupAccountMembershipFactory.create(group=group, role=GroupAccountMembership.ADMIN) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 2) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership_1) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_ACCOUNT_ADMIN_NOT_IN_ANVIL])) + record_result = audit_results.get_result_for_model_instance(membership_2) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_ACCOUNT_ADMIN_NOT_IN_ANVIL])) + + def test_one_account_admin_not_in_app(self): + """anvil_audit works correctly if this group has one account member not in the app.""" + group = ManagedGroupFactory.create() + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory(response=["test-admin@example.com"]).response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 1) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_not_in_app_results()[0] + self.assertIsInstance(record_result, managed_groups.ManagedGroupMembershipNotInAppResult) + self.assertEqual(record_result.record, "ADMIN: test-admin@example.com") + self.assertEqual(record_result.group, group) + self.assertEqual(record_result.email, "test-admin@example.com") + self.assertEqual(record_result.role, "ADMIN") + + def test_two_account_admin_not_in_app(self): + """anvil_audit works correctly if this group has two account admin not in the app.""" + group = ManagedGroupFactory.create() + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory( + response=["test-admin-1@example.com", "test-admin-2@example.com"] + ).response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 2) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_not_in_app_results()[0] + self.assertEqual(record_result.record, "ADMIN: test-admin-1@example.com") + record_result = audit_results.get_not_in_app_results()[1] + self.assertEqual(record_result.record, "ADMIN: test-admin-2@example.com") + + def test_one_account_admin_case_insensitive(self): + """anvil_audit works correctly if this group has one account member not in the app.""" + group = ManagedGroupFactory.create() + membership = GroupAccountMembershipFactory.create( + group=group, + account__email="tEsT-aDmIn@example.com", + role=GroupAccountMembership.ADMIN, + ) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory(response=["Test-Admin@example.com"]).response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership) + self.assertTrue(record_result.ok()) + + def test_account_different_role_member_in_app_admin_in_anvil(self): + """anvil_audit works correctly if an account has a different role in AnVIL.""" + group = ManagedGroupFactory.create() + membership = GroupAccountMembershipFactory.create(group=group, role=GroupAccountMembership.MEMBER) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory(response=[membership.account.email]).response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 1) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_ACCOUNT_MEMBER_NOT_IN_ANVIL])) + record_result = audit_results.get_not_in_app_results()[0] + self.assertEqual(record_result.record, "ADMIN: " + membership.account.email) + + def test_one_group_members(self): + """anvil_audit works correctly if this group has one group member.""" + group = ManagedGroupFactory.create() + membership = GroupGroupMembershipFactory.create(parent_group=group) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory(response=[membership.child_group.email]).response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership) + self.assertTrue(record_result.ok()) + + def test_two_group_members(self): + """anvil_audit works correctly if this group has two account members.""" + group = ManagedGroupFactory.create() + membership_1 = GroupGroupMembershipFactory.create(parent_group=group) + membership_2 = GroupGroupMembershipFactory.create(parent_group=group) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory( + response=[ + membership_1.child_group.email, + membership_2.child_group.email, + ] + ).response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 2) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership_1) + self.assertTrue(record_result.ok()) + record_result = audit_results.get_result_for_model_instance(membership_2) + self.assertTrue(record_result.ok()) + + def test_one_group_members_not_in_anvil(self): + """anvil_audit works correctly if this group has one group member not in anvil.""" + group = ManagedGroupFactory.create() + membership = GroupGroupMembershipFactory.create(parent_group=group) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_GROUP_MEMBER_NOT_IN_ANVIL])) + + def test_two_group_members_not_in_anvil(self): + """anvil_audit works correctly if this group has two group member not in anvil.""" + group = ManagedGroupFactory.create() + membership_1 = GroupGroupMembershipFactory.create(parent_group=group) + membership_2 = GroupGroupMembershipFactory.create(parent_group=group) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 2) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership_1) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_GROUP_MEMBER_NOT_IN_ANVIL])) + record_result = audit_results.get_result_for_model_instance(membership_2) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_GROUP_MEMBER_NOT_IN_ANVIL])) + + def test_one_group_members_not_in_app(self): + """anvil_audit works correctly if this group has one group member not in the app.""" + group = ManagedGroupFactory.create() + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory(response=["test-member@firecloud.org"]).response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 1) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_not_in_app_results()[0] + self.assertIsInstance(record_result, managed_groups.ManagedGroupMembershipNotInAppResult) + self.assertEqual(record_result.record, "MEMBER: test-member@firecloud.org") + self.assertEqual(record_result.group, group) + self.assertEqual(record_result.email, "test-member@firecloud.org") + self.assertEqual(record_result.role, "MEMBER") + + def test_two_group_members_not_in_app(self): + """anvil_audit works correctly if this group has two group member not in the app.""" + group = ManagedGroupFactory.create() + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory( + response=["test-member-1@firecloud.org", "test-member-2@firecloud.org"] + ).response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 2) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_not_in_app_results()[0] + self.assertEqual(record_result.record, "MEMBER: test-member-1@firecloud.org") + record_result = audit_results.get_not_in_app_results()[1] + self.assertEqual(record_result.record, "MEMBER: test-member-2@firecloud.org") + + def test_one_group_members_ignored(self): + """anvil_audit works correctly if this group has one ignored group member.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create() + group = obj.group + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory(response=[obj.ignored_email]).response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 1) + record_result = audit_results.get_ignored_results()[0] + self.assertEqual(record_result.model_instance, obj) + self.assertEqual(record_result.record, "MEMBER: " + obj.ignored_email) + + def test_two_group_members_ignored(self): + """anvil_audit works correctly if this group has two ignored group members.""" + group = ManagedGroupFactory.create() + obj_1 = factories.IgnoredManagedGroupMembershipFactory.create(group=group) + obj_2 = factories.IgnoredManagedGroupMembershipFactory.create(group=group) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory(response=[obj_1.ignored_email, obj_2.ignored_email]).response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 2) + record_results = audit_results.get_ignored_results() + record_result = [record_result for record_result in record_results if record_result.model_instance == obj_1][0] + self.assertEqual(record_result.record, "MEMBER: " + obj_1.ignored_email) + record_result = [record_result for record_result in record_results if record_result.model_instance == obj_2][0] + self.assertEqual(record_result.record, "MEMBER: " + obj_2.ignored_email) + + def test_ignored_still_reports_records_when_email_not_member_of_group(self): + obj = factories.IgnoredManagedGroupMembershipFactory.create() + group = obj.group + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 1) + record_result = audit_results.get_ignored_results()[0] + self.assertEqual(record_result.model_instance, obj) + self.assertIsNone(record_result.record) + + def test_one_group_member_ignored_case_insensitive(self): + obj = factories.IgnoredManagedGroupMembershipFactory.create(ignored_email="foo@bar.com") + group = obj.group + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory(response=["FoO@bAr.CoM"]).response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 1) + record_result = audit_results.get_ignored_results()[0] + self.assertEqual(record_result.model_instance, obj) + self.assertEqual(record_result.record, "MEMBER: foo@bar.com") + + def test_one_group_members_case_insensitive(self): + """anvil_audit works correctly if this group has one group member not in the app.""" + group = ManagedGroupFactory.create() + membership = GroupGroupMembershipFactory.create(parent_group=group, child_group__name="tEsT-mEmBeR") + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory(response=["Test-Member@firecloud.org"]).response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership) + self.assertTrue(record_result.ok()) + + def test_one_group_admin(self): + """anvil_audit works correctly if this group has one group admin.""" + group = ManagedGroupFactory.create() + membership = GroupGroupMembershipFactory.create(parent_group=group, role=GroupGroupMembership.ADMIN) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory(response=[membership.child_group.email]).response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership) + self.assertTrue(record_result.ok()) + + def test_two_group_admin(self): + """anvil_audit works correctly if this group has two group admin.""" + group = ManagedGroupFactory.create() + membership_1 = GroupGroupMembershipFactory.create(parent_group=group, role=GroupGroupMembership.ADMIN) + membership_2 = GroupGroupMembershipFactory.create(parent_group=group, role=GroupGroupMembership.ADMIN) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory( + response=[ + membership_1.child_group.email, + membership_2.child_group.email, + ] + ).response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 2) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership_1) + self.assertTrue(record_result.ok()) + record_result = audit_results.get_result_for_model_instance(membership_2) + self.assertTrue(record_result.ok()) + + def test_one_group_admin_not_in_anvil(self): + """anvil_audit works correctly if this group has one group member not in anvil.""" + group = ManagedGroupFactory.create() + membership = GroupGroupMembershipFactory.create(parent_group=group, role=GroupGroupMembership.ADMIN) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_GROUP_ADMIN_NOT_IN_ANVIL])) + + def test_two_group_admins_not_in_anvil(self): + """anvil_audit works correctly if this group has two group member not in anvil.""" + group = ManagedGroupFactory.create() + membership_1 = GroupGroupMembershipFactory.create(parent_group=group, role=GroupGroupMembership.ADMIN) + membership_2 = GroupGroupMembershipFactory.create(parent_group=group, role=GroupGroupMembership.ADMIN) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 2) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership_1) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_GROUP_ADMIN_NOT_IN_ANVIL])) + record_result = audit_results.get_result_for_model_instance(membership_2) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_GROUP_ADMIN_NOT_IN_ANVIL])) + + def test_one_group_admin_not_in_app(self): + """anvil_audit works correctly if this group has one group member not in the app.""" + group = ManagedGroupFactory.create() + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory(response=["test-admin@firecloud.org"]).response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 1) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_not_in_app_results()[0] + self.assertIsInstance(record_result, managed_groups.ManagedGroupMembershipNotInAppResult) + self.assertEqual(record_result.record, "ADMIN: test-admin@firecloud.org") + self.assertEqual(record_result.group, group) + self.assertEqual(record_result.email, "test-admin@firecloud.org") + self.assertEqual(record_result.role, "ADMIN") + + def test_two_group_admin_not_in_app(self): + """anvil_audit works correctly if this group has two group admin not in the app.""" + group = ManagedGroupFactory.create() + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory( + response=["test-admin-1@firecloud.org", "test-admin-2@firecloud.org"] + ).response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 2) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_not_in_app_results()[0] + self.assertEqual(record_result.record, "ADMIN: test-admin-1@firecloud.org") + record_result = audit_results.get_not_in_app_results()[1] + self.assertEqual(record_result.record, "ADMIN: test-admin-2@firecloud.org") + + def test_one_group_admin_ignored(self): + """anvil_audit works correctly if this group has one ignored group member.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create() + group = obj.group + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipResponseFactory(response=[obj.ignored_email]).response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 1) + record_result = audit_results.get_ignored_results()[0] + self.assertEqual(record_result.model_instance, obj) + + def test_two_group_admins_ignored(self): + group = ManagedGroupFactory.create() + obj_1 = factories.IgnoredManagedGroupMembershipFactory.create(group=group) + obj_2 = factories.IgnoredManagedGroupMembershipFactory.create(group=group) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipResponseFactory(response=[obj_1.ignored_email, obj_2.ignored_email]).response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 2) + record_results = audit_results.get_ignored_results() + self.assertIn(obj_1, [record_result.model_instance for record_result in record_results]) + self.assertIn(obj_2, [record_result.model_instance for record_result in record_results]) + + def test_one_group_admin_ignored_case_insensitive(self): + obj = factories.IgnoredManagedGroupMembershipFactory.create(ignored_email="foo@bar.com") + group = obj.group + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipResponseFactory(response=["FoO@bAr.CoM"]).response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 1) + record_result = audit_results.get_ignored_results()[0] + self.assertEqual(record_result.model_instance, obj) + self.assertEqual(record_result.record, "ADMIN: foo@bar.com") + + def test_one_group_admin_case_insensitive(self): + """anvil_audit works correctly if this group has one group member not in the app.""" + group = ManagedGroupFactory.create() + membership = GroupGroupMembershipFactory.create( + parent_group=group, + child_group__name="tEsT-aDmIn", + role=GroupGroupMembership.ADMIN, + ) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory(response=["Test-Admin@firecloud.org"]).response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership) + self.assertTrue(record_result.ok()) + + def test_group_different_role_member_in_app_admin_in_anvil(self): + """anvil_audit works correctly if an group has a different role in AnVIL.""" + group = ManagedGroupFactory.create() + membership = GroupGroupMembershipFactory.create(parent_group=group, role=GroupGroupMembership.MEMBER) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory(response=[membership.child_group.email]).response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 1) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_GROUP_MEMBER_NOT_IN_ANVIL])) + record_result = audit_results.get_not_in_app_results()[0] + self.assertEqual(record_result.record, "ADMIN: " + membership.child_group.email) + + def test_service_account_is_both_admin_and_member(self): + """No errors are reported when the service account is both a member and an admin of a group.""" + group = ManagedGroupFactory.create() + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory(response=[self.service_account_email]).response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + + def test_different_group_member_email(self): + """anvil_audit works correctly if this group has one group member with a different email.""" + group = ManagedGroupFactory.create() + membership = GroupGroupMembershipFactory.create(parent_group=group, child_group__email="foo@bar.com") + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory(response=[membership.child_group.email]).response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership) + self.assertTrue(record_result.ok()) + + def test_different_group_member_email_case_insensitive(self): + """anvil_audit works correctly if this group has one group member with a different email, case insensitive.""" + group = ManagedGroupFactory.create() + membership = GroupGroupMembershipFactory.create(parent_group=group, child_group__email="foo@bar.com") + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory(response=["Foo@Bar.com"]).response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership) + self.assertTrue(record_result.ok()) + + def test_service_account_is_not_directly_admin(self): + """Audit works when the service account is not directly an admin of a group (but is via a group admin).""" + group = ManagedGroupFactory.create() + membership = GroupGroupMembershipFactory.create( + parent_group=group, + child_group__email="foo@bar.com", + role=GroupGroupMembership.ADMIN, + ) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + # Use the Membership factory because it doesn't add the service account as a direct admin. + json=GetGroupMembershipResponseFactory(response=["foo@bar.com"]).response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership) + self.assertTrue(record_result.ok()) + + def test_group_is_both_admin_and_member(self): + group = ManagedGroupFactory.create() + membership = GroupGroupMembershipFactory.create(parent_group=group, role=GroupGroupMembership.ADMIN) + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory(response=[membership.child_group.email]).response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory(response=[membership.child_group.email]).response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership) + self.assertTrue(record_result.ok()) + + def test_deactivated_account_not_member_in_anvil(self): + """Audit fails if a deactivated account is not in the group on AnVIL.""" + group = ManagedGroupFactory.create() + # Create an inactive account that is a member of this group. + membership = GroupAccountMembershipFactory.create(group=group, account__status=Account.INACTIVE_STATUS) + # The Account is not a member in AnVIL + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + model_result = audit_results.get_result_for_model_instance(membership) + self.assertFalse(model_result.ok()) + self.assertEqual( + model_result.errors, + set([audit_results.ERROR_ACCOUNT_MEMBER_NOT_IN_ANVIL, audit_results.ERROR_DEACTIVATED_ACCOUNT]), + ) + + def test_deactivated_account_member_in_anvil(self): + """Audit is not ok if a deactivated account is in the group on AnVIL.""" + group = ManagedGroupFactory.create() + # Create an inactive account that is a member of this group. + membership = GroupAccountMembershipFactory.create(group=group, account__status=Account.INACTIVE_STATUS) + # The Account is not a member in AnVIL + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory(response=[membership.account.email]).response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership) + self.assertEqual( + record_result.errors, + set([audit_results.ERROR_DEACTIVATED_ACCOUNT]), + ) + + def test_deactivated_account_not_admin_in_anvil(self): + """Audit is not ok if a deactivated account is not in the group on AnVIL.""" + group = ManagedGroupFactory.create() + # Create an inactive account that is a member of this group. + membership = GroupAccountMembershipFactory.create( + group=group, + account__status=Account.INACTIVE_STATUS, + role=GroupAccountMembership.ADMIN, + ) + # The Account is not a member in AnVIL + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + model_result = audit_results.get_result_for_model_instance(membership) + self.assertFalse(model_result.ok()) + self.assertEqual( + model_result.errors, + set([audit_results.ERROR_ACCOUNT_ADMIN_NOT_IN_ANVIL, audit_results.ERROR_DEACTIVATED_ACCOUNT]), + ) + + def test_deactivated_account_admin_in_anvil(self): + """Audit is not ok if a deactivated account is in the group on AnVIL.""" + group = ManagedGroupFactory.create() + # Create an inactive account that is a member of this group. + membership = GroupAccountMembershipFactory.create( + group=group, + account__status=Account.INACTIVE_STATUS, + role=GroupAccountMembership.ADMIN, + ) + # The Account is not a member in AnVIL + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipAdminResponseFactory(response=[membership.account.email]).response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + record_result = audit_results.get_result_for_model_instance(membership) + self.assertEqual( + record_result.errors, + set([audit_results.ERROR_DEACTIVATED_ACCOUNT]), + ) + + def test_ignored_same_email_different_group(self): + """This email is ignored for a different group.""" + group = ManagedGroupFactory.create() + # Create an ignored record for this email, but a different group. + obj = factories.IgnoredManagedGroupMembershipFactory.create() + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipResponseFactory(response=[obj.ignored_email]).response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 1) + self.assertEqual(len(audit_results.get_ignored_results()), 0) + + def test_ignored_different_email_same_group(self): + """A different email is ignored for a this group.""" + # Create an ignored record for this email, but a different group. + obj = factories.IgnoredManagedGroupMembershipFactory.create() + group = obj.group + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=GetGroupMembershipResponseFactory(response=["foo@bar.com"]).response, + ) + audit_results = managed_groups.ManagedGroupMembershipAudit(group) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 1) + self.assertEqual(len(audit_results.get_ignored_results()), 1) + record_result = audit_results.get_not_in_app_results()[0] + self.assertEqual(record_result.record, "ADMIN: foo@bar.com") + record_result = audit_results.get_ignored_results()[0] + self.assertEqual(record_result.model_instance, obj) + self.assertIsNone(record_result.record) diff --git a/anvil_consortium_manager/auditor/tests/test_audit_workspaces.py b/anvil_consortium_manager/auditor/tests/test_audit_workspaces.py new file mode 100644 index 00000000..03998935 --- /dev/null +++ b/anvil_consortium_manager/auditor/tests/test_audit_workspaces.py @@ -0,0 +1,1972 @@ +import responses +from django.test import TestCase +from faker import Faker + +from anvil_consortium_manager.models import WorkspaceGroupSharing +from anvil_consortium_manager.tests.factories import ( + WorkspaceAuthorizationDomainFactory, + WorkspaceFactory, + WorkspaceGroupSharingFactory, +) +from anvil_consortium_manager.tests.utils import AnVILAPIMockTestMixin + +from ..audit import workspaces as workspaces + +fake = Faker() + + +class WorkspaceAuditTest(AnVILAPIMockTestMixin, TestCase): + """Tests for the Workspace.anvil_audit method.""" + + def get_api_url(self): + return self.api_client.rawls_entry_point + "/api/workspaces" + + def get_api_workspace_json( + self, + billing_project_name, + workspace_name, + access, + auth_domains=[], + is_locked=False, + ): + """Return the json dictionary for a single workspace on AnVIL.""" + return { + "accessLevel": access, + "workspace": { + "name": workspace_name, + "namespace": billing_project_name, + "authorizationDomain": [{"membersGroupName": x} for x in auth_domains], + "isLocked": is_locked, + }, + } + + def get_api_workspace_acl_url(self, billing_project_name, workspace_name): + return ( + self.api_client.rawls_entry_point + + "/api/workspaces/" + + billing_project_name + + "/" + + workspace_name + + "/acl" + ) + + def get_api_workspace_acl_response(self): + """Return a json for the workspace/acl method where no one else can access.""" + return { + "acl": { + self.service_account_email: { + "accessLevel": "OWNER", + "canCompute": True, + "canShare": True, + "pending": False, + } + } + } + + def get_api_bucket_options_url(self, billing_project_name, workspace_name): + return self.api_client.rawls_entry_point + "/api/workspaces/" + billing_project_name + "/" + workspace_name + + def get_api_bucket_options_response(self): + """Return a json for the workspace/acl method that is not requester pays.""" + return {"bucketOptions": {"requesterPays": False}} + + def test_anvil_audit_no_workspaces(self): + """anvil_audit works correct if there are no Workspaces in the app.""" + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[], + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + + def test_anvil_audit_one_workspace_no_errors(self): + """anvil_audit works correct if there is one workspace in the app and it exists on AnVIL.""" + workspace = WorkspaceFactory.create() + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[self.get_api_workspace_json(workspace.billing_project.name, workspace.name, "OWNER")], + ) + # Response to check workspace access. + workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(workspace) + self.assertTrue(record_result.ok()) + + def test_anvil_audit_one_workspace_not_on_anvil(self): + """anvil_audit raises exception if one group exists in the app but not on AnVIL.""" + workspace = WorkspaceFactory.create() + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[], + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(workspace) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) + + def test_anvil_audit_one_workspace_owner_in_app_reader_on_anvil(self): + """anvil_audit raises exception if one workspace exists in the app but the access on AnVIL is READER.""" + workspace = WorkspaceFactory.create() + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[self.get_api_workspace_json(workspace.billing_project.name, workspace.name, "READER")], + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(workspace) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_OWNER_ON_ANVIL])) + + def test_anvil_audit_one_workspace_owner_in_app_writer_on_anvil(self): + """anvil_audit raises exception if one workspace exists in the app but the access on AnVIL is WRITER.""" + workspace = WorkspaceFactory.create() + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[self.get_api_workspace_json(workspace.billing_project.name, workspace.name, "WRITER")], + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(workspace) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_OWNER_ON_ANVIL])) + + def test_anvil_audit_one_workspace_is_locked_in_app_not_on_anvil(self): + """anvil_audit raises exception if workspace is locked in the app but not on AnVIL.""" + workspace = WorkspaceFactory.create(is_locked=True) + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[ + self.get_api_workspace_json( + workspace.billing_project.name, + workspace.name, + "OWNER", + is_locked=False, + ) + ], + ) + # Response to check workspace access. + workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(workspace) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_LOCK])) + + def test_anvil_audit_one_workspace_is_not_locked_in_app_but_is_on_anvil(self): + """anvil_audit raises exception if workspace is locked in the app but not on AnVIL.""" + workspace = WorkspaceFactory.create(is_locked=False) + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[ + self.get_api_workspace_json( + workspace.billing_project.name, + workspace.name, + "OWNER", + is_locked=True, + ) + ], + ) + # Response to check workspace access. + workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(workspace) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_LOCK])) + + def test_anvil_audit_one_workspace_is_requester_pays_in_app_not_on_anvil(self): + """anvil_audit raises exception if workspace is requester_pays in the app but not on AnVIL.""" + workspace = WorkspaceFactory.create(is_requester_pays=True) + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[ + self.get_api_workspace_json( + workspace.billing_project.name, + workspace.name, + "OWNER", + is_locked=False, + ) + ], + ) + # Response to check workspace access. + workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(workspace) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_REQUESTER_PAYS])) + + def test_anvil_audit_one_workspace_is_not_requester_pays_in_app_but_is_on_anvil(self): + """anvil_audit raises exception if workspace is requester_pays in the app but not on AnVIL.""" + workspace = WorkspaceFactory.create(is_requester_pays=False) + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[ + self.get_api_workspace_json( + workspace.billing_project.name, + workspace.name, + "OWNER", + is_locked=False, + ) + ], + ) + # Response to check workspace access. + workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) + response = self.get_api_bucket_options_response() + response["bucketOptions"]["requesterPays"] = True + self.anvil_response_mock.add(responses.GET, workspace_acl_url, status=200, json=response) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(workspace) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_REQUESTER_PAYS])) + + def test_anvil_audit_two_workspaces_no_errors(self): + """anvil_audit returns None if if two workspaces exist in both the app and AnVIL.""" + workspace_1 = WorkspaceFactory.create() + workspace_2 = WorkspaceFactory.create() + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[ + self.get_api_workspace_json(workspace_1.billing_project.name, workspace_1.name, "OWNER"), + self.get_api_workspace_json(workspace_2.billing_project.name, workspace_2.name, "OWNER"), + ], + ) + # Response to check workspace access. + workspace_acl_url_1 = self.get_api_workspace_acl_url(workspace_1.billing_project.name, workspace_1.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url_1, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace access. + workspace_acl_url_2 = self.get_api_workspace_acl_url(workspace_2.billing_project.name, workspace_2.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url_2, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace_1.billing_project.name, workspace_1.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace_2.billing_project.name, workspace_2.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 2) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(workspace_1) + self.assertTrue(record_result.ok()) + record_result = audit_results.get_result_for_model_instance(workspace_2) + self.assertTrue(record_result.ok()) + + def test_anvil_audit_two_groups_json_response_order_does_not_matter(self): + """Order of groups in the json response does not matter.""" + workspace_1 = WorkspaceFactory.create() + workspace_2 = WorkspaceFactory.create() + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[ + self.get_api_workspace_json(workspace_2.billing_project.name, workspace_2.name, "OWNER"), + self.get_api_workspace_json(workspace_1.billing_project.name, workspace_1.name, "OWNER"), + ], + ) + # Response to check workspace access. + workspace_acl_url_1 = self.get_api_workspace_acl_url(workspace_1.billing_project.name, workspace_1.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url_1, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace access. + workspace_acl_url_2 = self.get_api_workspace_acl_url(workspace_2.billing_project.name, workspace_2.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url_2, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace_1.billing_project.name, workspace_1.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace_2.billing_project.name, workspace_2.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 2) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(workspace_1) + self.assertTrue(record_result.ok()) + record_result = audit_results.get_result_for_model_instance(workspace_2) + self.assertTrue(record_result.ok()) + + def test_anvil_audit_two_workspaces_first_not_on_anvil(self): + """anvil_audit raises exception if two workspaces exist in the app but the first is not not on AnVIL.""" + workspace_1 = WorkspaceFactory.create() + workspace_2 = WorkspaceFactory.create() + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[ + self.get_api_workspace_json(workspace_2.billing_project.name, workspace_2.name, "OWNER"), + ], + ) + # Response to check workspace access. + workspace_acl_url_2 = self.get_api_workspace_acl_url(workspace_2.billing_project.name, workspace_2.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url_2, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace_2.billing_project.name, workspace_2.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(workspace_1) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) + record_result = audit_results.get_result_for_model_instance(workspace_2) + self.assertTrue(record_result.ok()) + + def test_anvil_audit_two_workspaces_first_different_access(self): + """anvil_audit when if two workspaces exist in the app but access to the first is different on AnVIL.""" + workspace_1 = WorkspaceFactory.create() + workspace_2 = WorkspaceFactory.create() + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[ + self.get_api_workspace_json(workspace_1.billing_project.name, workspace_1.name, "READER"), + self.get_api_workspace_json(workspace_2.billing_project.name, workspace_2.name, "OWNER"), + ], + ) + # Response to check workspace access. + workspace_acl_url_2 = self.get_api_workspace_acl_url(workspace_2.billing_project.name, workspace_2.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url_2, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace_1.billing_project.name, workspace_1.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace_2.billing_project.name, workspace_2.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(workspace_1) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_OWNER_ON_ANVIL])) + record_result = audit_results.get_result_for_model_instance(workspace_2) + self.assertTrue(record_result.ok()) + + def test_anvil_audit_two_workspaces_both_missing_in_anvil(self): + """anvil_audit when there are two workspaces that exist in the app but not in AnVIL.""" + workspace_1 = WorkspaceFactory.create() + workspace_2 = WorkspaceFactory.create() + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[], + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 2) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(workspace_1) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) + record_result = audit_results.get_result_for_model_instance(workspace_2) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) + + def test_anvil_audit_one_workspace_missing_in_app(self): + """anvil_audit returns not_in_app info if a workspace exists on AnVIL but not in the app.""" + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[self.get_api_workspace_json("test-bp", "test-ws", "OWNER")], + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 1) + record_result = audit_results.get_not_in_app_results()[0] + self.assertEqual(record_result.record, "test-bp/test-ws") + + def test_anvil_audit_two_workspaces_missing_in_app(self): + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[ + self.get_api_workspace_json("test-bp-1", "test-ws-1", "OWNER"), + self.get_api_workspace_json("test-bp-2", "test-ws-2", "OWNER"), + ], + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 2) + record_result = audit_results.get_not_in_app_results()[0] + self.assertEqual(record_result.record, "test-bp-1/test-ws-1") + record_result = audit_results.get_not_in_app_results()[1] + self.assertEqual(record_result.record, "test-bp-2/test-ws-2") + + def test_different_billing_project(self): + """A workspace is reported as missing if it has the same name but a different billing project in app.""" + workspace = WorkspaceFactory.create(billing_project__name="test-bp-app", name="test-ws") + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[self.get_api_workspace_json("test-bp-anvil", "test-ws", "OWNER")], + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 1) + record_result = audit_results.get_result_for_model_instance(workspace) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) + record_result = audit_results.get_not_in_app_results()[0] + self.assertEqual(record_result.record, "test-bp-anvil/test-ws") + + def test_ignores_workspaces_where_app_is_reader_on_anvil(self): + """Audit ignores workspaces on AnVIL where app is a READER on AnVIL.""" + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[self.get_api_workspace_json("test-bp", "test-ws", "READER")], + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + + def test_ignores_workspaces_where_app_is_writer_on_anvil(self): + """Audit ignores workspaces on AnVIL where app is a WRITER on AnVIL.""" + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[self.get_api_workspace_json("test-bp", "test-ws", "WRITER")], + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + + def test_one_workspace_one_auth_domain(self): + """anvil_audit works properly when there is one workspace with one auth domain.""" + auth_domain = WorkspaceAuthorizationDomainFactory.create() + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[ + self.get_api_workspace_json( + auth_domain.workspace.billing_project.name, + auth_domain.workspace.name, + "OWNER", + auth_domains=[auth_domain.group.name], + ) + ], + ) + # Response to check workspace access. + workspace_acl_url = self.get_api_workspace_acl_url( + auth_domain.workspace.billing_project.name, auth_domain.workspace.name + ) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url( + auth_domain.workspace.billing_project.name, auth_domain.workspace.name + ) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(auth_domain.workspace) + self.assertTrue(record_result.ok()) + + def test_one_workspace_two_auth_domains(self): + """anvil_audit works properly when there is one workspace with two auth domains.""" + workspace = WorkspaceFactory.create() + auth_domain_1 = WorkspaceAuthorizationDomainFactory.create(workspace=workspace) + auth_domain_2 = WorkspaceAuthorizationDomainFactory.create(workspace=workspace) + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[ + self.get_api_workspace_json( + workspace.billing_project.name, + workspace.name, + "OWNER", + auth_domains=[auth_domain_1.group.name, auth_domain_2.group.name], + ) + ], + ) + # Response to check workspace access. + workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(workspace) + self.assertTrue(record_result.ok()) + + def test_one_workspace_two_auth_domains_order_does_not_matter(self): + """anvil_audit works properly when there is one workspace with two auth domains.""" + workspace = WorkspaceFactory.create() + auth_domain_1 = WorkspaceAuthorizationDomainFactory.create(workspace=workspace, group__name="aa") + auth_domain_2 = WorkspaceAuthorizationDomainFactory.create(workspace=workspace, group__name="zz") + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[ + self.get_api_workspace_json( + workspace.billing_project.name, + workspace.name, + "OWNER", + auth_domains=[auth_domain_2.group.name, auth_domain_1.group.name], + ) + ], + ) + # Response to check workspace access. + workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(workspace) + self.assertTrue(record_result.ok()) + + def test_one_workspace_no_auth_domain_in_app_one_auth_domain_on_anvil(self): + """anvil_audit works properly when there is one workspace with no auth domain in the app but one on AnVIL.""" + workspace = WorkspaceFactory.create() + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[ + self.get_api_workspace_json( + workspace.billing_project.name, + workspace.name, + "OWNER", + auth_domains=["auth-anvil"], + ) + ], + ) + # Response to check workspace access. + workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(workspace) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_AUTH_DOMAINS])) + + def test_one_workspace_one_auth_domain_in_app_no_auth_domain_on_anvil(self): + """anvil_audit works properly when there is one workspace with one auth domain in the app but none on AnVIL.""" + auth_domain = WorkspaceAuthorizationDomainFactory.create() + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[ + self.get_api_workspace_json( + auth_domain.workspace.billing_project.name, + auth_domain.workspace.name, + "OWNER", + auth_domains=[], + ) + ], + ) + # Response to check workspace access. + workspace_acl_url = self.get_api_workspace_acl_url( + auth_domain.workspace.billing_project.name, auth_domain.workspace.name + ) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url( + auth_domain.workspace.billing_project.name, auth_domain.workspace.name + ) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(auth_domain.workspace) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_AUTH_DOMAINS])) + + def test_one_workspace_no_auth_domain_in_app_two_auth_domains_on_anvil(self): + """anvil_audit works properly when there is one workspace with no auth domain in the app but two on AnVIL.""" + workspace = WorkspaceFactory.create() + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[ + self.get_api_workspace_json( + workspace.billing_project.name, + workspace.name, + "OWNER", + auth_domains=["auth-domain"], + ) + ], + ) + # Response to check workspace access. + workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(workspace) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_AUTH_DOMAINS])) + + def test_one_workspace_two_auth_domains_in_app_no_auth_domain_on_anvil(self): + """anvil_audit works properly when there is one workspace with two auth domains in the app but none on AnVIL.""" + workspace = WorkspaceFactory.create() + WorkspaceAuthorizationDomainFactory.create(workspace=workspace) + WorkspaceAuthorizationDomainFactory.create(workspace=workspace) + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[ + self.get_api_workspace_json( + workspace.billing_project.name, + workspace.name, + "OWNER", + auth_domains=[], + ) + ], + ) + # Response to check workspace access. + workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(workspace) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_AUTH_DOMAINS])) + + def test_one_workspace_two_auth_domains_in_app_one_auth_domain_on_anvil(self): + """anvil_audit works properly when there is one workspace with two auth domains in the app but one on AnVIL.""" + workspace = WorkspaceFactory.create() + auth_domain_1 = WorkspaceAuthorizationDomainFactory.create(workspace=workspace) + WorkspaceAuthorizationDomainFactory.create(workspace=workspace) + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[ + self.get_api_workspace_json( + workspace.billing_project.name, + workspace.name, + "OWNER", + auth_domains=[auth_domain_1.group.name], + ) + ], + ) + # Response to check workspace access. + workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(workspace) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_AUTH_DOMAINS])) + + def test_one_workspace_different_auth_domains(self): + """anvil_audit works properly when the app and AnVIL have different auth domains for the same workspace.""" + auth_domain = WorkspaceAuthorizationDomainFactory.create(group__name="app") + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[ + self.get_api_workspace_json( + auth_domain.workspace.billing_project.name, + auth_domain.workspace.name, + "OWNER", + auth_domains=["anvil"], + ) + ], + ) + # Response to check workspace access. + workspace_acl_url = self.get_api_workspace_acl_url( + auth_domain.workspace.billing_project.name, auth_domain.workspace.name + ) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url( + auth_domain.workspace.billing_project.name, auth_domain.workspace.name + ) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(auth_domain.workspace) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_AUTH_DOMAINS])) + + def test_two_workspaces_first_auth_domains_do_not_match(self): + """anvil_audit works properly when there are two workspaces in the app and the first has auth domain issues.""" + workspace_1 = WorkspaceFactory.create() + workspace_2 = WorkspaceFactory.create() + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[ + self.get_api_workspace_json( + workspace_1.billing_project.name, + workspace_1.name, + "OWNER", + auth_domains=["anvil"], + ), + self.get_api_workspace_json( + workspace_2.billing_project.name, + workspace_2.name, + "OWNER", + auth_domains=[], + ), + ], + ) + # Response to check workspace access. + workspace_acl_url_1 = self.get_api_workspace_acl_url(workspace_1.billing_project.name, workspace_1.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url_1, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace access. + workspace_acl_url_2 = self.get_api_workspace_acl_url(workspace_2.billing_project.name, workspace_2.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url_2, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace_1.billing_project.name, workspace_1.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace_2.billing_project.name, workspace_2.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(workspace_1) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_AUTH_DOMAINS])) + record_result = audit_results.get_result_for_model_instance(workspace_2) + self.assertTrue(record_result.ok()) + + def test_two_workspaces_auth_domains_do_not_match_for_both(self): + """anvil_audit works properly when there are two workspaces in the app and both have auth domain issues.""" + workspace_1 = WorkspaceFactory.create() + workspace_2 = WorkspaceFactory.create() + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[ + self.get_api_workspace_json( + workspace_1.billing_project.name, + workspace_1.name, + "OWNER", + auth_domains=["anvil-1"], + ), + self.get_api_workspace_json( + workspace_2.billing_project.name, + workspace_2.name, + "OWNER", + auth_domains=["anvil-2"], + ), + ], + ) + # Response to check workspace access. + workspace_acl_url_1 = self.get_api_workspace_acl_url(workspace_1.billing_project.name, workspace_1.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url_1, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace access. + workspace_acl_url_2 = self.get_api_workspace_acl_url(workspace_2.billing_project.name, workspace_2.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url_2, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace_1.billing_project.name, workspace_1.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace_2.billing_project.name, workspace_2.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 2) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(workspace_1) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_AUTH_DOMAINS])) + record_result = audit_results.get_result_for_model_instance(workspace_2) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_AUTH_DOMAINS])) + + def test_one_workspace_with_two_errors(self): + """One workspace has two errors: different auth domains and not owner.""" + workspace = WorkspaceFactory.create() + WorkspaceAuthorizationDomainFactory.create(workspace=workspace) + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[self.get_api_workspace_json(workspace.billing_project.name, workspace.name, "READER")], + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(workspace) + self.assertFalse(record_result.ok()) + self.assertEqual( + record_result.errors, + set( + [ + audit_results.ERROR_NOT_OWNER_ON_ANVIL, + audit_results.ERROR_DIFFERENT_AUTH_DOMAINS, + ] + ), + ) + + def test_fails_sharing_audit(self): + """anvil_audit works properly when one workspace fails its sharing audit.""" + workspace = WorkspaceFactory.create() + WorkspaceGroupSharingFactory.create(workspace=workspace) + # Response for the main call about workspaces. + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[ + self.get_api_workspace_json( + workspace.billing_project.name, + workspace.name, + "OWNER", + ) + ], + ) + # Response to check workspace access. + workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + audit_results = workspaces.WorkspaceAudit() + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + record_result = audit_results.get_result_for_model_instance(workspace) + self.assertFalse(record_result.ok()) + self.assertEqual(record_result.errors, set([audit_results.ERROR_WORKSPACE_SHARING])) + + +class WorkspaceSharingAuditTest(AnVILAPIMockTestMixin, TestCase): + """Tests for the WorkspaceSharingAudit class.""" + + def setUp(self): + super().setUp() + # Set this variable here because it will include the service account. + # Tests can update it with the update_api_response method. + self.api_response = {"acl": {}} + # Create a workspace for use in tests. + self.workspace = WorkspaceFactory.create() + self.api_url = ( + self.api_client.rawls_entry_point + + "/api/workspaces/" + + self.workspace.billing_project.name + + "/" + + self.workspace.name + + "/acl" + ) + + def update_api_response(self, email, access, can_compute=False, can_share=False): + """Return a paired down json for a single ACL, including the service account.""" + self.api_response["acl"].update( + { + email: { + "accessLevel": access, + "canCompute": can_compute, + "canShare": can_share, + "pending": False, + } + } + ) + + def test_no_access(self): + """anvil_audit works correctly if this workspace is not shared with any groups.""" + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + + def test_one_group_reader(self): + """anvil_audit works correctly if this group has one group member.""" + access = WorkspaceGroupSharingFactory.create(workspace=self.workspace) + self.update_api_response(access.group.email, access.access) + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + model_result = audit_results.get_result_for_model_instance(access) + self.assertTrue(model_result.ok()) + + def test_two_group_readers(self): + """anvil_audit works correctly if this workspace has two group readers.""" + access_1 = WorkspaceGroupSharingFactory.create(workspace=self.workspace) + access_2 = WorkspaceGroupSharingFactory.create(workspace=self.workspace) + self.update_api_response(access_1.group.email, "READER") + self.update_api_response(access_2.group.email, "READER") + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 2) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + model_result = audit_results.get_result_for_model_instance(access_1) + self.assertTrue(model_result.ok()) + model_result = audit_results.get_result_for_model_instance(access_2) + self.assertTrue(model_result.ok()) + + def test_one_group_reader_not_in_anvil(self): + """anvil_audit works correctly if this workspace has one group reader not in anvil.""" + access = WorkspaceGroupSharingFactory.create(workspace=self.workspace) + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + model_result = audit_results.get_result_for_model_instance(access) + self.assertFalse(model_result.ok()) + self.assertEqual(model_result.errors, set([audit_results.ERROR_NOT_SHARED_IN_ANVIL])) + + def test_two_group_readers_not_in_anvil(self): + """anvil_audit works correctly if this workspace has two group readers not in anvil.""" + access_1 = WorkspaceGroupSharingFactory.create(workspace=self.workspace) + access_2 = WorkspaceGroupSharingFactory.create(workspace=self.workspace) + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 2) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + model_result = audit_results.get_result_for_model_instance(access_1) + self.assertFalse(model_result.ok()) + self.assertEqual(model_result.errors, set([audit_results.ERROR_NOT_SHARED_IN_ANVIL])) + model_result = audit_results.get_result_for_model_instance(access_2) + self.assertFalse(model_result.ok()) + self.assertEqual(model_result.errors, set([audit_results.ERROR_NOT_SHARED_IN_ANVIL])) + + def test_one_group_readers_not_in_app(self): + """anvil_audit works correctly if this workspace has one group reader not in the app.""" + self.update_api_response("test-member@firecloud.org", "READER") + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 1) + model_result = audit_results.get_not_in_app_results()[0] + self.assertEqual(model_result.record, "READER: test-member@firecloud.org") + + def test_two_group_readers_not_in_app(self): + """anvil_audit works correctly if this workspace has two group readers not in the app.""" + self.update_api_response("test-member-1@firecloud.org", "READER") + self.update_api_response("test-member-2@firecloud.org", "READER") + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 2) + model_result = audit_results.get_not_in_app_results()[0] + self.assertEqual(model_result.record, "READER: test-member-1@firecloud.org") + model_result = audit_results.get_not_in_app_results()[1] + self.assertEqual(model_result.record, "READER: test-member-2@firecloud.org") + + def test_one_group_members_case_insensitive(self): + """anvil_audit ignores case.""" + access = WorkspaceGroupSharingFactory.create(workspace=self.workspace, group__name="tEsT-mEmBeR") + self.update_api_response("Test-Member@firecloud.org", "READER") + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + model_result = audit_results.get_result_for_model_instance(access) + self.assertTrue(model_result.ok()) + + def test_one_group_writer(self): + """anvil_audit works correctly if this workspace has one group writer.""" + access = WorkspaceGroupSharingFactory.create(workspace=self.workspace, access=WorkspaceGroupSharing.WRITER) + self.update_api_response(access.group.email, "WRITER") + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + model_result = audit_results.get_result_for_model_instance(access) + self.assertTrue(model_result.ok()) + + def test_two_group_writers(self): + """anvil_audit works correctly if this workspace has two group writers.""" + access_1 = WorkspaceGroupSharingFactory.create(workspace=self.workspace, access=WorkspaceGroupSharing.WRITER) + access_2 = WorkspaceGroupSharingFactory.create(workspace=self.workspace, access=WorkspaceGroupSharing.WRITER) + self.update_api_response(access_1.group.email, "WRITER") + self.update_api_response(access_2.group.email, "WRITER") + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 2) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + model_result = audit_results.get_result_for_model_instance(access_1) + self.assertTrue(model_result.ok()) + model_result = audit_results.get_result_for_model_instance(access_2) + self.assertTrue(model_result.ok()) + + def test_one_group_writer_not_in_anvil(self): + """anvil_audit works correctly if this workspace has one group writer not in anvil.""" + access = WorkspaceGroupSharingFactory.create(workspace=self.workspace, access=WorkspaceGroupSharing.WRITER) + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + model_result = audit_results.get_result_for_model_instance(access) + self.assertFalse(model_result.ok()) + self.assertEqual(model_result.errors, set([audit_results.ERROR_NOT_SHARED_IN_ANVIL])) + + def test_two_group_writers_not_in_anvil(self): + """anvil_audit works correctly if this workspace has two group writers not in anvil.""" + access_1 = WorkspaceGroupSharingFactory.create(workspace=self.workspace, access=WorkspaceGroupSharing.WRITER) + access_2 = WorkspaceGroupSharingFactory.create(workspace=self.workspace, access=WorkspaceGroupSharing.WRITER) + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 2) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + model_result = audit_results.get_result_for_model_instance(access_1) + self.assertFalse(model_result.ok()) + self.assertEqual(model_result.errors, set([audit_results.ERROR_NOT_SHARED_IN_ANVIL])) + model_result = audit_results.get_result_for_model_instance(access_2) + self.assertFalse(model_result.ok()) + self.assertEqual(model_result.errors, set([audit_results.ERROR_NOT_SHARED_IN_ANVIL])) + + def test_one_group_writer_not_in_app(self): + """anvil_audit works correctly if this workspace has one group writer not in the app.""" + self.update_api_response("test-writer@firecloud.org", "WRITER") + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 1) + model_result = audit_results.get_not_in_app_results()[0] + self.assertEqual(model_result.record, "WRITER: test-writer@firecloud.org") + + def test_two_group_writers_not_in_app(self): + """anvil_audit works correctly if this workspace has two group writers not in the app.""" + self.update_api_response("test-writer-1@firecloud.org", "WRITER") + self.update_api_response("test-writer-2@firecloud.org", "WRITER") + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 2) + model_result = audit_results.get_not_in_app_results()[0] + self.assertEqual(model_result.record, "WRITER: test-writer-1@firecloud.org") + model_result = audit_results.get_not_in_app_results()[1] + self.assertEqual(model_result.record, "WRITER: test-writer-2@firecloud.org") + + def test_one_group_admin_case_insensitive(self): + """anvil_audit works correctly if this workspace has one group member not in the app.""" + access = WorkspaceGroupSharingFactory.create( + workspace=self.workspace, + group__name="tEsT-wRiTeR", + access=WorkspaceGroupSharing.WRITER, + ) + self.update_api_response("Test-Writer@firecloud.org", "WRITER") + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + model_result = audit_results.get_result_for_model_instance(access) + self.assertTrue(model_result.ok()) + + def test_one_group_owner(self): + """anvil_audit works correctly if this workspace has one group owner.""" + access = WorkspaceGroupSharingFactory.create(workspace=self.workspace, access=WorkspaceGroupSharing.OWNER) + self.update_api_response(access.group.email, "OWNER", can_share=True) + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + model_result = audit_results.get_result_for_model_instance(access) + self.assertTrue(model_result.ok()) + + def test_two_group_owners(self): + """anvil_audit works correctly if this workspace has two group owners.""" + access_1 = WorkspaceGroupSharingFactory.create(workspace=self.workspace, access=WorkspaceGroupSharing.OWNER) + access_2 = WorkspaceGroupSharingFactory.create(workspace=self.workspace, access=WorkspaceGroupSharing.OWNER) + self.update_api_response(access_1.group.email, "OWNER", can_share=True) + self.update_api_response(access_2.group.email, "OWNER", can_share=True) + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 2) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + model_result = audit_results.get_result_for_model_instance(access_1) + self.assertTrue(model_result.ok()) + model_result = audit_results.get_result_for_model_instance(access_2) + self.assertTrue(model_result.ok()) + + def test_one_group_owner_not_in_anvil(self): + """anvil_audit works correctly if this workspace has one group owners not in anvil.""" + access = WorkspaceGroupSharingFactory.create(workspace=self.workspace, access=WorkspaceGroupSharing.OWNER) + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + model_result = audit_results.get_result_for_model_instance(access) + self.assertFalse(model_result.ok()) + self.assertEqual(model_result.errors, set([audit_results.ERROR_NOT_SHARED_IN_ANVIL])) + + def test_two_group_owners_not_in_anvil(self): + """anvil_audit works correctly if this workspace has two group owners not in anvil.""" + access_1 = WorkspaceGroupSharingFactory.create(workspace=self.workspace, access=WorkspaceGroupSharing.OWNER) + access_2 = WorkspaceGroupSharingFactory.create(workspace=self.workspace, access=WorkspaceGroupSharing.OWNER) + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 2) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + model_result = audit_results.get_result_for_model_instance(access_1) + self.assertFalse(model_result.ok()) + self.assertEqual(model_result.errors, set([audit_results.ERROR_NOT_SHARED_IN_ANVIL])) + model_result = audit_results.get_result_for_model_instance(access_2) + self.assertFalse(model_result.ok()) + self.assertEqual(model_result.errors, set([audit_results.ERROR_NOT_SHARED_IN_ANVIL])) + + def test_one_group_owner_not_in_app(self): + """anvil_audit works correctly if this workspace has one group owner not in the app.""" + self.update_api_response("test-writer@firecloud.org", "OWNER") + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 1) + model_result = audit_results.get_not_in_app_results()[0] + self.assertEqual(model_result.record, "OWNER: test-writer@firecloud.org") + + def test_two_group_owners_not_in_app(self): + """anvil_audit works correctly if this workspace has two group owners not in the app.""" + self.update_api_response("test-writer-1@firecloud.org", "OWNER") + self.update_api_response("test-writer-2@firecloud.org", "OWNER") + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 2) + model_result = audit_results.get_not_in_app_results()[0] + self.assertEqual(model_result.record, "OWNER: test-writer-1@firecloud.org") + model_result = audit_results.get_not_in_app_results()[1] + self.assertEqual(model_result.record, "OWNER: test-writer-2@firecloud.org") + + def test_one_group_owner_case_insensitive(self): + """anvil_audit works correctly with different cases for owner emails.""" + access = WorkspaceGroupSharingFactory.create( + workspace=self.workspace, + group__name="tEsT-oWnEr", + access=WorkspaceGroupSharing.OWNER, + ) + self.update_api_response("Test-Owner@firecloud.org", "OWNER", can_share=True) + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + model_result = audit_results.get_result_for_model_instance(access) + self.assertTrue(model_result.ok()) + + def test_group_different_access_reader_in_app_writer_in_anvil(self): + """anvil_audit works correctly if a group has different access to a workspace in AnVIL.""" + access = WorkspaceGroupSharingFactory.create(workspace=self.workspace, access=WorkspaceGroupSharing.READER) + self.update_api_response(access.group.email, "WRITER") + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + model_result = audit_results.get_result_for_model_instance(access) + self.assertFalse(model_result.ok()) + self.assertEqual(model_result.errors, set([audit_results.ERROR_DIFFERENT_ACCESS])) + + def test_group_different_access_reader_in_app_owner_in_anvil(self): + """anvil_audit works correctly if a group has different access to a workspace in AnVIL.""" + access = WorkspaceGroupSharingFactory.create(workspace=self.workspace, access=WorkspaceGroupSharing.READER) + self.update_api_response(access.group.email, "OWNER", can_compute=True, can_share=True) + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + model_result = audit_results.get_result_for_model_instance(access) + self.assertFalse(model_result.ok()) + self.assertEqual( + model_result.errors, + set( + [ + audit_results.ERROR_DIFFERENT_ACCESS, + audit_results.ERROR_DIFFERENT_CAN_COMPUTE, + audit_results.ERROR_DIFFERENT_CAN_SHARE, + ] + ), + ) + + def test_group_different_can_compute(self): + """anvil_audit works correctly if can_compute is different between the app and AnVIL.""" + access = WorkspaceGroupSharingFactory.create( + workspace=self.workspace, + access=WorkspaceGroupSharing.WRITER, + can_compute=True, + ) + self.update_api_response(access.group.email, "WRITER", can_compute=False) + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + model_result = audit_results.get_result_for_model_instance(access) + self.assertFalse(model_result.ok()) + self.assertEqual(model_result.errors, set([audit_results.ERROR_DIFFERENT_CAN_COMPUTE])) + + def test_group_different_can_share(self): + """anvil_audit works correctly if can_share is True in AnVIL.""" + access = WorkspaceGroupSharingFactory.create(workspace=self.workspace, access=WorkspaceGroupSharing.WRITER) + self.update_api_response(access.group.email, "WRITER", can_share=True) + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + model_result = audit_results.get_result_for_model_instance(access) + self.assertFalse(model_result.ok()) + self.assertEqual(model_result.errors, set([audit_results.ERROR_DIFFERENT_CAN_SHARE])) + + def test_removes_service_account(self): + """Removes the service account from acl if it exists.""" + self.update_api_response(self.service_account_email, "OWNER", can_compute=True, can_share=True) + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + + def test_group_owner_can_share_true(self): + """Owners must have can_share=True.""" + access = WorkspaceGroupSharingFactory.create( + workspace=self.workspace, + access=WorkspaceGroupSharing.OWNER, + can_compute=True, + ) + self.update_api_response(access.group.email, "OWNER", can_compute=True, can_share=True) + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertTrue(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 1) + self.assertEqual(len(audit_results.get_error_results()), 0) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + + def test_group_writer_can_share_false(self): + """Writers must have can_share=False.""" + access = WorkspaceGroupSharingFactory.create( + workspace=self.workspace, + access=WorkspaceGroupSharing.WRITER, + can_compute=True, + ) + self.update_api_response(access.group.email, "WRITER", can_compute=True, can_share=True) + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + model_result = audit_results.get_result_for_model_instance(access) + self.assertFalse(model_result.ok()) + self.assertEqual(model_result.errors, set([audit_results.ERROR_DIFFERENT_CAN_SHARE])) + + def test_group_reader_can_share_false(self): + """Readers must have can_share=False.""" + access = WorkspaceGroupSharingFactory.create( + workspace=self.workspace, + access=WorkspaceGroupSharing.READER, + can_compute=False, + ) + self.update_api_response(access.group.email, "READER", can_compute=False, can_share=True) + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + audit_results = workspaces.WorkspaceSharingAudit(self.workspace) + audit_results.run_audit() + self.assertFalse(audit_results.ok()) + self.assertEqual(len(audit_results.get_verified_results()), 0) + self.assertEqual(len(audit_results.get_error_results()), 1) + self.assertEqual(len(audit_results.get_not_in_app_results()), 0) + model_result = audit_results.get_result_for_model_instance(access) + self.assertFalse(model_result.ok()) + self.assertEqual(model_result.errors, set([audit_results.ERROR_DIFFERENT_CAN_SHARE])) diff --git a/anvil_consortium_manager/auditor/tests/test_commands.py b/anvil_consortium_manager/auditor/tests/test_commands.py new file mode 100644 index 00000000..681a9319 --- /dev/null +++ b/anvil_consortium_manager/auditor/tests/test_commands.py @@ -0,0 +1,440 @@ +"""Tests for management commands in `anvil_consortium_manager.auditor`.""" + +import pprint +from io import StringIO +from unittest import skip + +import responses +from django.contrib.sites.models import Site +from django.core import mail +from django.core.management import CommandError, call_command +from django.test import TestCase + +from anvil_consortium_manager.tests.api_factories import ( + GetGroupMembershipAdminResponseFactory, + GetGroupMembershipResponseFactory, + GetGroupsResponseFactory, + GroupDetailsAdminFactory, +) +from anvil_consortium_manager.tests.factories import AccountFactory, BillingProjectFactory +from anvil_consortium_manager.tests.utils import AnVILAPIMockTestMixin + +from ..audit import base, billing_projects, managed_groups +from ..management.commands.run_anvil_audit import ErrorTableWithLink +from . import factories + + +class RunAnvilAuditTest(AnVILAPIMockTestMixin, TestCase): + """Tests for the run_anvil_audit command""" + + def get_api_url_billing_project(self, billing_project_name): + return self.api_client.rawls_entry_point + "/api/billing/v2/" + billing_project_name + + def test_command_output_invalid_model(self): + """Appropriate error is returned when an invalid model is specified.""" + out = StringIO() + with self.assertRaises(CommandError) as e: + # Call with the "--models=foo" so it goes through the argparse validation. + # Calling with models=["foo"] does not throw an exception. + call_command("run_anvil_audit", "--models=foo", stdout=out) + self.assertIn("invalid choice", str(e.exception)) + + def test_command_output_no_model_specified(self): + """Runs on all models if no models are specified.""" + # Add API call responses. + self.anvil_response_mock.add( + responses.GET, + self.api_client.sam_entry_point + "/api/groups/v1", + status=200, + json=[], + ) + self.anvil_response_mock.add( + responses.GET, + self.api_client.rawls_entry_point + "/api/workspaces", + status=200, + json=[], + ) + out = StringIO() + call_command("run_anvil_audit", "--no-color", stdout=out) + self.assertIn("BillingProjectAudit... ok!", out.getvalue()) + self.assertIn("AccountAudit... ok!", out.getvalue()) + self.assertIn("ManagedGroupAudit... ok!", out.getvalue()) + self.assertIn("WorkspaceAudit... ok!", out.getvalue()) + + def test_command_output_multiple_models(self): + """Can audit multiple models at the same time.""" + out = StringIO() + call_command( + "run_anvil_audit", + "--no-color", + models=["BillingProject", "Account"], + stdout=out, + ) + self.assertIn("BillingProjectAudit... ok!", out.getvalue()) + self.assertIn("AccountAudit... ok!", out.getvalue()) + + def test_command_output_billing_project_no_instances(self): + """Test command output.""" + out = StringIO() + call_command("run_anvil_audit", "--no-color", models=["BillingProject"], stdout=out) + self.assertIn("BillingProjectAudit... ok!", out.getvalue()) + + def test_command_output_account_no_instances(self): + """Test command output.""" + out = StringIO() + call_command("run_anvil_audit", "--no-color", models=["Account"], stdout=out) + self.assertIn("AccountAudit... ok!", out.getvalue()) + + def test_command_output_managed_group_no_instances(self): + """Test command output.""" + self.anvil_response_mock.add( + responses.GET, + self.api_client.sam_entry_point + "/api/groups/v1", + status=200, + json=[], + ) + out = StringIO() + call_command("run_anvil_audit", "--no-color", models=["ManagedGroup"], stdout=out) + self.assertIn("ManagedGroupAudit... ok!", out.getvalue()) + + def test_command_output_workspace_no_instances(self): + """Test command output.""" + self.anvil_response_mock.add( + responses.GET, + self.api_client.rawls_entry_point + "/api/workspaces", + status=200, + json=[], + ) + out = StringIO() + call_command("run_anvil_audit", "--no-color", models=["Workspace"], stdout=out) + self.assertIn("WorkspaceAudit... ok!", out.getvalue()) + + def test_command_output_ignored_one_record(self): + """Test command output.""" + factories.IgnoredManagedGroupMembershipFactory.create(group__name="test-group") + self.anvil_response_mock.add( + responses.GET, + self.api_client.sam_entry_point + "/api/groups/v1", + status=200, + json=GetGroupsResponseFactory(response=[GroupDetailsAdminFactory(groupName="test-group")]).response, + ) + self.anvil_response_mock.add( + responses.GET, + self.api_client.sam_entry_point + "/api/groups/v1/test-group/member", + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + self.anvil_response_mock.add( + responses.GET, + self.api_client.sam_entry_point + "/api/groups/v1/test-group/admin", + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + out = StringIO() + call_command("run_anvil_audit", "--no-color", models=["ManagedGroup"], stdout=out) + self.assertIn("ManagedGroupAudit... ok! (ignoring 1 records)", out.getvalue()) + + def test_command_output_ignored_two_records(self): + """Test command output.""" + factories.IgnoredManagedGroupMembershipFactory.create_batch(2, group__name="test-group") + self.anvil_response_mock.add( + responses.GET, + self.api_client.sam_entry_point + "/api/groups/v1", + status=200, + json=GetGroupsResponseFactory(response=[GroupDetailsAdminFactory(groupName="test-group")]).response, + ) + self.anvil_response_mock.add( + responses.GET, + self.api_client.sam_entry_point + "/api/groups/v1/test-group/member", + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + self.anvil_response_mock.add( + responses.GET, + self.api_client.sam_entry_point + "/api/groups/v1/test-group/admin", + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + out = StringIO() + call_command("run_anvil_audit", "--no-color", models=["ManagedGroup"], stdout=out) + self.assertIn("ManagedGroupAudit... ok! (ignoring 2 records)", out.getvalue()) + + def test_command_run_audit_one_instance_ok(self): + """Test command output.""" + billing_project = BillingProjectFactory.create() + # Add a response. + api_url = self.get_api_url_billing_project(billing_project.name) + self.anvil_response_mock.add(responses.GET, api_url, status=200) + out = StringIO() + call_command("run_anvil_audit", "--no-color", models=["BillingProject"], stdout=out) + self.assertIn("BillingProjectAudit... ok!", out.getvalue()) + self.assertNotIn("errors", out.getvalue()) + self.assertNotIn("not_in_app", out.getvalue()) + + def test_command_run_audit_ok_email(self): + """Test command output.""" + billing_project = BillingProjectFactory.create() + # Add a response. + api_url = self.get_api_url_billing_project(billing_project.name) + self.anvil_response_mock.add(responses.GET, api_url, status=200) + out = StringIO() + call_command( + "run_anvil_audit", + "--no-color", + models=["BillingProject"], + email="test@example.com", + stdout=out, + ) + self.assertIn("BillingProjectAudit... ok!", out.getvalue()) + # One message has been sent by default. + self.assertEqual(len(mail.outbox), 1) + email = mail.outbox[0] + self.assertEqual(email.to, ["test@example.com"]) + self.assertIn("ok", email.subject) + # Text body. + audit_results = billing_projects.BillingProjectAudit() + audit_results.run_audit() + self.assertEqual(pprint.pformat(audit_results.export()), email.body) + # HTML body. + self.assertEqual(len(email.alternatives), 1) + # Check that the number of "ok" instances is correct in email body. + self.assertIn("1 instance(s) verified", email.alternatives[0][0]) + # Check ignored instances. + self.assertIn("Ignoring 0 record(s)", email.alternatives[0][0]) + + def test_command_run_audit_ok_email_errors_only(self): + """Test command output when email and errors_only is set.""" + billing_project = BillingProjectFactory.create() + # Add a response. + api_url = self.get_api_url_billing_project(billing_project.name) + self.anvil_response_mock.add(responses.GET, api_url, status=200) + out = StringIO() + call_command( + "run_anvil_audit", + "--no-color", + models=["BillingProject"], + email="test@example.com", + errors_only=True, + stdout=out, + ) + self.assertIn("BillingProjectAudit... ok!", out.getvalue()) + # No message has been sent by default. + self.assertEqual(len(mail.outbox), 0) + + def test_command_run_audit_ok_ignored_records_email(self): + """Test command output.""" + factories.IgnoredManagedGroupMembershipFactory.create(group__name="test-group") + self.anvil_response_mock.add( + responses.GET, + self.api_client.sam_entry_point + "/api/groups/v1", + status=200, + json=GetGroupsResponseFactory(response=[GroupDetailsAdminFactory(groupName="test-group")]).response, + ) + self.anvil_response_mock.add( + responses.GET, + self.api_client.sam_entry_point + "/api/groups/v1/test-group/member", + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + self.anvil_response_mock.add( + responses.GET, + self.api_client.sam_entry_point + "/api/groups/v1/test-group/admin", + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + out = StringIO() + call_command( + "run_anvil_audit", + "--no-color", + models=["ManagedGroup"], + email="test@example.com", + stdout=out, + ) + # One message has been sent by default. + self.assertEqual(len(mail.outbox), 1) + email = mail.outbox[0] + self.assertEqual(email.to, ["test@example.com"]) + self.assertIn("ok", email.subject) + # Text body. + audit_results = managed_groups.ManagedGroupAudit() + audit_results.run_audit() + self.assertEqual(pprint.pformat(audit_results.export()), email.body) + # HTML body. + self.assertEqual(len(email.alternatives), 1) + # Check that the number of "ok" instances is correct in email body. + self.assertIn("1 instance(s) verified", email.alternatives[0][0]) + # Check ignored instances. + self.assertIn("Ignoring 1 record(s)", email.alternatives[0][0]) + + def test_command_run_audit_ok_ignored_records_email_errors_only(self): + """Test command output when email and errors_only is set, and there are ignored records.""" + factories.IgnoredManagedGroupMembershipFactory.create(group__name="test-group") + self.anvil_response_mock.add( + responses.GET, + self.api_client.sam_entry_point + "/api/groups/v1", + status=200, + json=GetGroupsResponseFactory(response=[GroupDetailsAdminFactory(groupName="test-group")]).response, + ) + self.anvil_response_mock.add( + responses.GET, + self.api_client.sam_entry_point + "/api/groups/v1/test-group/member", + status=200, + json=GetGroupMembershipResponseFactory().response, + ) + self.anvil_response_mock.add( + responses.GET, + self.api_client.sam_entry_point + "/api/groups/v1/test-group/admin", + status=200, + json=GetGroupMembershipAdminResponseFactory().response, + ) + out = StringIO() + call_command( + "run_anvil_audit", + "--no-color", + models=["ManagedGroup"], + email="test@example.com", + errors_only=True, + stdout=out, + ) + self.assertIn("ManagedGroupAudit... ok!", out.getvalue()) + # No message has been sent by default. + self.assertEqual(len(mail.outbox), 0) + + def test_command_run_audit_not_ok(self): + """Test command output when BillingProject audit is not ok.""" + billing_project = BillingProjectFactory.create() + # Add a response. + api_url = self.get_api_url_billing_project(billing_project.name) + self.anvil_response_mock.add(responses.GET, api_url, status=404, json={"message": "error"}) + out = StringIO() + call_command("run_anvil_audit", "--no-color", models=["BillingProject"], stdout=out) + self.assertIn("BillingProjectAudit... problems found.", out.getvalue()) + self.assertIn("""'errors':""", out.getvalue()) + self.assertIn(billing_projects.BillingProjectAudit.ERROR_NOT_IN_ANVIL, out.getvalue()) + + def test_command_run_audit_not_ok_email(self): + """Test command output when BillingProject audit is not ok with email specified.""" + billing_project = BillingProjectFactory.create() + # Add a response. + api_url = self.get_api_url_billing_project(billing_project.name) + self.anvil_response_mock.add(responses.GET, api_url, status=404, json={"message": "error"}) + out = StringIO() + call_command( + "run_anvil_audit", + "--no-color", + models=["BillingProject"], + email="test@example.com", + stdout=out, + ) + self.assertIn("BillingProjectAudit... problems found.", out.getvalue()) + # Not printed to stdout. + self.assertIn("""'errors':""", out.getvalue()) + # One message has been sent. + self.assertEqual(len(mail.outbox), 1) + email = mail.outbox[0] + self.assertEqual(email.to, ["test@example.com"]) + self.assertIn("errors", email.subject) + # Text body. + audit_results = billing_projects.BillingProjectAudit() + audit_results.run_audit() + # HTML body. + self.assertEqual(len(email.alternatives), 1) + + def test_command_run_audit_not_ok_email_has_html_link(self): + """Test command output when BillingProject audit is not ok with email specified.""" + billing_project = BillingProjectFactory.create() + # Add a response. + api_url = self.get_api_url_billing_project(billing_project.name) + self.anvil_response_mock.add(responses.GET, api_url, status=404, json={"message": "error"}) + out = StringIO() + call_command( + "run_anvil_audit", + "--no-color", + models=["BillingProject"], + email="test@example.com", + stdout=out, + ) + email = mail.outbox[0] + self.assertEqual(len(email.alternatives), 1) + html_fragment = """{obj}""".format( + obj=str(billing_project), url=billing_project.get_absolute_url() + ) + self.assertInHTML(html_fragment, email.alternatives[0][0]) + + def test_command_run_audit_not_ok_email_has_html_link_different_domain(self): + """Test command output when BillingProject audit is not ok with email specified.""" + site = Site.objects.create(domain="foobar.com", name="test") + site.save() + with self.settings(SITE_ID=site.id): + billing_project = BillingProjectFactory.create() + # Add a response. + api_url = self.get_api_url_billing_project(billing_project.name) + self.anvil_response_mock.add(responses.GET, api_url, status=404, json={"message": "error"}) + out = StringIO() + call_command( + "run_anvil_audit", + "--no-color", + models=["BillingProject"], + email="test@example.com", + stdout=out, + ) + email = mail.outbox[0] + self.assertEqual(len(email.alternatives), 1) + html_fragment = """{obj}""".format( + obj=str(billing_project), url=billing_project.get_absolute_url() + ) + self.assertInHTML(html_fragment, email.alternatives[0][0]) + + def test_command_run_audit_api_error(self): + """Test command output when BillingProject audit is not ok.""" + billing_project = BillingProjectFactory.create() + # Add a response. + api_url = self.get_api_url_billing_project(billing_project.name) + self.anvil_response_mock.add(responses.GET, api_url, status=500, json={"message": "error"}) + out = StringIO() + with self.assertRaises(CommandError): + call_command("run_anvil_audit", "--no-color", models=["BillingProject"], stdout=out) + + # This test is complicated so skipping for now. + # When trying to change the settings, the test attempts to repopulate the + # workspace registry. This then causes an error. Skip it until we figure out + # how best to handle this situation. + @skip + def test_command_without_sites(self): + """Test command behavior without the Sites framework enabled.""" + out = StringIO() + with self.modify_settings(INSTALLED_APPS={"remove": ["django.contrib.sites"]}): + # with self.assertRaises(ImproperlyConfigured): + call_command( + "run_anvil_audit", + "--no-color", + models=["BillingProject"], + email="test@example.com", + stdout=out, + ) + + +class RunAnVILAuditTablesTest(TestCase): + def setUp(self): + super().setUp() + + class GenericAuditResults(base.AnVILAudit): + TEST_ERROR_1 = "Test error 1" + TEST_ERROR_2 = "Test error 2" + + self.audit_results = GenericAuditResults() + # It doesn't matter what model we use at this point, so just pick Account. + self.model_factory = AccountFactory + + def test_errors_table(self): + """Errors table is correct.""" + obj_verified = self.model_factory.create() + self.audit_results.add_result(base.ModelInstanceResult(obj_verified)) + obj_error = self.model_factory.create() + error_result = base.ModelInstanceResult(obj_error) + error_result.add_error(self.audit_results.TEST_ERROR_1) + error_result.add_error(self.audit_results.TEST_ERROR_2) + self.audit_results.add_result(error_result) + self.audit_results.add_result(base.NotInAppResult("foo")) + table = ErrorTableWithLink(self.audit_results.get_error_results()) + self.assertEqual(table.rows[0].get_cell("errors"), "Test error 1, Test error 2") diff --git a/anvil_consortium_manager/auditor/tests/test_forms.py b/anvil_consortium_manager/auditor/tests/test_forms.py new file mode 100644 index 00000000..25f67aab --- /dev/null +++ b/anvil_consortium_manager/auditor/tests/test_forms.py @@ -0,0 +1,84 @@ +"""Test forms for the anvil_consortium_manager.auditor app.""" + +from django.core.exceptions import NON_FIELD_ERRORS +from django.test import TestCase + +from .. import forms +from . import factories + + +class IgnoredManagedGroupMembershipForm(TestCase): + """Tests for the IgnoredManagedGroupMembershipForm class.""" + + form_class = forms.IgnoredManagedGroupMembershipForm + + def setUp(self): + """Create a group and account for use in tests.""" + self.group = factories.ManagedGroupFactory.create() + + def test_valid(self): + """Form is valid with necessary input.""" + form_data = { + "group": self.group, + "ignored_email": "test_email@example.com", + "note": "test note", + } + form = self.form_class(data=form_data) + self.assertTrue(form.is_valid()) + + def test_invalid_missing_group(self): + """Form is invalid when missing email.""" + form_data = { + # "group": self.group, + "ignored_email": "test_email@example.com", + "note": "test note", + } + form = self.form_class(data=form_data) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertIn("group", form.errors) + self.assertEqual(len(form.errors["group"]), 1) + self.assertIn("required", form.errors["group"][0]) + + def test_invalid_missing_email(self): + """Form is invalid when missing email.""" + form_data = { + "group": self.group, + # "ignored_email": "test_email@example.com", + "note": "test note", + } + form = self.form_class(data=form_data) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertIn("ignored_email", form.errors) + self.assertEqual(len(form.errors["ignored_email"]), 1) + self.assertIn("required", form.errors["ignored_email"][0]) + + def test_invalid_missing_note(self): + """Form is invalid when missing email.""" + form_data = { + "group": self.group, + "ignored_email": "test_email@example.com", + # "note": "test note", + } + form = self.form_class(data=form_data) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertIn("note", form.errors) + self.assertEqual(len(form.errors["note"]), 1) + self.assertIn("required", form.errors["note"][0]) + + def test_invalid_duplicate(self): + """Form is invalid with a duplicated instance.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create(group=self.group) + form_data = { + "group": obj.group, + "ignored_email": obj.ignored_email, + "note": "foo", + } + form = self.form_class(data=form_data) + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertIn(NON_FIELD_ERRORS, form.errors) + self.assertEqual(len(form.errors[NON_FIELD_ERRORS]), 1) + self.assertIn("already exists", form.errors[NON_FIELD_ERRORS][0]) diff --git a/anvil_consortium_manager/auditor/tests/test_models.py b/anvil_consortium_manager/auditor/tests/test_models.py new file mode 100644 index 00000000..8317fe4d --- /dev/null +++ b/anvil_consortium_manager/auditor/tests/test_models.py @@ -0,0 +1,86 @@ +from django.db.utils import IntegrityError + +from anvil_consortium_manager.tests.utils import TestCase # Redefined to work with Django < 4.2 and Django=4.2. + +from .. import models +from . import factories + + +class IgnoredManagedGroupMembershipTest(TestCase): + """Tests for the models.IgnoredManagedGroupMembership model.""" + + def test_model_saving(self): + """Creation using the model constructor and .save() works.""" + group = factories.ManagedGroupFactory.create() + user = factories.UserFactory.create() + instance = models.IgnoredManagedGroupMembership( + group=group, + ignored_email="email@example.com", + added_by=user, + note="foo", + ) + instance.save() + self.assertIsInstance(instance, models.IgnoredManagedGroupMembership) + + def test_str_method(self): + """The custom __str__ method returns the correct string.""" + instance = factories.IgnoredManagedGroupMembershipFactory.create( + group__name="foo", ignored_email="email@example.com" + ) + instance.save() + self.assertIsInstance(instance.__str__(), str) + self.assertEqual(instance.__str__(), "foo membership: ignoring email@example.com") + + def test_get_absolute_url(self): + """The get_absolute_url() method works.""" + instance = factories.IgnoredManagedGroupMembershipFactory() + self.assertIsInstance(instance.get_absolute_url(), str) + + def test_history(self): + """A simple history record is created when model is updated.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create() + # History was created. + self.assertEqual(obj.history.count(), 1) + # A new entry is created on update. + obj.note = "foo bar" + obj.save() + self.assertEqual(obj.history.count(), 2) + # An entry is created upon deletion. + obj.delete() + self.assertEqual(models.IgnoredManagedGroupMembership.history.count(), 3) + + def test_unique(self): + # Cannot save the same record for the same group and email. + group = factories.ManagedGroupFactory.create() + email = "email@example.com" + instance_1 = factories.IgnoredManagedGroupMembershipFactory.build( + group=group, + ignored_email=email, + added_by=factories.UserFactory.create(), + ) + instance_1.save() + instance_2 = factories.IgnoredManagedGroupMembershipFactory.build( + group=group, + ignored_email=email, + added_by=factories.UserFactory.create(), + ) + with self.assertRaises(IntegrityError): + instance_2.save() + + def test_unique_case_insensitive(self): + # Cannot save the same record for the same group and email. + group = factories.ManagedGroupFactory.create() + email = "email@example.com" + instance_1 = factories.IgnoredManagedGroupMembershipFactory.build( + group=group, + ignored_email=email, + added_by=factories.UserFactory.create(), + ) + instance_1.save() + instance_2 = factories.IgnoredManagedGroupMembershipFactory.build( + group=group, + ignored_email=email.upper(), + added_by=factories.UserFactory.create(), + ) + with self.assertRaises(IntegrityError): + instance_2.save() diff --git a/anvil_consortium_manager/auditor/tests/test_tables.py b/anvil_consortium_manager/auditor/tests/test_tables.py new file mode 100644 index 00000000..f95e43e4 --- /dev/null +++ b/anvil_consortium_manager/auditor/tests/test_tables.py @@ -0,0 +1,24 @@ +from django.test import TestCase + +from .. import models, tables +from . import factories + + +class IgnoredManagedGroupMembershipTableTest(TestCase): + model = models.IgnoredManagedGroupMembership + model_factory = factories.IgnoredManagedGroupMembershipFactory + table_class = tables.IgnoredManagedGroupMembershipTable + + def test_row_count_with_no_objects(self): + table = self.table_class(self.model.objects.all()) + self.assertEqual(len(table.rows), 0) + + def test_row_count_with_one_object(self): + self.model_factory.create() + table = self.table_class(self.model.objects.all()) + self.assertEqual(len(table.rows), 1) + + def test_row_count_with_two_objects(self): + self.model_factory.create_batch(2) + table = self.table_class(self.model.objects.all()) + self.assertEqual(len(table.rows), 2) diff --git a/anvil_consortium_manager/auditor/tests/test_viewmixins.py b/anvil_consortium_manager/auditor/tests/test_viewmixins.py new file mode 100644 index 00000000..34959eed --- /dev/null +++ b/anvil_consortium_manager/auditor/tests/test_viewmixins.py @@ -0,0 +1,12 @@ +from django.core.exceptions import ImproperlyConfigured +from django.test import TestCase + +from .. import viewmixins + + +class AnVILAuditMixinTest(TestCase): + """ManagedGroupGraphMixin tests that aren't covered elsewhere.""" + + def test_run_audit_not_implemented(self): + with self.assertRaises(ImproperlyConfigured): + viewmixins.AnVILAuditMixin().run_audit() diff --git a/anvil_consortium_manager/auditor/tests/test_views.py b/anvil_consortium_manager/auditor/tests/test_views.py new file mode 100644 index 00000000..c331b3e6 --- /dev/null +++ b/anvil_consortium_manager/auditor/tests/test_views.py @@ -0,0 +1,2428 @@ +import responses +from django.conf import settings +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Permission, User +from django.contrib.messages import get_messages +from django.core.exceptions import PermissionDenied +from django.forms import HiddenInput +from django.http.response import Http404 +from django.shortcuts import resolve_url +from django.test import RequestFactory +from django.urls import reverse +from faker import Faker + +from anvil_consortium_manager import anvil_api +from anvil_consortium_manager.models import ( + Account, + AnVILProjectManagerAccess, +) +from anvil_consortium_manager.tests.factories import ( + AccountFactory, + BillingProjectFactory, + GroupAccountMembershipFactory, + ManagedGroupFactory, + WorkspaceFactory, + WorkspaceGroupSharingFactory, +) +from anvil_consortium_manager.tests.utils import AnVILAPIMockTestMixin, TestCase + +from .. import forms, models, tables, views +from ..audit import base as base_audit +from ..audit import managed_groups as managed_group_audit +from . import factories + +fake = Faker() + + +class BillingProjectAuditTest(AnVILAPIMockTestMixin, TestCase): + """Tests for the BillingProjectAudit view.""" + + def setUp(self): + """Set up test class.""" + super().setUp() + self.factory = RequestFactory() + # Create a user with only view permission. + self.user = User.objects.create_user(username="test", password="test") + self.user.user_permissions.add( + Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) + ) + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("anvil_consortium_manager:auditor:billing_projects:all", args=args) + + def get_api_url(self, billing_project_name): + return self.api_client.rawls_entry_point + "/api/billing/v2/" + billing_project_name + + def get_api_json_response(self): + return { + "roles": ["User"], + } + + def get_view(self): + """Return the view being tested.""" + return views.BillingProjectAudit.as_view() + + def test_view_redirect_not_logged_in(self): + "View redirects to login view when user is not logged in." + # Need a client for redirects. + response = self.client.get(self.get_url()) + self.assertRedirects(response, resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url()) + + def test_status_code_with_user_permission(self): + """Returns successful response code.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, 200) + + def test_access_without_user_permission(self): + """Raises permission denied if user has no permissions.""" + user_no_perms = User.objects.create_user(username="test-none", password="test-none") + request = self.factory.get(self.get_url()) + request.user = user_no_perms + with self.assertRaises(PermissionDenied): + self.get_view()(request) + + def test_access_with_limited_view_permission(self): + """Raises permission denied if user has limited view permission.""" + user = User.objects.create_user(username="test-limited", password="test-limited") + user.user_permissions.add(Permission.objects.get(codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME)) + request = self.factory.get(self.get_url()) + request.user = user + with self.assertRaises(PermissionDenied): + self.get_view()(request) + + def test_template(self): + """Template loads successfully.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, 200) + + def test_audit_verified(self): + """audit_verified is in the context data.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("verified_table", response.context_data) + self.assertIsInstance(response.context_data["verified_table"], base_audit.VerifiedTable) + self.assertEqual(len(response.context_data["verified_table"].rows), 0) + + def test_audit_verified_one_record(self): + """audit_verified with one verified record.""" + billing_project = BillingProjectFactory.create() + api_url = self.get_api_url(billing_project.name) + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=self.get_api_json_response(), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("verified_table", response.context_data) + self.assertEqual(len(response.context_data["verified_table"].rows), 1) + + def test_audit_errors(self): + """audit_errors is in the context data.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("error_table", response.context_data) + self.assertIsInstance(response.context_data["error_table"], base_audit.ErrorTable) + self.assertEqual(len(response.context_data["error_table"].rows), 0) + + def test_audit_errors_one_record(self): + """audit_errors with one verified record.""" + billing_project = BillingProjectFactory.create() + api_url = self.get_api_url(billing_project.name) + self.anvil_response_mock.add( + responses.GET, + api_url, + status=404, + json={"message": "other error"}, + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("error_table", response.context_data) + self.assertEqual(len(response.context_data["error_table"].rows), 1) + + def test_audit_not_in_app(self): + """audit_errors is in the context data.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("not_in_app_table", response.context_data) + self.assertIsInstance(response.context_data["not_in_app_table"], base_audit.NotInAppTable) + self.assertEqual(len(response.context_data["not_in_app_table"].rows), 0) + + def test_audit_ok_is_ok(self): + """audit_ok when audit_results.ok() is True.""" + billing_project = BillingProjectFactory.create() + api_url = self.get_api_url(billing_project.name) + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("audit_ok", response.context_data) + self.assertEqual(response.context_data["audit_ok"], True) + + def test_audit_ok_is_not_ok(self): + """audit_ok when audit_results.ok() is True.""" + billing_project = BillingProjectFactory.create() + api_url = self.get_api_url(billing_project.name) + self.anvil_response_mock.add( + responses.GET, + api_url, + status=404, + json={"message": "other error"}, + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("audit_ok", response.context_data) + self.assertEqual(response.context_data["audit_ok"], False) + + +class AccountAuditTest(AnVILAPIMockTestMixin, TestCase): + """Tests for the AccountAudit view.""" + + def setUp(self): + """Set up test class.""" + super().setUp() + self.factory = RequestFactory() + # Create a user with only view permission. + self.user = User.objects.create_user(username="test", password="test") + self.user.user_permissions.add( + Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) + ) + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("anvil_consortium_manager:auditor:accounts:all", args=args) + + def get_api_url(self, email): + return self.api_client.sam_entry_point + "/api/users/v1/" + email + + def get_api_json_response(self, email): + id = fake.bothify(text="#" * 21) + return { + "googleSubjectId": id, + "userEmail": email, + "userSubjectId": id, + } + + def get_view(self): + """Return the view being tested.""" + return views.AccountAudit.as_view() + + def test_view_redirect_not_logged_in(self): + "View redirects to login view when user is not logged in." + # Need a client for redirects. + response = self.client.get(self.get_url()) + self.assertRedirects(response, resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url()) + + def test_status_code_with_user_permission(self): + """Returns successful response code.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, 200) + + def test_access_with_limited_view_permission(self): + """Raises permission denied if user has limited view permission.""" + user = User.objects.create_user(username="test-limited", password="test-limited") + user.user_permissions.add(Permission.objects.get(codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME)) + request = self.factory.get(self.get_url()) + request.user = user + with self.assertRaises(PermissionDenied): + self.get_view()(request) + + def test_access_without_user_permission(self): + """Raises permission denied if user has no permissions.""" + user_no_perms = User.objects.create_user(username="test-none", password="test-none") + request = self.factory.get(self.get_url()) + request.user = user_no_perms + with self.assertRaises(PermissionDenied): + self.get_view()(request) + + def test_template(self): + """Template loads successfully.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, 200) + + def test_audit_verified(self): + """audit_verified is in the context data.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("verified_table", response.context_data) + self.assertIsInstance(response.context_data["verified_table"], base_audit.VerifiedTable) + self.assertEqual(len(response.context_data["verified_table"].rows), 0) + + def test_audit_verified_one_verified(self): + """audit_verified with one verified record.""" + account = AccountFactory.create() + api_url = self.get_api_url(account.email) + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=self.get_api_json_response(account.email), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("verified_table", response.context_data) + self.assertEqual(len(response.context_data["verified_table"].rows), 1) + + def test_audit_errors(self): + """audit_errors is in the context data.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("error_table", response.context_data) + self.assertIsInstance(response.context_data["error_table"], base_audit.ErrorTable) + self.assertEqual(len(response.context_data["error_table"].rows), 0) + + def test_audit_errors_one_verified(self): + """audit_errors with one verified record.""" + account = AccountFactory.create() + api_url = self.get_api_url(account.email) + self.anvil_response_mock.add( + responses.GET, + api_url, + status=404, + json={"message": "other error"}, + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("error_table", response.context_data) + self.assertEqual(len(response.context_data["error_table"].rows), 1) + + def test_audit_not_in_app(self): + """audit_errors is in the context data.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("not_in_app_table", response.context_data) + self.assertEqual(len(response.context_data["error_table"].rows), 0) + + def test_audit_ok_is_ok(self): + """audit_ok when audit_results.ok() is True.""" + account = AccountFactory.create() + api_url = self.get_api_url(account.email) + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=self.get_api_json_response(account.email), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("audit_ok", response.context_data) + self.assertEqual(response.context_data["audit_ok"], True) + + def test_audit_ok_is_not_ok(self): + """audit_ok when audit_results.ok() is True.""" + account = AccountFactory.create() + api_url = self.get_api_url(account.email) + self.anvil_response_mock.add( + responses.GET, + api_url, + status=404, + json={"message": "other error"}, + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("audit_ok", response.context_data) + self.assertEqual(response.context_data["audit_ok"], False) + + +class ManagedGroupAuditTest(AnVILAPIMockTestMixin, TestCase): + """Tests for the ManagedGroupAudit view.""" + + def setUp(self): + """Set up test class.""" + super().setUp() + self.factory = RequestFactory() + # Create a user with only view permission. + self.user = User.objects.create_user(username="test", password="test") + self.user.user_permissions.add( + Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) + ) + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("anvil_consortium_manager:auditor:managed_groups:all", args=args) + + def get_api_groups_url(self): + """Return the API url being called by the method.""" + return self.api_client.sam_entry_point + "/api/groups/v1" + + def get_api_group_json(self, group_name, role): + """Return json data about groups in the API format.""" + json_data = { + "groupEmail": group_name + "@firecloud.org", + "groupName": group_name, + "role": role, + } + return json_data + + def get_api_url_members(self, group_name): + """Return the API url being called by the method.""" + return self.api_client.sam_entry_point + "/api/groups/v1/" + group_name + "/member" + + def get_api_url_admins(self, group_name): + """Return the API url being called by the method.""" + return self.api_client.sam_entry_point + "/api/groups/v1/" + group_name + "/admin" + + def get_api_json_response_admins(self, emails=[]): + """Return json data about groups in the API format.""" + return [anvil_api.AnVILAPIClient().auth_session.credentials.service_account_email] + emails + + def get_api_json_response_members(self, emails=[]): + """Return json data about groups in the API format.""" + return emails + + def get_view(self): + """Return the view being tested.""" + return views.ManagedGroupAudit.as_view() + + def test_view_redirect_not_logged_in(self): + "View redirects to login view when user is not logged in." + # Need a client for redirects. + response = self.client.get(self.get_url()) + self.assertRedirects(response, resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url()) + + def test_status_code_with_user_permission(self): + """Returns successful response code.""" + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[], + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, 200) + + def test_access_with_limited_view_permission(self): + """Raises permission denied if user has limited view permission.""" + user = User.objects.create_user(username="test-limited", password="test-limited") + user.user_permissions.add(Permission.objects.get(codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME)) + request = self.factory.get(self.get_url()) + request.user = user + with self.assertRaises(PermissionDenied): + self.get_view()(request) + + def test_access_without_user_permission(self): + """Raises permission denied if user has no permissions.""" + user_no_perms = User.objects.create_user(username="test-none", password="test-none") + request = self.factory.get(self.get_url()) + request.user = user_no_perms + with self.assertRaises(PermissionDenied): + self.get_view()(request) + + def test_template(self): + """Template loads successfully.""" + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[], + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, 200) + + def test_audit_verified(self): + """audit_verified is in the context data.""" + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[], + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("verified_table", response.context_data) + self.assertIsInstance(response.context_data["verified_table"], base_audit.VerifiedTable) + self.assertEqual(len(response.context_data["verified_table"].rows), 0) + + def test_audit_verified_one_record(self): + """audit_verified with one verified record.""" + group = ManagedGroupFactory.create() + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[self.get_api_group_json(group.name, "Admin")], + ) + # Group membership API call. + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=self.get_api_json_response_members(emails=[]), + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=self.get_api_json_response_admins(emails=[]), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("verified_table", response.context_data) + self.assertEqual(len(response.context_data["verified_table"].rows), 1) + + def test_audit_errors(self): + """audit_errors is in the context data.""" + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[], + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("error_table", response.context_data) + self.assertIsInstance(response.context_data["error_table"], base_audit.ErrorTable) + self.assertEqual(len(response.context_data["error_table"].rows), 0) + + def test_audit_errors_one_record(self): + """audit_errors with one error record.""" + group = ManagedGroupFactory.create() + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + # Error - we are not the admin + json=[self.get_api_group_json(group.name, "Member")], + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("error_table", response.context_data) + self.assertEqual(len(response.context_data["error_table"].rows), 1) + + def test_audit_not_in_app(self): + """audit_not_in_app is in the context data.""" + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[], + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("not_in_app_table", response.context_data) + self.assertIsInstance(response.context_data["not_in_app_table"], base_audit.NotInAppTable) + self.assertEqual(len(response.context_data["not_in_app_table"].rows), 0) + + def test_audit_not_in_app_one_record(self): + """audit_not_in_app with one record not in app.""" + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[self.get_api_group_json("foo", "Admin")], + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("not_in_app_table", response.context_data) + self.assertEqual(len(response.context_data["not_in_app_table"].rows), 1) + + def test_audit_ok_is_ok(self): + """audit_ok when audit_results.ok() is True.""" + group = ManagedGroupFactory.create() + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[self.get_api_group_json(group.name, "Admin")], + ) + # Group membership API call. + api_url_members = self.get_api_url_members(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=self.get_api_json_response_members(emails=[]), + ) + api_url_admins = self.get_api_url_admins(group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=self.get_api_json_response_admins(emails=[]), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("audit_ok", response.context_data) + self.assertEqual(response.context_data["audit_ok"], True) + + def test_audit_ok_is_not_ok(self): + """audit_ok when audit_results.ok() is False.""" + group = ManagedGroupFactory.create() + api_url = self.get_api_groups_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + # Error - we are not admin. + json=[self.get_api_group_json(group.name, "Member")], + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("audit_ok", response.context_data) + self.assertEqual(response.context_data["audit_ok"], False) + + +class ManagedGroupMembershipAuditTest(AnVILAPIMockTestMixin, TestCase): + """Tests for the ManagedGroupAudit view.""" + + def setUp(self): + """Set up test class.""" + super().setUp() + self.factory = RequestFactory() + # Create a user with both view and edit permission. + self.user = User.objects.create_user(username="test", password="test") + self.user.user_permissions.add( + Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) + ) + self.group = ManagedGroupFactory.create() + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("anvil_consortium_manager:auditor:managed_groups:membership:by_group:all", args=args) + + def get_api_url_members(self, group_name): + """Return the API url being called by the method.""" + return self.api_client.sam_entry_point + "/api/groups/v1/" + group_name + "/member" + + def get_api_url_admins(self, group_name): + """Return the API url being called by the method.""" + return self.api_client.sam_entry_point + "/api/groups/v1/" + group_name + "/admin" + + def get_api_json_response_admins(self, emails=[]): + """Return json data about groups in the API format.""" + return [anvil_api.AnVILAPIClient().auth_session.credentials.service_account_email] + emails + + def get_api_json_response_members(self, emails=[]): + """Return json data about groups in the API format.""" + return emails + + def get_view(self): + """Return the view being tested.""" + return views.ManagedGroupMembershipAudit.as_view() + + def test_view_redirect_not_logged_in(self): + "View redirects to login view when user is not logged in." + # Need a client for redirects. + response = self.client.get(self.get_url("foo")) + self.assertRedirects(response, resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url("foo")) + + def test_status_code_with_user_permission(self): + """Returns successful response code.""" + api_url_members = self.get_api_url_members(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=self.get_api_json_response_members(emails=[]), + ) + api_url_admins = self.get_api_url_admins(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=self.get_api_json_response_admins(emails=[]), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.group.name)) + self.assertEqual(response.status_code, 200) + + def test_access_with_limited_view_permission(self): + """Raises permission denied if user has limited view permission.""" + user = User.objects.create_user(username="test-limited", password="test-limited") + user.user_permissions.add(Permission.objects.get(codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME)) + request = self.factory.get(self.get_url("foo")) + request.user = user + with self.assertRaises(PermissionDenied): + self.get_view()(request) + + def test_access_without_user_permission(self): + """Raises permission denied if user has no permissions.""" + user_no_perms = User.objects.create_user(username="test-none", password="test-none") + request = self.factory.get(self.get_url("foo")) + request.user = user_no_perms + with self.assertRaises(PermissionDenied): + self.get_view()(request, slug="foo") + + def test_template(self): + """Template loads successfully.""" + api_url_members = self.get_api_url_members(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=self.get_api_json_response_members(emails=[]), + ) + api_url_admins = self.get_api_url_admins(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=self.get_api_json_response_admins(emails=[]), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.group.name)) + self.assertEqual(response.status_code, 200) + + def test_table_classes(self): + api_url_members = self.get_api_url_members(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=self.get_api_json_response_members(emails=[]), + ) + api_url_admins = self.get_api_url_admins(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=self.get_api_json_response_admins(emails=[]), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.group.name)) + self.assertIn("verified_table", response.context_data) + self.assertIsInstance(response.context_data["verified_table"], base_audit.VerifiedTable) + self.assertIn("error_table", response.context_data) + self.assertIsInstance(response.context_data["error_table"], base_audit.ErrorTable) + self.assertIn("not_in_app_table", response.context_data) + self.assertIsInstance(response.context_data["not_in_app_table"], base_audit.NotInAppTable) + self.assertIn("ignored_table", response.context_data) + self.assertIsInstance( + response.context_data["ignored_table"], managed_group_audit.ManagedGroupMembershipIgnoredTable + ) + + def test_audit_verified(self): + """audit_verified is in the context data.""" + api_url_members = self.get_api_url_members(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=self.get_api_json_response_members(emails=[]), + ) + api_url_admins = self.get_api_url_admins(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=self.get_api_json_response_admins(emails=[]), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.group.name)) + self.assertIn("verified_table", response.context_data) + self.assertIsInstance(response.context_data["verified_table"], base_audit.VerifiedTable) + self.assertEqual(len(response.context_data["verified_table"].rows), 0) + + def test_audit_verified_one_record(self): + """audit_verified with one verified record.""" + membership = GroupAccountMembershipFactory.create(group=self.group) + api_url_members = self.get_api_url_members(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=self.get_api_json_response_members(emails=[membership.account.email]), + ) + api_url_admins = self.get_api_url_admins(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=self.get_api_json_response_admins(emails=[]), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.group.name)) + self.assertIn("verified_table", response.context_data) + self.assertEqual(len(response.context_data["verified_table"].rows), 1) + + def test_audit_errors(self): + """audit_errors is in the context data.""" + api_url_members = self.get_api_url_members(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=self.get_api_json_response_members(emails=[]), + ) + api_url_admins = self.get_api_url_admins(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=self.get_api_json_response_admins(emails=[]), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.group.name)) + self.assertIn("error_table", response.context_data) + self.assertIsInstance(response.context_data["error_table"], base_audit.ErrorTable) + self.assertEqual(len(response.context_data["error_table"].rows), 0) + + def test_audit_errors_one_record(self): + """audit_errors with one error record.""" + membership = GroupAccountMembershipFactory.create(group=self.group) + # Group membership API call. + api_url_members = self.get_api_url_members(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=self.get_api_json_response_members(emails=[]), + ) + api_url_admins = self.get_api_url_admins(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=self.get_api_json_response_admins(emails=[membership.account.email]), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.group.name)) + self.assertIn("error_table", response.context_data) + self.assertEqual(len(response.context_data["error_table"].rows), 1) + + def test_audit_not_in_app(self): + """audit_not_in_app is in the context data.""" + api_url_members = self.get_api_url_members(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=self.get_api_json_response_members(emails=[]), + ) + api_url_admins = self.get_api_url_admins(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=self.get_api_json_response_admins(emails=[]), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.group.name)) + self.assertIn("not_in_app_table", response.context_data) + self.assertIsInstance(response.context_data["not_in_app_table"], base_audit.NotInAppTable) + self.assertEqual(len(response.context_data["not_in_app_table"].rows), 0) + + def test_audit_not_in_app_one_record(self): + """audit_not_in_app with one record not in app.""" + api_url_members = self.get_api_url_members(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=self.get_api_json_response_members(emails=["foo@bar.com"]), + ) + api_url_admins = self.get_api_url_admins(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=self.get_api_json_response_admins(emails=[]), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.group.name)) + self.assertIn("not_in_app_table", response.context_data) + self.assertEqual(len(response.context_data["not_in_app_table"].rows), 1) + + def test_audit_not_in_app_link_to_ignore(self): + """Link to ignore create view appears when a not_in_app result is found.""" + api_url_members = self.get_api_url_members(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=self.get_api_json_response_members(emails=["foo@bar.com"]), + ) + api_url_admins = self.get_api_url_admins(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=self.get_api_json_response_admins(emails=[]), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.group.name)) + expected_url = reverse( + "anvil_consortium_manager:auditor:managed_groups:membership:by_group:ignored:new", + args=[self.group.name, "foo@bar.com"], + ) + self.assertIn(expected_url, response.content.decode("utf-8")) + + def test_audit_ignored(self): + """ignored_table is in the context data.""" + api_url_members = self.get_api_url_members(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=self.get_api_json_response_members(emails=[]), + ) + api_url_admins = self.get_api_url_admins(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=self.get_api_json_response_admins(emails=[]), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.group.name)) + self.assertIn("ignored_table", response.context_data) + self.assertIsInstance(response.context_data["ignored_table"], base_audit.IgnoredTable) + self.assertEqual(len(response.context_data["ignored_table"].rows), 0) + + def test_audit_one_ignored_record(self): + """ignored_table with one ignored record.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create(group=self.group) + api_url_members = self.get_api_url_members(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=self.get_api_json_response_members(emails=[obj.ignored_email]), + ) + api_url_admins = self.get_api_url_admins(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=self.get_api_json_response_admins(emails=[]), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.group.name)) + self.assertIn("ignored_table", response.context_data) + self.assertIsInstance(response.context_data["ignored_table"], base_audit.IgnoredTable) + self.assertEqual(len(response.context_data["ignored_table"].rows), 1) + + def test_audit_one_ignored_record_not_in_anvil(self): + """The ignored record is not a group member in AnVIL.""" + factories.IgnoredManagedGroupMembershipFactory.create(group=self.group) + api_url_members = self.get_api_url_members(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=self.get_api_json_response_members(emails=[]), + ) + api_url_admins = self.get_api_url_admins(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=self.get_api_json_response_admins(emails=[]), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.group.name)) + self.assertIn("ignored_table", response.context_data) + self.assertIsInstance(response.context_data["ignored_table"], base_audit.IgnoredTable) + self.assertEqual(len(response.context_data["ignored_table"].rows), 1) + + def test_audit_ok_is_ok(self): + """audit_ok when audit_results.ok() is True.""" + api_url_members = self.get_api_url_members(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=self.get_api_json_response_members(emails=[]), + ) + api_url_admins = self.get_api_url_admins(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=self.get_api_json_response_admins(emails=[]), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.group.name)) + self.assertIn("audit_ok", response.context_data) + self.assertEqual(response.context_data["audit_ok"], True) + + def test_audit_ok_is_not_ok(self): + """audit_ok when audit_results.ok() is False.""" + api_url_members = self.get_api_url_members(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_members, + status=200, + json=self.get_api_json_response_members(emails=["foo@bar.com"]), + ) + api_url_admins = self.get_api_url_admins(self.group.name) + self.anvil_response_mock.add( + responses.GET, + api_url_admins, + status=200, + json=self.get_api_json_response_admins(emails=[]), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.group.name)) + self.assertIn("audit_ok", response.context_data) + self.assertEqual(response.context_data["audit_ok"], False) + + def test_group_not_managed_by_app(self): + """Redirects with a message when group is not managed by app.""" + group = ManagedGroupFactory.create(is_managed_by_app=False) + # Only clients load the template. + self.client.force_login(self.user) + response = self.client.get(self.get_url(group.name), follow=True) + self.assertRedirects(response, group.get_absolute_url()) + messages = [m.message for m in get_messages(response.wsgi_request)] + self.assertEqual(len(messages), 1) + self.assertEqual( + str(messages[0]), + views.ManagedGroupMembershipAudit.message_not_managed_by_app, + ) + + def test_group_does_not_exist_in_app(self): + """Raises a 404 error with an invalid object pk.""" + ManagedGroupFactory.create() + request = self.factory.get(self.get_url("foo")) + request.user = self.user + with self.assertRaises(Http404): + self.get_view()(request, slug="foo") + + +class IgnoredManagedGroupMembershipDetailTest(TestCase): + """Tests for the IgnoredManagedGroupMembershipDetail view.""" + + def setUp(self): + """Set up test class.""" + self.factory = RequestFactory() + # Create a user with both view and edit permission. + self.user = User.objects.create_user(username="test", password="test") + self.user.user_permissions.add( + Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) + ) + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("anvil_consortium_manager:auditor:managed_groups:membership:by_group:ignored:detail", args=args) + + def get_view(self): + """Return the view being tested.""" + return views.IgnoredManagedGroupMembershipDetail.as_view() + + def test_view_redirect_not_logged_in(self): + "View redirects to login view when user is not logged in." + # Need a client for redirects. + response = self.client.get(self.get_url("foo", "bar@example.com")) + self.assertRedirects( + response, + resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url("foo", "bar@example.com"), + ) + + def test_status_code_with_user_permission(self): + """Returns successful response code.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create() + self.client.force_login(self.user) + response = self.client.get(obj.get_absolute_url()) + self.assertEqual(response.status_code, 200) + + def test_access_with_limited_view_permission(self): + """Raises permission denied if user has limited view permission.""" + user = User.objects.create_user(username="test-limited", password="test-limited") + user.user_permissions.add(Permission.objects.get(codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME)) + request = self.factory.get(self.get_url("foo", "bar@example.com")) + request.user = user + with self.assertRaises(PermissionDenied): + self.get_view()(request) + + def test_access_without_user_permission(self): + """Raises permission denied if user has no permissions.""" + user_no_perms = User.objects.create_user(username="test-none", password="test-none") + request = self.factory.get(self.get_url("foo1", "bar@example.com")) + request.user = user_no_perms + with self.assertRaises(PermissionDenied): + self.get_view()(request) + + def test_invalid_obj(self): + """Raises a 404 error with an invalid object pk.""" + request = self.factory.get(self.get_url("foo1", "bar@example.com")) + request.user = self.user + with self.assertRaises(Http404): + self.get_view()(request) + + def test_invalid_obj_different_group(self): + """Raises a 404 error with an invalid object pk.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create() + request = self.factory.get(self.get_url("foo", obj.ignored_email)) + request.user = self.user + with self.assertRaises(Http404): + self.get_view()(request) + + def test_invalid_obj_different_email(self): + """Raises a 404 error with an invalid object pk.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create() + email = fake.email() + request = self.factory.get(self.get_url(obj.group.name, email)) + request.user = self.user + with self.assertRaises(Http404): + self.get_view()(request) + + def test_detail_page_links_staff_view(self): + """Links to other object detail pages appear correctly when user has staff view permission.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create() + self.client.force_login(self.user) + response = self.client.get(obj.get_absolute_url()) + html = """{text}""".format(url=obj.group.get_absolute_url(), text=str(obj.group)) + self.assertContains(response, html) + # "Added by" link is tested in a separate test, since not all projects will have an absolute url for the user. + # Action buttons. + expected_url = reverse( + "anvil_consortium_manager:auditor:managed_groups:membership:by_group:ignored:delete", + args=[obj.group.name, obj.ignored_email], + ) + self.assertNotContains(response, expected_url) + expected_url = reverse( + "anvil_consortium_manager:auditor:managed_groups:membership:by_group:ignored:update", + args=[obj.group.name, obj.ignored_email], + ) + self.assertNotContains(response, expected_url) + + def test_detail_page_links_staff_edit(self): + """Links to other object detail pages appear correctly when user has staff edit permission.""" + user = User.objects.create_user(username="staff-edit", password="testpassword") + user.user_permissions.add( + Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) + ) + user.user_permissions.add( + Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME) + ) + obj = factories.IgnoredManagedGroupMembershipFactory.create() + self.client.force_login(user) + response = self.client.get(obj.get_absolute_url()) + html = """{text}""".format(url=obj.group.get_absolute_url(), text=str(obj.group)) + self.assertContains(response, html) + # "Added by" link is tested in a separate test, since not all projects will have an absolute url for the user. + # Action buttons. + expected_url = reverse( + "anvil_consortium_manager:auditor:managed_groups:membership:by_group:ignored:delete", + args=[obj.group.name, obj.ignored_email], + ) + self.assertContains(response, expected_url) + expected_url = reverse( + "anvil_consortium_manager:auditor:managed_groups:membership:by_group:ignored:update", + args=[obj.group.name, obj.ignored_email], + ) + self.assertContains(response, expected_url) + + def test_detail_page_links_user_get_absolute_url(self): + """HTML includes a link to the user profile when the added_by user has a get_absolute_url method.""" + + # Dynamically set the get_absolute_url method. This is hacky... + def foo(self): + return "test_profile_{}".format(self.username) + + UserModel = get_user_model() + setattr(UserModel, "get_absolute_url", foo) + user = UserModel.objects.create(username="testuser2", password="testpassword") + obj = factories.IgnoredManagedGroupMembershipFactory.create(added_by=user) + self.client.force_login(self.user) + response = self.client.get(obj.get_absolute_url()) + self.assertContains(response, user.get_absolute_url()) + + +class IgnoredManagedGroupMembershipCreateTest(TestCase): + """Tests for the IgnoredManagedGroupMembershipCreate view.""" + + def setUp(self): + """Set up test class.""" + # The superclass uses the responses package to mock API responses. + super().setUp() + self.factory = RequestFactory() + # Create a user with both view and edit permissions. + self.user = User.objects.create_user(username="test", password="test") + self.user.user_permissions.add( + Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) + ) + self.user.user_permissions.add( + Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME) + ) + self.group = ManagedGroupFactory.create() + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("anvil_consortium_manager:auditor:managed_groups:membership:by_group:ignored:new", args=args) + + def get_view(self): + """Return the view being tested.""" + return views.IgnoredManagedGroupMembershipCreate.as_view() + + def test_view_redirect_not_logged_in(self): + "View redirects to login view when user is not logged in." + # Need a client for redirects. + response = self.client.get(self.get_url("foo", "bar@example.com")) + self.assertRedirects( + response, + resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url("foo", "bar@example.com"), + ) + + def test_status_code_with_user_permission(self): + """Returns successful response code.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.group.name, "foo@bar.com")) + self.assertEqual(response.status_code, 200) + + def test_access_with_view_permission(self): + """Raises permission denied if user has only view permission.""" + user_with_view_perm = User.objects.create_user(username="test-other", password="test-other") + user_with_view_perm.user_permissions.add( + Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) + ) + request = self.factory.get(self.get_url("foo", "bar@example.com")) + request.user = user_with_view_perm + with self.assertRaises(PermissionDenied): + self.get_view()(request) + + def test_access_with_limited_view_permission(self): + """Raises permission denied if user has limited view permission.""" + user = User.objects.create_user(username="test-limited", password="test-limited") + user.user_permissions.add(Permission.objects.get(codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME)) + request = self.factory.get(self.get_url("foo", "bar@example.com")) + request.user = user + with self.assertRaises(PermissionDenied): + self.get_view()(request) + + def test_access_without_user_permission(self): + """Raises permission denied if user has no permissions.""" + user_no_perms = User.objects.create_user(username="test-none", password="test-none") + request = self.factory.get(self.get_url("foo", "bar@example.com")) + request.user = user_no_perms + with self.assertRaises(PermissionDenied): + self.get_view()(request) + + def test_has_form_in_context(self): + """Response includes a form.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.group.name, fake.email())) + self.assertTrue("form" in response.context_data) + self.assertIsInstance(response.context_data["form"], forms.IgnoredManagedGroupMembershipForm) + + def test_context_group(self): + """Context contains the group.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.group.name, fake.email())) + self.assertTrue("group" in response.context_data) + self.assertEqual(response.context_data["group"], self.group) + + def test_context_email(self): + """Context contains the email.""" + email = fake.email() + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.group.name, email)) + self.assertTrue("email" in response.context_data) + self.assertEqual(response.context_data["email"], email) + + def test_form_hidden_input(self): + """The proper inputs are hidden in the form.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.group.name, fake.email())) + self.assertTrue("form" in response.context_data) + self.assertIsInstance(response.context_data["form"].fields["group"].widget, HiddenInput) + self.assertIsInstance(response.context_data["form"].fields["ignored_email"].widget, HiddenInput) + + def test_get_initial(self): + """Initial data is set correctly.""" + email = fake.email() + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.group.name, email)) + initial = response.context_data["form"].initial + self.assertIn("group", initial) + self.assertEqual(self.group, initial["group"]) + self.assertIn("ignored_email", initial) + self.assertEqual(email, initial["ignored_email"]) + + def test_can_create_an_object(self): + """Posting valid data to the form creates an object.""" + self.client.force_login(self.user) + response = self.client.post( + self.get_url(self.group.name, "my@email.com"), + {"group": self.group.pk, "ignored_email": "my@email.com", "note": "foo bar"}, + ) + self.assertEqual(response.status_code, 302) + new_object = models.IgnoredManagedGroupMembership.objects.latest("pk") + self.assertIsInstance(new_object, models.IgnoredManagedGroupMembership) + self.assertEqual(new_object.group, self.group) + self.assertEqual(new_object.ignored_email, "my@email.com") + self.assertEqual(new_object.note, "foo bar") + self.assertEqual(new_object.added_by, self.user) + + def test_success_message(self): + """Response includes a success message if successful.""" + email = fake.email() + self.client.force_login(self.user) + response = self.client.post( + self.get_url(self.group.name, email), + { + "group": self.group.pk, + "ignored_email": email, + "note": fake.sentence(), + }, + follow=True, + ) + messages = [m.message for m in get_messages(response.wsgi_request)] + self.assertEqual(len(messages), 1) + self.assertEqual(views.IgnoredManagedGroupMembershipCreate.success_message, str(messages[0])) + + def test_success_redirect(self): + """After successfully creating an object, view redirects to the model's list view.""" + # This needs to use the client because the RequestFactory doesn't handle redirects. + email = fake.email() + self.client.force_login(self.user) + response = self.client.post( + self.get_url(self.group.name, email), + { + "group": self.group.pk, + "ignored_email": email, + "note": fake.sentence(), + }, + ) + obj = models.IgnoredManagedGroupMembership.objects.latest("pk") + self.assertRedirects(response, obj.get_absolute_url()) + + def test_cannot_create_duplicate_object(self): + """Cannot create a second object for the same group and email.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create(note="original note") + self.client.force_login(self.user) + response = self.client.post( + self.get_url(self.group.name, obj.ignored_email), + {"group": obj.group.pk, "ignored_email": obj.ignored_email, "note": "foo"}, + ) + self.assertEqual(response.status_code, 200) + form = response.context_data["form"] + self.assertFalse(form.is_valid()) + # import ipdb; ipdb.set_trace() + self.assertIn("already exists", form.non_field_errors()[0]) + self.assertQuerySetEqual( + models.IgnoredManagedGroupMembership.objects.all(), + models.IgnoredManagedGroupMembership.objects.filter(pk=obj.pk), + ) + obj.refresh_from_db() + self.assertEqual(obj.note, "original note") + + def test_get_group_not_found(self): + """Raises 404 if group in URL does not exist when posting data.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url("foo", "test@eaxmple.com")) + self.assertEqual(response.status_code, 404) + self.assertEqual(models.IgnoredManagedGroupMembership.objects.count(), 0) + + def test_post_group_not_found(self): + """Raises 404 if group in URL does not exist when posting data.""" + self.client.force_login(self.user) + response = self.client.post( + self.get_url("foo", "test@eaxmple.com"), + { + "group": "foo", + "ignored_email": "test@example.com", + "note": "a note", + }, + ) + self.assertEqual(response.status_code, 404) + self.assertEqual(models.IgnoredManagedGroupMembership.objects.count(), 0) + + def test_group_not_managed_by_app(self): + """Form is not valid if the group is not managed by the app.""" + group = ManagedGroupFactory.create(is_managed_by_app=False) + self.client.force_login(self.user) + response = self.client.post( + self.get_url(group.name, "test@example.com"), + { + "group": group.pk, + "ignored_email": "test@example.com", + "note": " a note", + }, + ) + self.assertEqual(response.status_code, 200) + form = response.context_data["form"] + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertIn("group", form.errors) + self.assertEqual(len(form.errors["group"]), 1) + self.assertIn("valid choice", form.errors["group"][0]) + self.assertEqual(models.IgnoredManagedGroupMembership.objects.count(), 0) + + def test_invalid_input_email(self): + """Posting invalid data to role field does not create an object.""" + self.client.force_login(self.user) + response = self.client.post( + self.get_url(self.group.name, "foo"), + { + "group": self.group.pk, + "ignored_email": "foo", + "note": "bar", + }, + ) + self.assertEqual(response.status_code, 200) + form = response.context_data["form"] + self.assertFalse(form.is_valid()) + self.assertIn("ignored_email", form.errors.keys()) + self.assertEqual(len(form.errors["ignored_email"]), 1) + self.assertIn("valid email", form.errors["ignored_email"][0]) + self.assertEqual(models.IgnoredManagedGroupMembership.objects.count(), 0) + + def test_post_blank_data(self): + """Posting blank data does not create an object.""" + self.client.force_login(self.user) + response = self.client.post(self.get_url(self.group.name, "foo@bar.com"), {}) + self.assertEqual(response.status_code, 200) + form = response.context_data["form"] + self.assertFalse(form.is_valid()) + self.assertIn("group", form.errors.keys()) + self.assertIn("required", form.errors["group"][0]) + self.assertIn("ignored_email", form.errors.keys()) + self.assertIn("required", form.errors["ignored_email"][0]) + self.assertIn("note", form.errors.keys()) + self.assertIn("required", form.errors["note"][0]) + self.assertEqual(models.IgnoredManagedGroupMembership.objects.count(), 0) + + def test_post_blank_data_group(self): + """Posting blank data to the group field does not create an object.""" + email = fake.email(0) + self.client.force_login(self.user) + response = self.client.post( + self.get_url(self.group.name, email), + { + "ignored_email": email, + "note": "foo bar", + }, + ) + self.assertEqual(response.status_code, 200) + form = response.context_data["form"] + self.assertFalse(form.is_valid()) + self.assertIn("group", form.errors.keys()) + self.assertIn("required", form.errors["group"][0]) + self.assertEqual(models.IgnoredManagedGroupMembership.objects.count(), 0) + + def test_post_blank_data_email(self): + """Posting blank data to the account field does not create an object.""" + email = fake.email(0) + self.client.force_login(self.user) + response = self.client.post( + self.get_url(self.group.name, email), + { + "group": self.group.pk, + # "ignored_email": email, + "note": "foo bar", + }, + ) + self.assertEqual(response.status_code, 200) + form = response.context_data["form"] + self.assertFalse(form.is_valid()) + self.assertIn("ignored_email", form.errors.keys()) + self.assertIn("required", form.errors["ignored_email"][0]) + self.assertEqual(models.IgnoredManagedGroupMembership.objects.count(), 0) + + def test_post_blank_data_note(self): + """Posting blank data to the note field does not create an object.""" + email = fake.email(0) + self.client.force_login(self.user) + response = self.client.post( + self.get_url(self.group.name, email), + { + "group": self.group.pk, + "ignored_email": email, + # "note": "foo bar", + }, + ) + self.assertEqual(response.status_code, 200) + form = response.context_data["form"] + self.assertFalse(form.is_valid()) + self.assertIn("note", form.errors.keys()) + self.assertIn("required", form.errors["note"][0]) + self.assertEqual(models.IgnoredManagedGroupMembership.objects.count(), 0) + + def test_get_object_exists(self): + obj = factories.IgnoredManagedGroupMembershipFactory.create() + self.client.force_login(self.user) + response = self.client.get(self.get_url(obj.group.name, obj.ignored_email)) + self.assertRedirects(response, obj.get_absolute_url()) + + def test_post_object_exists(self): + obj = factories.IgnoredManagedGroupMembershipFactory.create() + self.client.force_login(self.user) + response = self.client.post( + self.get_url(obj.group.name, obj.ignored_email), + { + "group": obj.group.pk, + "ignored_email": obj.ignored_email, + "note": fake.sentence(), + }, + ) + self.assertRedirects(response, obj.get_absolute_url()) + self.assertEqual(models.IgnoredManagedGroupMembership.objects.count(), 1) + self.assertIn(obj, models.IgnoredManagedGroupMembership.objects.all()) + + +class IgnoredManagedGroupMembershipDeleteTest(TestCase): + def setUp(self): + """Set up test class.""" + super().setUp() + self.factory = RequestFactory() + # Create a user with both view and edit permissions. + self.user = User.objects.create_user(username="test", password="test") + self.user.user_permissions.add( + Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) + ) + self.user.user_permissions.add( + Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME) + ) + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("anvil_consortium_manager:auditor:managed_groups:membership:by_group:ignored:delete", args=args) + + def get_view(self): + """Return the view being tested.""" + return views.IgnoredManagedGroupMembershipDelete.as_view() + + def test_view_redirect_not_logged_in(self): + "View redirects to login view when user is not logged in." + # Need a client for redirects. + response = self.client.get(self.get_url("foo", "bar")) + self.assertRedirects(response, resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url("foo", "bar")) + + def test_status_code_with_user_permission(self): + """Returns successful response code.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create() + self.client.force_login(self.user) + response = self.client.get(self.get_url(obj.group, obj.ignored_email)) + self.assertEqual(response.status_code, 200) + + def test_access_with_view_permission(self): + """Raises permission denied if user has only view permission.""" + user_with_view_perm = User.objects.create_user(username="test-other", password="test-other") + user_with_view_perm.user_permissions.add( + Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) + ) + request = self.factory.get(self.get_url("foo", "bar")) + request.user = user_with_view_perm + with self.assertRaises(PermissionDenied): + self.get_view()(request, slug="foo", email="bar") + + def test_access_with_limited_view_permission(self): + """Raises permission denied if user has limited view permission.""" + user = User.objects.create_user(username="test-limited", password="test-limited") + user.user_permissions.add(Permission.objects.get(codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME)) + request = self.factory.get(self.get_url("foo", "bar")) + request.user = user + with self.assertRaises(PermissionDenied): + self.get_view()(request, slug="foo", email="bar") + + def test_access_without_user_permission(self): + """Raises permission denied if user has no permissions.""" + user_no_perms = User.objects.create_user(username="test-none", password="test-none") + request = self.factory.get(self.get_url("foo", "bar")) + request.user = user_no_perms + with self.assertRaises(PermissionDenied): + self.get_view()(request, slug="foo", email="bar") + + def test_view_with_invalid_pk(self): + """Returns a 404 when the object doesn't exist.""" + request = self.factory.get(self.get_url("foo", "bar")) + request.user = self.user + with self.assertRaises(Http404): + self.get_view()(request, slug="foo", email="bar") + + def test_view_deletes_object(self): + """Posting submit to the form successfully deletes the object.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create() + self.client.force_login(self.user) + response = self.client.post(self.get_url(obj.group, obj.ignored_email), {"submit": ""}) + self.assertEqual(response.status_code, 302) + self.assertEqual(Account.objects.count(), 0) + # History is added. + self.assertEqual(obj.history.count(), 2) + self.assertEqual(obj.history.latest().history_type, "-") + + def test_success_message(self): + """Response includes a success message if successful.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create() + self.client.force_login(self.user) + response = self.client.post(self.get_url(obj.group, obj.ignored_email), {"submit": ""}, follow=True) + messages = [m.message for m in get_messages(response.wsgi_request)] + self.assertEqual(len(messages), 1) + self.assertEqual(views.IgnoredManagedGroupMembershipDelete.success_message, str(messages[0])) + + def test_only_deletes_specified_pk(self): + """View only deletes the specified pk.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create() + other_obj = factories.IgnoredManagedGroupMembershipFactory.create() + self.client.force_login(self.user) + response = self.client.post(self.get_url(obj.group, obj.ignored_email), {"submit": ""}) + self.assertEqual(response.status_code, 302) + self.assertEqual(models.IgnoredManagedGroupMembership.objects.count(), 1) + self.assertQuerySetEqual( + Account.objects.all(), + Account.objects.filter(pk=other_obj.pk), + ) + + def test_success_url(self): + """Redirects to the expected page.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create() + # Need to use the client instead of RequestFactory to check redirection url. + self.client.force_login(self.user) + response = self.client.post(self.get_url(obj.group, obj.ignored_email), {"submit": ""}) + self.assertEqual(response.status_code, 302) + self.assertRedirects(response, obj.group.get_absolute_url()) + + +class IgnoredManagedGroupMembershipListTest(TestCase): + def setUp(self): + """Set up test class.""" + self.factory = RequestFactory() + # Create a user with both view and edit permission. + self.user = User.objects.create_user(username="test", password="test") + self.user.user_permissions.add( + Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) + ) + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("anvil_consortium_manager:auditor:managed_groups:membership:ignored", args=args) + + def get_view(self): + """Return the view being tested.""" + return views.IgnoredManagedGroupMembershipList.as_view() + + def test_view_redirect_not_logged_in(self): + "View redirects to login view when user is not logged in." + # Need a client for redirects. + response = self.client.get(self.get_url()) + self.assertRedirects(response, resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url()) + + def test_status_code_with_user_permission(self): + """Returns successful response code.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, 200) + + def test_access_with_limited_view_permission(self): + """Raises permission denied if user has limited view permission.""" + user = User.objects.create_user(username="test-limited", password="test-limited") + user.user_permissions.add(Permission.objects.get(codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME)) + request = self.factory.get(self.get_url()) + request.user = user + with self.assertRaises(PermissionDenied): + self.get_view()(request) + + def test_template_with_user_permission(self): + """Returns successful response code.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, 200) + + def test_access_without_user_permission(self): + """Raises permission denied if user has no permissions.""" + user_no_perms = User.objects.create_user(username="test-none", password="test-none") + request = self.factory.get(self.get_url()) + request.user = user_no_perms + with self.assertRaises(PermissionDenied): + self.get_view()(request) + + def test_view_has_correct_table_class(self): + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("table", response.context_data) + self.assertIsInstance(response.context_data["table"], tables.IgnoredManagedGroupMembershipTable) + + def test_view_with_no_objects(self): + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 0) + + def test_view_with_one_object(self): + factories.IgnoredManagedGroupMembershipFactory() + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 1) + + def test_view_with_two_objects(self): + factories.IgnoredManagedGroupMembershipFactory.create_batch(2) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 2) + + def test_view_with_filter_group_name_return_no_object(self): + factories.IgnoredManagedGroupMembershipFactory.create(group__name="foo") + factories.IgnoredManagedGroupMembershipFactory.create(group__name="bar") + self.client.force_login(self.user) + response = self.client.get(self.get_url(), {"group__name__icontains": "abc"}) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 0) + + def test_view_with_filter_group_name_returns_one_object_exact(self): + instance = factories.IgnoredManagedGroupMembershipFactory.create(group__name="foo") + factories.IgnoredManagedGroupMembershipFactory.create(group__name="bar") + self.client.force_login(self.user) + response = self.client.get(self.get_url(), {"group__name__icontains": "foo"}) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 1) + self.assertIn(instance, response.context_data["table"].data) + + def test_view_with_filter_group_name_returns_one_object_case_insensitive(self): + instance = factories.IgnoredManagedGroupMembershipFactory.create(group__name="Foo") + factories.IgnoredManagedGroupMembershipFactory.create(group__name="bar") + self.client.force_login(self.user) + response = self.client.get(self.get_url(), {"group__name__icontains": "foo"}) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 1) + self.assertIn(instance, response.context_data["table"].data) + + def test_view_with_filter_group_name_returns_one_object_case_contains(self): + instance = factories.IgnoredManagedGroupMembershipFactory.create(group__name="foo") + factories.IgnoredManagedGroupMembershipFactory.create(group__name="bar") + self.client.force_login(self.user) + response = self.client.get(self.get_url(), {"group__name__icontains": "oo"}) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 1) + self.assertIn(instance, response.context_data["table"].data) + + def test_view_with_filter_group_name_returns_multiple_objects(self): + instance_1 = factories.IgnoredManagedGroupMembershipFactory.create(group__name="group1") + instance_2 = factories.IgnoredManagedGroupMembershipFactory.create(group__name="group2") + self.client.force_login(self.user) + response = self.client.get(self.get_url(), {"group__name__icontains": "group"}) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 2) + self.assertIn(instance_1, response.context_data["table"].data) + self.assertIn(instance_2, response.context_data["table"].data) + + def test_view_with_filter_email_return_no_object(self): + factories.IgnoredManagedGroupMembershipFactory.create(ignored_email="foo@test.com") + factories.IgnoredManagedGroupMembershipFactory.create(ignored_email="bar") + self.client.force_login(self.user) + response = self.client.get(self.get_url(), {"ignored_email__icontains": "abc"}) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 0) + + def test_view_with_filter_email_returns_one_object_exact(self): + instance = factories.IgnoredManagedGroupMembershipFactory.create(ignored_email="foo@test.com") + factories.IgnoredManagedGroupMembershipFactory.create(ignored_email="bar@test.com") + self.client.force_login(self.user) + response = self.client.get(self.get_url(), {"ignored_email__icontains": "foo@test.com"}) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 1) + self.assertIn(instance, response.context_data["table"].data) + + def test_view_with_filter_email_returns_one_object_case_insensitive(self): + instance = factories.IgnoredManagedGroupMembershipFactory.create(ignored_email="Foo@test.com") + factories.IgnoredManagedGroupMembershipFactory.create(ignored_email="bar@test.com") + self.client.force_login(self.user) + response = self.client.get(self.get_url(), {"ignored_email__icontains": "foo@test.com"}) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 1) + self.assertIn(instance, response.context_data["table"].data) + + def test_view_with_filter_email_returns_one_object_case_contains(self): + instance = factories.IgnoredManagedGroupMembershipFactory.create(ignored_email="foo@test.com") + factories.IgnoredManagedGroupMembershipFactory.create(ignored_email="bar@test.com") + self.client.force_login(self.user) + response = self.client.get(self.get_url(), {"ignored_email__icontains": "oo"}) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 1) + self.assertIn(instance, response.context_data["table"].data) + + def test_view_with_filter_email_returns_multiple_objects(self): + instance_1 = factories.IgnoredManagedGroupMembershipFactory.create(ignored_email="foo1@test.com") + instance_2 = factories.IgnoredManagedGroupMembershipFactory.create(ignored_email="foo2@test.com") + self.client.force_login(self.user) + response = self.client.get(self.get_url(), {"ignored_email__icontains": "foo"}) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 2) + self.assertIn(instance_1, response.context_data["table"].data) + self.assertIn(instance_2, response.context_data["table"].data) + + def test_view_with_filter_group_name_and_email(self): + factories.IgnoredManagedGroupMembershipFactory.create(group__name="abc", ignored_email="foo@test.com") + instance = factories.IgnoredManagedGroupMembershipFactory.create( + group__name="def", ignored_email="foo@test.com" + ) + factories.IgnoredManagedGroupMembershipFactory.create(group__name="def", ignored_email="bar@test.com") + self.client.force_login(self.user) + response = self.client.get(self.get_url(), {"group__name__icontains": "def", "ignored_email__icontains": "foo"}) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 1) + self.assertIn(instance, response.context_data["table"].data) + + +class IgnoredManagedGroupMembershipUpdateTest(TestCase): + def setUp(self): + """Set up test class.""" + super().setUp() + self.factory = RequestFactory() + # Create a user with both view and edit permissions. + self.user = User.objects.create_user(username="test", password="test") + self.user.user_permissions.add( + Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) + ) + self.user.user_permissions.add( + Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME) + ) + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("anvil_consortium_manager:auditor:managed_groups:membership:by_group:ignored:update", args=args) + + def get_view(self): + """Return the view being tested.""" + return views.IgnoredManagedGroupMembershipUpdate.as_view() + + def test_view_redirect_not_logged_in(self): + "View redirects to login view when user is not logged in." + # Need a client for redirects. + response = self.client.get(self.get_url("foo", "bar")) + self.assertRedirects(response, resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url("foo", "bar")) + + def test_status_code_with_user_permission(self): + """Returns successful response code.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create() + self.client.force_login(self.user) + response = self.client.get(self.get_url(obj.group.name, obj.ignored_email)) + self.assertEqual(response.status_code, 200) + + def test_access_with_view_permission(self): + """Raises permission denied if user has only view permission.""" + user_with_view_perm = User.objects.create_user(username="test-other", password="test-other") + user_with_view_perm.user_permissions.add( + Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) + ) + request = self.factory.get(self.get_url("foo", "bar")) + request.user = user_with_view_perm + with self.assertRaises(PermissionDenied): + self.get_view()(request, slug="foo", email="bar") + + def test_access_with_limited_view_permission(self): + """Raises permission denied if user has limited view permission.""" + user = User.objects.create_user(username="test-limited", password="test-limited") + user.user_permissions.add(Permission.objects.get(codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME)) + request = self.factory.get(self.get_url("foo", "bar")) + request.user = user + with self.assertRaises(PermissionDenied): + self.get_view()(request, slug="foo", email="bar") + + def test_access_without_user_permission(self): + """Raises permission denied if user has no permissions.""" + user_no_perms = User.objects.create_user(username="test-none", password="test-none") + request = self.factory.get(self.get_url("foo", "bar")) + request.user = user_no_perms + with self.assertRaises(PermissionDenied): + self.get_view()(request, slug="foo", email="bar") + + def test_object_does_not_exist(self): + """Raises Http404 if object does not exist.""" + request = self.factory.get(self.get_url("foo", "bar")) + request.user = self.user + with self.assertRaises(Http404): + self.get_view()(request, slug="foo", email="bar") + + def test_has_form_in_context(self): + """Response includes a form.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create() + self.client.force_login(self.user) + response = self.client.get(self.get_url(obj.group.name, obj.ignored_email)) + self.assertTrue("form" in response.context_data) + # Form is auto-generated by the view, so don't check the class. + + def test_can_modify_note(self): + """Can set the note when creating a billing project.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create(note="original note") + # Need a client for messages. + self.client.force_login(self.user) + response = self.client.post(self.get_url(obj.group.name, obj.ignored_email), {"note": "new note"}) + self.assertEqual(response.status_code, 302) + obj.refresh_from_db() + self.assertEqual(obj.note, "new note") + + def test_success_message(self): + """Response includes a success message if successful.""" + obj = factories.IgnoredManagedGroupMembershipFactory.create() + self.client.force_login(self.user) + response = self.client.post(self.get_url(obj.group.name, obj.ignored_email), {"note": "new note"}, follow=True) + messages = [m.message for m in get_messages(response.wsgi_request)] + self.assertEqual(len(messages), 1) + self.assertEqual(views.IgnoredManagedGroupMembershipUpdate.success_message, str(messages[0])) + + def test_redirects_to_object_detail(self): + """After successfully creating an object, view redirects to the object's detail page.""" + # This needs to use the client because the RequestFactory doesn't handle redirects. + obj = factories.IgnoredManagedGroupMembershipFactory.create() + self.client.force_login(self.user) + response = self.client.post(self.get_url(obj.group.name, obj.ignored_email), {"note": "new note"}) + self.assertRedirects(response, obj.get_absolute_url()) + + def test_missing_note(self): + obj = factories.IgnoredManagedGroupMembershipFactory.create(note="original note") + self.client.force_login(self.user) + response = self.client.post(self.get_url(obj.group.name, obj.ignored_email), {}) + self.assertEqual(response.status_code, 200) + form = response.context_data["form"] + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertIn("note", form.errors) + self.assertEqual(len(form.errors["note"]), 1) + self.assertIn("required", form.errors["note"][0]) + obj.refresh_from_db() + self.assertEqual(obj.note, "original note") + + def test_blank_note(self): + obj = factories.IgnoredManagedGroupMembershipFactory.create(note="original note") + self.client.force_login(self.user) + response = self.client.post(self.get_url(obj.group.name, obj.ignored_email), {"note": ""}) + self.assertEqual(response.status_code, 200) + form = response.context_data["form"] + self.assertFalse(form.is_valid()) + self.assertEqual(len(form.errors), 1) + self.assertIn("note", form.errors) + self.assertEqual(len(form.errors["note"]), 1) + self.assertIn("required", form.errors["note"][0]) + obj.refresh_from_db() + self.assertEqual(obj.note, "original note") + + +class WorkspaceAuditTest(AnVILAPIMockTestMixin, TestCase): + """Tests for the WorkspaceAudit view.""" + + def setUp(self): + """Set up test class.""" + super().setUp() + self.factory = RequestFactory() + # Create a user with only view permission. + self.user = User.objects.create_user(username="test", password="test") + self.user.user_permissions.add( + Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) + ) + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("anvil_consortium_manager:auditor:workspaces:all", args=args) + + def get_api_url(self): + return self.api_client.rawls_entry_point + "/api/workspaces" + + def get_api_workspace_json(self, billing_project_name, workspace_name, access, auth_domains=[]): + """Return the json dictionary for a single workspace on AnVIL.""" + return { + "accessLevel": access, + "workspace": { + "name": workspace_name, + "namespace": billing_project_name, + "authorizationDomain": [{"membersGroupName": x} for x in auth_domains], + "isLocked": False, + }, + } + + def get_api_workspace_acl_url(self, billing_project_name, workspace_name): + return ( + self.api_client.rawls_entry_point + + "/api/workspaces/" + + billing_project_name + + "/" + + workspace_name + + "/acl" + ) + + def get_api_workspace_acl_response(self): + """Return a json for the workspace/acl method where no one else can access.""" + return { + "acl": { + self.service_account_email: { + "accessLevel": "OWNER", + "canCompute": True, + "canShare": True, + "pending": False, + } + } + } + + def get_api_bucket_options_url(self, billing_project_name, workspace_name): + return self.api_client.rawls_entry_point + "/api/workspaces/" + billing_project_name + "/" + workspace_name + + def get_api_bucket_options_response(self): + """Return a json for the workspace/acl method that is not requester pays.""" + return {"bucketOptions": {"requesterPays": False}} + + def get_view(self): + """Return the view being tested.""" + return views.WorkspaceAudit.as_view() + + def test_view_redirect_not_logged_in(self): + "View redirects to login view when user is not logged in." + # Need a client for redirects. + response = self.client.get(self.get_url()) + self.assertRedirects(response, resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url()) + + def test_status_code_with_user_permission(self): + """Returns successful response code.""" + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[], + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, 200) + + def test_access_with_limited_view_permission(self): + """Raises permission denied if user has limited view permission.""" + user = User.objects.create_user(username="test-limited", password="test-limited") + user.user_permissions.add(Permission.objects.get(codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME)) + request = self.factory.get(self.get_url()) + request.user = user + with self.assertRaises(PermissionDenied): + self.get_view()(request) + + def test_access_without_user_permission(self): + """Raises permission denied if user has no permissions.""" + user_no_perms = User.objects.create_user(username="test-none", password="test-none") + request = self.factory.get(self.get_url()) + request.user = user_no_perms + with self.assertRaises(PermissionDenied): + self.get_view()(request) + + def test_template(self): + """Template loads successfully.""" + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[], + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, 200) + + def test_audit_verified(self): + """audit_verified is in the context data.""" + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[], + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("verified_table", response.context_data) + self.assertIsInstance(response.context_data["verified_table"], base_audit.VerifiedTable) + self.assertEqual(len(response.context_data["verified_table"].rows), 0) + + def test_audit_verified_one_record(self): + """audit_verified with one verified record.""" + workspace = WorkspaceFactory.create() + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[self.get_api_workspace_json(workspace.billing_project.name, workspace.name, "OWNER")], + ) + # Response to check workspace access. + workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_workspace_acl_response(), + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("verified_table", response.context_data) + self.assertEqual(len(response.context_data["verified_table"].rows), 1) + + def test_audit_errors(self): + """audit_errors is in the context data.""" + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[], + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("error_table", response.context_data) + self.assertIsInstance(response.context_data["error_table"], base_audit.ErrorTable) + self.assertEqual(len(response.context_data["error_table"].rows), 0) + + def test_audit_errors_one_record(self): + """audit_errors with one error record.""" + workspace = WorkspaceFactory.create() + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + # Error - we are not an owner. + json=[self.get_api_workspace_json(workspace.billing_project.name, workspace.name, "READER")], + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("error_table", response.context_data) + self.assertEqual(len(response.context_data["error_table"].rows), 1) + + def test_audit_not_in_app(self): + """audit_not_in_app is in the context data.""" + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[], + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("not_in_app_table", response.context_data) + self.assertIsInstance(response.context_data["not_in_app_table"], base_audit.NotInAppTable) + self.assertEqual(len(response.context_data["not_in_app_table"].rows), 0) + + def test_audit_not_in_app_one_record(self): + """audit_not_in_app with one record not in app.""" + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[self.get_api_workspace_json("foo", "bar", "OWNER")], + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("not_in_app_table", response.context_data) + self.assertEqual(len(response.context_data["not_in_app_table"].rows), 1) + + def test_audit_ok_is_ok(self): + """audit_ok when audit_results.ok() is True.""" + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + json=[], + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("audit_ok", response.context_data) + self.assertEqual(response.context_data["audit_ok"], True) + + def test_audit_ok_is_not_ok(self): + """audit_ok when audit_results.ok() is False.""" + workspace = WorkspaceFactory.create() + api_url = self.get_api_url() + self.anvil_response_mock.add( + responses.GET, + api_url, + status=200, + # Error - we are not admin. + json=[self.get_api_workspace_json(workspace.billing_project.name, workspace.name, "READER")], + ) + # Response to check workspace bucket options. + workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) + self.anvil_response_mock.add( + responses.GET, + workspace_acl_url, + status=200, + json=self.get_api_bucket_options_response(), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertIn("audit_ok", response.context_data) + self.assertEqual(response.context_data["audit_ok"], False) + + +class WorkspaceSharingAuditTest(AnVILAPIMockTestMixin, TestCase): + """Tests for the WorkspaceSharingAudit view.""" + + def setUp(self): + """Set up test class.""" + super().setUp() + self.factory = RequestFactory() + # Create a user with both view and edit permission. + self.user = User.objects.create_user(username="test", password="test") + self.user.user_permissions.add( + Permission.objects.get(codename=AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) + ) + # Set this variable here because it will include the service account. + # Tests can update it with the update_api_response method. + self.api_response = {"acl": {}} + self.update_api_response(self.service_account_email, "OWNER", can_compute=True, can_share=True) + # Create a workspace for use in tests. + self.workspace = WorkspaceFactory.create() + self.api_url = ( + self.api_client.rawls_entry_point + + "/api/workspaces/" + + self.workspace.billing_project.name + + "/" + + self.workspace.name + + "/acl" + ) + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("anvil_consortium_manager:auditor:workspaces:sharing:all", args=args) + + def update_api_response(self, email, access, can_compute=False, can_share=False): + """Return a paired down json for a single ACL, including the service account.""" + self.api_response["acl"].update( + { + email: { + "accessLevel": access, + "canCompute": can_compute, + "canShare": can_share, + "pending": False, + } + } + ) + + def get_view(self): + """Return the view being tested.""" + return views.WorkspaceSharingAudit.as_view() + + def test_view_redirect_not_logged_in(self): + "View redirects to login view when user is not logged in." + # Need a client for redirects. + response = self.client.get(self.get_url("foo", "bar")) + self.assertRedirects( + response, + resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url("foo", "bar"), + ) + + def test_status_code_with_user_permission(self): + """Returns successful response code.""" + # Group membership API call. + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.workspace.billing_project.name, self.workspace.name)) + self.assertEqual(response.status_code, 200) + + def test_access_with_limited_view_permission(self): + """Raises permission denied if user has limited view permission.""" + user = User.objects.create_user(username="test-limited", password="test-limited") + user.user_permissions.add(Permission.objects.get(codename=AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME)) + request = self.factory.get(self.get_url("foo", "bar")) + request.user = user + with self.assertRaises(PermissionDenied): + self.get_view()(request) + + def test_access_without_user_permission(self): + """Raises permission denied if user has no permissions.""" + user_no_perms = User.objects.create_user(username="test-none", password="test-none") + request = self.factory.get(self.get_url("foo", "bar")) + request.user = user_no_perms + with self.assertRaises(PermissionDenied): + self.get_view()(request, billing_project_slug="foo", workspace_slug="bar") + + def test_template(self): + """Template loads successfully.""" + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.workspace.billing_project.name, self.workspace.name)) + self.assertEqual(response.status_code, 200) + + def test_audit_verified(self): + """audit_verified is in the context data.""" + # Group membership API call. + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.workspace.billing_project.name, self.workspace.name)) + self.assertIn("verified_table", response.context_data) + self.assertIsInstance(response.context_data["verified_table"], base_audit.VerifiedTable) + self.assertEqual(len(response.context_data["verified_table"].rows), 0) + + def test_audit_verified_one_record(self): + """audit_verified with one verified record.""" + access = WorkspaceGroupSharingFactory.create(workspace=self.workspace) + self.update_api_response(access.group.email, "READER") + # Group membership API call. + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.workspace.billing_project.name, self.workspace.name)) + self.assertIn("verified_table", response.context_data) + self.assertEqual(len(response.context_data["verified_table"].rows), 1) + + def test_audit_errors(self): + """audit_errors is in the context data.""" + # Group membership API call. + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.workspace.billing_project.name, self.workspace.name)) + self.assertIn("error_table", response.context_data) + self.assertIsInstance(response.context_data["error_table"], base_audit.ErrorTable) + self.assertEqual(len(response.context_data["error_table"].rows), 0) + + def test_audit_errors_one_record(self): + """audit_errors with one error record.""" + access = WorkspaceGroupSharingFactory.create(workspace=self.workspace) + # Different access recorded. + self.update_api_response(access.group.email, "WRITER") + # Group membership API call. + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.workspace.billing_project.name, self.workspace.name)) + self.assertIn("error_table", response.context_data) + self.assertEqual(len(response.context_data["error_table"].rows), 1) + + def test_audit_not_in_app(self): + """audit_not_in_app is in the context data.""" + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + # Admin insetad of member. + # json=self.get_api_group_members_json_response(), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.workspace.billing_project.name, self.workspace.name)) + self.assertIn("not_in_app_table", response.context_data) + self.assertIsInstance(response.context_data["not_in_app_table"], base_audit.NotInAppTable) + self.assertEqual(len(response.context_data["not_in_app_table"].rows), 0) + + def test_audit_not_in_app_one_record(self): + """audit_not_in_app with one record not in app.""" + self.update_api_response("foo@bar.com", "READER") + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.workspace.billing_project.name, self.workspace.name)) + self.assertIn("not_in_app_table", response.context_data) + self.assertEqual(len(response.context_data["not_in_app_table"].rows), 1) + + def test_audit_ok_is_ok(self): + """audit_ok when audit_results.ok() is True.""" + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + # Admin insetad of member. + # json=self.get_api_group_members_json_response(), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.workspace.billing_project.name, self.workspace.name)) + self.assertIn("audit_ok", response.context_data) + self.assertEqual(response.context_data["audit_ok"], True) + + def test_audit_ok_is_not_ok(self): + """audit_ok when audit_results.ok() is False.""" + self.update_api_response("foo@bar.com", "READER") + self.anvil_response_mock.add( + responses.GET, + self.api_url, + status=200, + json=self.api_response, + # Not in app + # json=self.get_api_group_members_json_response(members=["foo@bar.com"]), + ) + self.client.force_login(self.user) + response = self.client.get(self.get_url(self.workspace.billing_project.name, self.workspace.name)) + self.assertIn("audit_ok", response.context_data) + self.assertEqual(response.context_data["audit_ok"], False) + + def test_workspace_does_not_exist_in_app(self): + """Raises a 404 error with an invalid workspace slug.""" + billing_project = BillingProjectFactory.create() + request = self.factory.get(self.get_url(billing_project.name, self.workspace.name)) + request.user = self.user + with self.assertRaises(Http404): + self.get_view()( + request, + billing_project_slug=billing_project.name, + workspace_slug=self.workspace.name, + ) + + def test_billing_project_does_not_exist_in_app(self): + """Raises a 404 error with an invalid billing project slug.""" + request = self.factory.get(self.get_url("foo", self.workspace.name)) + request.user = self.user + with self.assertRaises(Http404): + self.get_view()(request, billing_project_slug="foo", workspace_slug=self.workspace.name) diff --git a/anvil_consortium_manager/auditor/urls.py b/anvil_consortium_manager/auditor/urls.py new file mode 100644 index 00000000..de4e8e52 --- /dev/null +++ b/anvil_consortium_manager/auditor/urls.py @@ -0,0 +1,79 @@ +from django.urls import include, path + +from . import views + +app_name = "auditor" + + +billing_project_patterns = ( + [ + path("", views.BillingProjectAudit.as_view(), name="all"), + ], + "billing_projects", +) + +account_patterns = ( + [ + path("audit/", views.AccountAudit.as_view(), name="all"), + ], + "accounts", +) + +managed_group_membership_by_group_ignore_patterns = ( + [ + path("/", views.IgnoredManagedGroupMembershipDetail.as_view(), name="detail"), + path("/new/", views.IgnoredManagedGroupMembershipCreate.as_view(), name="new"), + path("/update/", views.IgnoredManagedGroupMembershipUpdate.as_view(), name="update"), + path("/delete/", views.IgnoredManagedGroupMembershipDelete.as_view(), name="delete"), + ], + "ignored", +) + +managed_group_membership_by_group_patterns = ( + [ + path("", views.ManagedGroupMembershipAudit.as_view(), name="all"), + path("ignored/", include(managed_group_membership_by_group_ignore_patterns)), + ], + "by_group", +) +managed_group_membership_patterns = ( + [ + path("ignored/", views.IgnoredManagedGroupMembershipList.as_view(), name="ignored"), + path("/", include(managed_group_membership_by_group_patterns)), + ], + "membership", +) + +managed_group_patterns = ( + [ + path("audit/", views.ManagedGroupAudit.as_view(), name="all"), + path("membership/", include(managed_group_membership_patterns)), + ], + "managed_groups", +) + +workspace_sharing_patterns = ( + [ + path( + "", + views.WorkspaceSharingAudit.as_view(), + name="all", + ), + ], + "sharing", +) + +workspace_patterns = ( + [ + path("", views.WorkspaceAudit.as_view(), name="all"), + path("//sharing/", include(workspace_sharing_patterns)), + ], + "workspaces", +) + +urlpatterns = [ + path("billing_projects/", include(billing_project_patterns)), + path("accounts/", include(account_patterns)), + path("managed_groups/", include(managed_group_patterns)), + path("workspaces/", include(workspace_patterns)), +] diff --git a/anvil_consortium_manager/auditor/viewmixins.py b/anvil_consortium_manager/auditor/viewmixins.py new file mode 100644 index 00000000..72cc9342 --- /dev/null +++ b/anvil_consortium_manager/auditor/viewmixins.py @@ -0,0 +1,36 @@ +from django.core.exceptions import ImproperlyConfigured +from django.utils import timezone + + +class AnVILAuditMixin: + """Mixin to display AnVIL audit results.""" + + audit_class = None + + def get_audit_instance(self): + if not self.audit_class: + raise ImproperlyConfigured( + "%(cls)s is missing an audit class. Define %(cls)s.audit_class or override " + "%(cls)s.get_audit_instance()." % {"cls": self.__class__.__name__} + ) + else: + return self.audit_class() + + def run_audit(self): + self.audit_results = self.get_audit_instance() + self.audit_results.run_audit() + + def get(self, request, *args, **kwargs): + self.run_audit() + return super().get(request, *args, **kwargs) + + def get_context_data(self, *args, **kwargs): + """Add audit results to the context data.""" + context = super().get_context_data(*args, **kwargs) + context["audit_timestamp"] = timezone.now() + context["audit_ok"] = self.audit_results.ok() + context["verified_table"] = self.audit_results.get_verified_table() + context["error_table"] = self.audit_results.get_error_table() + context["not_in_app_table"] = self.audit_results.get_not_in_app_table() + context["ignored_table"] = self.audit_results.get_ignored_table() + return context diff --git a/anvil_consortium_manager/auditor/views.py b/anvil_consortium_manager/auditor/views.py new file mode 100644 index 00000000..4b914311 --- /dev/null +++ b/anvil_consortium_manager/auditor/views.py @@ -0,0 +1,280 @@ +from django.contrib import messages +from django.contrib.messages.views import SuccessMessageMixin +from django.forms import HiddenInput +from django.http import Http404, HttpResponseRedirect +from django.utils.translation import gettext_lazy as _ +from django.views.generic import CreateView, DeleteView, DetailView, TemplateView, UpdateView +from django.views.generic.detail import SingleObjectMixin +from django_filters.views import FilterView +from django_tables2.views import SingleTableMixin + +from anvil_consortium_manager import auth +from anvil_consortium_manager.models import ManagedGroup, Workspace + +from . import filters, forms, models, tables, viewmixins +from .audit import accounts as account_audit +from .audit import billing_projects as billing_project_audit +from .audit import managed_groups as managed_group_audit +from .audit import workspaces as workspace_audit + + +class BillingProjectAudit(auth.AnVILConsortiumManagerStaffViewRequired, viewmixins.AnVILAuditMixin, TemplateView): + """View to run an audit on Workspaces and display the results.""" + + template_name = "anvil_consortium_manager/billing_project_audit.html" + audit_class = billing_project_audit.BillingProjectAudit + + +class AccountAudit(auth.AnVILConsortiumManagerStaffViewRequired, viewmixins.AnVILAuditMixin, TemplateView): + """View to run an audit on Accounts and display the results.""" + + template_name = "anvil_consortium_manager/account_audit.html" + audit_class = account_audit.AccountAudit + + +class ManagedGroupAudit(auth.AnVILConsortiumManagerStaffViewRequired, viewmixins.AnVILAuditMixin, TemplateView): + """View to run an audit on ManagedGroups and display the results.""" + + template_name = "anvil_consortium_manager/managedgroup_audit.html" + audit_class = managed_group_audit.ManagedGroupAudit + + +class ManagedGroupMembershipAudit( + auth.AnVILConsortiumManagerStaffViewRequired, + SingleObjectMixin, + viewmixins.AnVILAuditMixin, + TemplateView, +): + """View to run an audit on ManagedGroups and display the results.""" + + model = ManagedGroup + slug_field = "name" + template_name = "anvil_consortium_manager/managedgroup_membership_audit.html" + message_not_managed_by_app = "Cannot audit membership because group is not managed by this app." + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + # Check if managed by the app. + if not self.object.is_managed_by_app: + messages.add_message( + self.request, + messages.ERROR, + self.message_not_managed_by_app, + ) + # Redirect to the object detail page. + return HttpResponseRedirect(self.object.get_absolute_url()) + # Otherwise, return the response. + return super().get(request, *args, **kwargs) + + def get_audit_instance(self): + return managed_group_audit.ManagedGroupMembershipAudit(self.object) + + +class IgnoredManagedGroupMembershipDetail(auth.AnVILConsortiumManagerStaffViewRequired, DetailView): + """View to display the details of an models.IgnoredManagedGroupMembership.""" + + model = models.IgnoredManagedGroupMembership + + def get_object(self, queryset=None): + """Return the object the view is displaying.""" + if queryset is None: + queryset = self.get_queryset() + # Filter the queryset based on kwargs. + group_slug = self.kwargs.get("slug", None) + email = self.kwargs.get("email", None) + queryset = queryset.filter(group__name=group_slug, ignored_email=email) + try: + # Get the single item from the filtered queryset + obj = queryset.get() + except queryset.model.DoesNotExist: + raise Http404( + _("No %(verbose_name)s found matching the query") % {"verbose_name": queryset.model._meta.verbose_name} + ) + return obj + + +class IgnoredManagedGroupMembershipCreate( + auth.AnVILConsortiumManagerStaffEditRequired, SuccessMessageMixin, CreateView +): + """View to create a new models.IgnoredManagedGroupMembership.""" + + model = models.IgnoredManagedGroupMembership + form_class = forms.IgnoredManagedGroupMembershipForm + message_already_exists = "Record already exists for this group and email." + success_message = "Successfully ignored managed group membership." + + def get_group(self): + try: + name = self.kwargs["slug"] + group = ManagedGroup.objects.get(name=name) + except ManagedGroup.DoesNotExist: + raise Http404("ManagedGroup not found.") + return group + + def get_email(self): + return self.kwargs["email"] + + def get(self, request, *args, **kwargs): + self.group = self.get_group() + self.email = self.get_email() + try: + obj = models.IgnoredManagedGroupMembership.objects.get(group=self.group, ignored_email=self.email) + messages.error(self.request, self.message_already_exists) + return HttpResponseRedirect(obj.get_absolute_url()) + except models.IgnoredManagedGroupMembership.DoesNotExist: + return super().get(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + self.group = self.get_group() + self.email = self.get_email() + try: + obj = models.IgnoredManagedGroupMembership.objects.get(group=self.group, ignored_email=self.email) + messages.error(self.request, self.message_already_exists) + return HttpResponseRedirect(obj.get_absolute_url()) + except models.IgnoredManagedGroupMembership.DoesNotExist: + return super().post(request, *args, **kwargs) + + def get_initial(self): + initial = super().get_initial() + initial["group"] = self.group + initial["ignored_email"] = self.email + return initial + + def get_form(self, **kwargs): + """Get the form and set the inputs to use a hidden widget.""" + form = super().get_form(**kwargs) + form.fields["group"].widget = HiddenInput() + form.fields["ignored_email"].widget = HiddenInput() + return form + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["group"] = self.group + context["email"] = self.email + return context + + def form_valid(self, form): + """If the form is valid, save the associated model.""" + form.instance.added_by = self.request.user + return super().form_valid(form) + + +class IgnoredManagedGroupMembershipList(auth.AnVILConsortiumManagerStaffViewRequired, SingleTableMixin, FilterView): + """View to display a list of models.IgnoredManagedGroupMembership.""" + + model = models.IgnoredManagedGroupMembership + table_class = tables.IgnoredManagedGroupMembershipTable + template_name = "auditor/ignoredmanagedgroupmembership_list.html" + filterset_class = filters.IgnoredManagedGroupMembershipFilter + + +class IgnoredManagedGroupMembershipUpdate( + auth.AnVILConsortiumManagerStaffEditRequired, SuccessMessageMixin, UpdateView +): + """View to update an existing models.IgnoredManagedGroupMembership.""" + + model = models.IgnoredManagedGroupMembership + fields = ("note",) + success_message = "Successfully updated ignored record." + + def get_object(self, queryset=None): + if queryset is None: + queryset = self.get_queryset() + # Filter the queryset based on kwargs. + group_slug = self.kwargs.get("slug", None) + email = self.kwargs.get("email", None) + queryset = queryset.filter(group__name=group_slug, ignored_email=email) + try: + # Get the single item from the filtered queryset + obj = queryset.get() + except queryset.model.DoesNotExist: + raise Http404( + _("No %(verbose_name)s found matching the query") % {"verbose_name": queryset.model._meta.verbose_name} + ) + return obj + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["group"] = self.object.group + context["email"] = self.object.ignored_email + return context + + +class IgnoredManagedGroupMembershipDelete( + auth.AnVILConsortiumManagerStaffEditRequired, SuccessMessageMixin, DeleteView +): + model = models.IgnoredManagedGroupMembership + success_message = "Successfully stopped ignoring managed group membership record." + + def get_object(self, queryset=None): + """Return the object the view is displaying.""" + + # Use a custom queryset if provided; this is required for subclasses + # like DateDetailView + if queryset is None: + queryset = self.get_queryset() + # Filter the queryset based on kwargs. + group_slug = self.kwargs.get("slug", None) + email = self.kwargs.get("email", None) + queryset = queryset.filter( + group__name=group_slug, + ignored_email=email, + ) + try: + # Get the single item from the filtered queryset + obj = queryset.get() + except queryset.model.DoesNotExist: + raise Http404( + _("No %(verbose_name)s found matching the query") % {"verbose_name": queryset.model._meta.verbose_name} + ) + return obj + + def get_success_url(self): + return self.object.group.get_absolute_url() + + +class WorkspaceAudit(auth.AnVILConsortiumManagerStaffViewRequired, viewmixins.AnVILAuditMixin, TemplateView): + """View to run an audit on Workspaces and display the results.""" + + template_name = "anvil_consortium_manager/workspace_audit.html" + audit_class = workspace_audit.WorkspaceAudit + + +class WorkspaceSharingAudit( + auth.AnVILConsortiumManagerStaffViewRequired, + SingleObjectMixin, + viewmixins.AnVILAuditMixin, + TemplateView, +): + """View to run an audit on access to a specific Workspace and display the results.""" + + model = Workspace + template_name = "anvil_consortium_manager/workspace_sharing_audit.html" + + def get_object(self, queryset=None): + """Return the object the view is displaying.""" + + # Use a custom queryset if provided; this is required for subclasses + # like DateDetailView + if queryset is None: + queryset = self.get_queryset() + # Filter the queryset based on kwargs. + billing_project_slug = self.kwargs.get("billing_project_slug", None) + workspace_slug = self.kwargs.get("workspace_slug", None) + queryset = queryset.filter(billing_project__name=billing_project_slug, name=workspace_slug) + try: + # Get the single item from the filtered queryset + obj = queryset.get() + except queryset.model.DoesNotExist: + raise Http404( + _("No %(verbose_name)s found matching the query") % {"verbose_name": queryset.model._meta.verbose_name} + ) + return obj + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + # Otherwise, return the response. + return super().get(request, *args, **kwargs) + + def get_audit_instance(self): + return workspace_audit.WorkspaceSharingAudit(self.object) diff --git a/anvil_consortium_manager/templates/anvil_consortium_manager/audit.html b/anvil_consortium_manager/templates/anvil_consortium_manager/audit.html index 7491206b..0a3b1647 100644 --- a/anvil_consortium_manager/templates/anvil_consortium_manager/audit.html +++ b/anvil_consortium_manager/templates/anvil_consortium_manager/audit.html @@ -103,5 +103,30 @@

{% endblock not_in_app %} +{% block ignored %} +
+
+
+

+ +

+
+
+ +

This table shows any "not in app" records that have been marked as ignored.

+ + {% render_table ignored_table %} + +
+
+
+
+
+{% endblock ignored %} + {% endblock content %} diff --git a/anvil_consortium_manager/templates/anvil_consortium_manager/index.html b/anvil_consortium_manager/templates/anvil_consortium_manager/index.html index adca82f9..2c6a7aac 100644 --- a/anvil_consortium_manager/templates/anvil_consortium_manager/index.html +++ b/anvil_consortium_manager/templates/anvil_consortium_manager/index.html @@ -45,10 +45,6 @@

Billin {% endif %} -
  • - Audit billing projects - -
  • @@ -72,10 +68,6 @@
    Accounts
    {% endif %} -
  • - Audit accounts - -
  • @@ -112,10 +104,6 @@
    Managed {% endif %} -
  • - Audit groups - -
  • @@ -146,12 +134,48 @@
    Workspace {% endif %} + + + + + +
    +
    +
    AnVIL Audits
    +
    +

    + Audit information in this app against AnVIL. + These audits check that the information in this app is consistent with AnVIL. +

    +
    + +
  • + List ignored group memberships + +
  • + +
    @@ -164,7 +188,7 @@
    Workspace
    -

    © 2023 Adrienne Stilp

    +

    © 2024-5 Adrienne Stilp

    AnVIL Consortium Manager v{{ app_version }}

    diff --git a/anvil_consortium_manager/templates/anvil_consortium_manager/managedgroup_detail.html b/anvil_consortium_manager/templates/anvil_consortium_manager/managedgroup_detail.html index 7f96a27e..2ccc22e8 100644 --- a/anvil_consortium_manager/templates/anvil_consortium_manager/managedgroup_detail.html +++ b/anvil_consortium_manager/templates/anvil_consortium_manager/managedgroup_detail.html @@ -174,7 +174,7 @@

    {% endif %}

    - Audit membership + Audit membership

    {% if show_edit_links %}

    diff --git a/anvil_consortium_manager/templates/anvil_consortium_manager/navbar.html b/anvil_consortium_manager/templates/anvil_consortium_manager/navbar.html index 57687f0d..25bf9906 100644 --- a/anvil_consortium_manager/templates/anvil_consortium_manager/navbar.html +++ b/anvil_consortium_manager/templates/anvil_consortium_manager/navbar.html @@ -34,10 +34,6 @@ {% endif %} -

  • - Audit billing projects -
  • - @@ -57,9 +53,6 @@ {% endif %} -
  • - Audit accounts -
  • @@ -85,9 +78,7 @@ Add a group to a group {% endif %} -
  • - Audit groups -
  • + @@ -110,13 +101,11 @@ {% endif %} -
  • - Audit workspaces -
  • - + {% include 'auditor/nav_items.html' %} + {% endblock anvil_consortium_manager_nav_items %} diff --git a/anvil_consortium_manager/templates/anvil_consortium_manager/snippets/audit_managedgroupmembership_notinapp_ignore_button.html b/anvil_consortium_manager/templates/anvil_consortium_manager/snippets/audit_managedgroupmembership_notinapp_ignore_button.html new file mode 100644 index 00000000..62df9e72 --- /dev/null +++ b/anvil_consortium_manager/templates/anvil_consortium_manager/snippets/audit_managedgroupmembership_notinapp_ignore_button.html @@ -0,0 +1,8 @@ + + + Add ignored result + diff --git a/anvil_consortium_manager/templates/anvil_consortium_manager/workspace_detail.html b/anvil_consortium_manager/templates/anvil_consortium_manager/workspace_detail.html index 10b5754d..4914e9e4 100644 --- a/anvil_consortium_manager/templates/anvil_consortium_manager/workspace_detail.html +++ b/anvil_consortium_manager/templates/anvil_consortium_manager/workspace_detail.html @@ -112,7 +112,7 @@

    {% block action_buttons %}

    {% if perms.anvil_consortium_manager.anvil_consortium_manager_staff_view %} - Audit sharing + Audit sharing {% endif %} {% if show_edit_links %} diff --git a/anvil_consortium_manager/templates/anvil_consortium_manager/email_audit_report.html b/anvil_consortium_manager/templates/auditor/email_audit_report.html similarity index 82% rename from anvil_consortium_manager/templates/anvil_consortium_manager/email_audit_report.html rename to anvil_consortium_manager/templates/auditor/email_audit_report.html index 28434835..90ea23f6 100644 --- a/anvil_consortium_manager/templates/anvil_consortium_manager/email_audit_report.html +++ b/anvil_consortium_manager/templates/auditor/email_audit_report.html @@ -17,7 +17,12 @@

    Audit report - {{ model_name }}

    Verified

    -

    {{ verified_results|length }} instance(s) verified.

    +

    {{ audit_results.get_verified_results|length }} instance(s) verified.

    +
    + +

    Ignored

    +
    +

    Ignoring {{ n_ignored }} record(s)

    Errors

    diff --git a/anvil_consortium_manager/templates/auditor/ignoredmanagedgroupmembership_confirm_delete.html b/anvil_consortium_manager/templates/auditor/ignoredmanagedgroupmembership_confirm_delete.html new file mode 100644 index 00000000..704d8b5e --- /dev/null +++ b/anvil_consortium_manager/templates/auditor/ignoredmanagedgroupmembership_confirm_delete.html @@ -0,0 +1,29 @@ +{% extends "anvil_consortium_manager/base.html" %} +{% load static %} + +{% block title %}Stop ignoring Managed Group Membership Audit result{% endblock %} + +{% block content %} +
    + +
    +
    + + +

    Stop ignoring Managed Group Membership Audit result

    + +

    Are you sure you want to remove {{ object }} on AnVIL?

    + +
    {% csrf_token %} + {{ form }} + + + No, cancel +
    + +
    +
    + + +
    +{% endblock content %} diff --git a/anvil_consortium_manager/templates/auditor/ignoredmanagedgroupmembership_detail.html b/anvil_consortium_manager/templates/auditor/ignoredmanagedgroupmembership_detail.html new file mode 100644 index 00000000..4429e986 --- /dev/null +++ b/anvil_consortium_manager/templates/auditor/ignoredmanagedgroupmembership_detail.html @@ -0,0 +1,35 @@ +{% extends "anvil_consortium_manager/__object_detail.html" %} +{% load static %} + + +{% block title %}{{ object }}{% endblock %} + + +{% block panel %} +
    +
    Managed Group
    + {{ object.group }} +
    +
    Ignored email
    {{ object.ignored_email }}
    +
    Added by
    + {% if object.added_by.get_absolute_url %} + {{ object.added_by }} + {% else %} + {{ object.added_by }} + {% endif %} +
    +
    Date created
    {{ object.created }}
    +
    Date modified
    {{ object.modified }}
    +
    +{% endblock panel %} + + +{% block action_buttons %} + +{% if perms.anvil_consortium_manager.anvil_consortium_manager_staff_edit %} +

    + Update + Stop ignoring +

    +{% endif %} +{% endblock action_buttons %} diff --git a/anvil_consortium_manager/templates/auditor/ignoredmanagedgroupmembership_form.html b/anvil_consortium_manager/templates/auditor/ignoredmanagedgroupmembership_form.html new file mode 100644 index 00000000..7429345d --- /dev/null +++ b/anvil_consortium_manager/templates/auditor/ignoredmanagedgroupmembership_form.html @@ -0,0 +1,26 @@ +{% extends "anvil_consortium_manager/__object_detail.html" %} +{% load static %} +{% load crispy_forms_tags %} + +{% block title %}Ignore Managed Group Membership audit result{% endblock title %} + +{% block panel %} +
    +
    Managed group
    + {{ group }} +
    +
    Ignored email
    {{ email }}
    +{% endblock panel %} + +{% block after_panel %} + +
    + {% csrf_token %} + {{ form|crispy }} + +
    +{% endblock after_panel %} + +{% block inline_javascript %} + {{ form.media }} +{% endblock inline_javascript %} diff --git a/anvil_consortium_manager/templates/auditor/ignoredmanagedgroupmembership_list.html b/anvil_consortium_manager/templates/auditor/ignoredmanagedgroupmembership_list.html new file mode 100644 index 00000000..e4dcdf0d --- /dev/null +++ b/anvil_consortium_manager/templates/auditor/ignoredmanagedgroupmembership_list.html @@ -0,0 +1,32 @@ +{% extends "anvil_consortium_manager/base.html" %} +{% load static %} + +{% load render_table from django_tables2 %} +{% load crispy_forms_tags %} + +{% block title %}Ignored Managed Group membership audit records{% endblock %} + +{% block content %} +
    + +
    +
    + +

    Ignored Managed Group membership audit records

    + +

    + The following records will be ignored when running AnVIL audits for group membership. +

    + +
    + {% crispy filter.form %} +
    + + {% render_table table %} + +
    +
    + +
    + +{% endblock content %} diff --git a/anvil_consortium_manager/templates/auditor/nav_items.html b/anvil_consortium_manager/templates/auditor/nav_items.html new file mode 100644 index 00000000..2eeec563 --- /dev/null +++ b/anvil_consortium_manager/templates/auditor/nav_items.html @@ -0,0 +1,30 @@ + diff --git a/anvil_consortium_manager/tests/settings/test.py b/anvil_consortium_manager/tests/settings/test.py index 7e9fd426..58dc45db 100644 --- a/anvil_consortium_manager/tests/settings/test.py +++ b/anvil_consortium_manager/tests/settings/test.py @@ -73,6 +73,7 @@ "fontawesomefree", # icons # Your stuff: custom apps go here "anvil_consortium_manager", + "anvil_consortium_manager.auditor", # Test app "anvil_consortium_manager.tests.test_app", ] diff --git a/anvil_consortium_manager/tests/test_audit.py b/anvil_consortium_manager/tests/test_audit.py deleted file mode 100644 index d00a0fc4..00000000 --- a/anvil_consortium_manager/tests/test_audit.py +++ /dev/null @@ -1,4649 +0,0 @@ -import responses -from django.test import TestCase -from faker import Faker - -from .. import exceptions, models -from ..audit import audit -from . import api_factories, factories -from .utils import AnVILAPIMockTestMixin - -fake = Faker() - - -class ModelInstanceResultTest(TestCase): - def test_init(self): - """Constructor works as expected.""" - obj = factories.AccountFactory.create() - result = audit.ModelInstanceResult(obj) - self.assertEqual(result.model_instance, obj) - self.assertEqual(result.errors, set()) - - def test_str(self): - """__str__ method works as expected.""" - obj = factories.AccountFactory.create() - result = audit.ModelInstanceResult(obj) - self.assertEqual(str(result), (str(obj))) - - def test_eq_no_errors(self): - """__eq__ method works as expected when there are no errors.""" - obj = factories.AccountFactory.create() - result_1 = audit.ModelInstanceResult(obj) - result_2 = audit.ModelInstanceResult(obj) - self.assertEqual(result_1, result_2) - - def test_eq_errors(self): - """__eq__ method works as expected when there are errors.""" - obj = factories.AccountFactory.create() - result_1 = audit.ModelInstanceResult(obj) - result_1.add_error("foo") - result_2 = audit.ModelInstanceResult(obj) - self.assertNotEqual(result_1, result_2) - result_2.add_error("foo") - self.assertEqual(result_1, result_2) - - def test_add_error(self): - """add_error method works as expected.""" - obj = factories.AccountFactory.create() - result = audit.ModelInstanceResult(obj) - result.add_error("foo") - self.assertEqual(result.errors, set(["foo"])) - result.add_error("bar") - self.assertEqual(result.errors, set(["foo", "bar"])) - - def test_add_error_duplicate(self): - """can add a second, duplicate error without error.""" - obj = factories.AccountFactory.create() - result = audit.ModelInstanceResult(obj) - result.add_error("foo") - self.assertEqual(result.errors, set(["foo"])) - result.add_error("foo") - self.assertEqual(result.errors, set(["foo"])) - - def test_ok_no_errors(self): - """ok method returns True when there are no errors.""" - obj = factories.AccountFactory.create() - result = audit.ModelInstanceResult(obj) - self.assertTrue(result.ok()) - - def test_ok_errors(self): - """ok method returns False when there are errors.""" - obj = factories.AccountFactory.create() - result = audit.ModelInstanceResult(obj) - result.add_error("foo") - self.assertFalse(result.ok()) - - -class NotInAppResultTest(TestCase): - def test_init(self): - """Constructor works as expected.""" - result = audit.NotInAppResult("foo bar") - self.assertEqual(result.record, "foo bar") - - def test_str(self): - """__str__ method works as expected.""" - result = audit.NotInAppResult("foo bar") - self.assertEqual(str(result), "foo bar") - - def test_eq(self): - """__eq__ method works as expected.""" - result = audit.NotInAppResult("foo") - self.assertEqual(audit.NotInAppResult("foo"), result) - self.assertNotEqual(audit.NotInAppResult("bar"), result) - - -class VerifiedTableTest(TestCase): - def test_zero_rows(self): - results = [] - table = audit.VerifiedTable(results) - self.assertEqual(len(table.rows), 0) - - def test_one_row(self): - results = [audit.ModelInstanceResult(factories.AccountFactory())] - table = audit.VerifiedTable(results) - self.assertEqual(len(table.rows), 1) - - def test_two_rows(self): - results = [ - audit.ModelInstanceResult(factories.AccountFactory()), - audit.ModelInstanceResult(factories.AccountFactory()), - ] - table = audit.VerifiedTable(results) - self.assertEqual(len(table.rows), 2) - - -class ErrorTableTest(TestCase): - def test_zero_rows(self): - results = [] - table = audit.ErrorTable(results) - self.assertEqual(len(table.rows), 0) - - def test_one_row(self): - results = [audit.ModelInstanceResult(factories.AccountFactory())] - table = audit.ErrorTable(results) - self.assertEqual(len(table.rows), 1) - - def test_two_rows(self): - result_1 = audit.ModelInstanceResult(factories.AccountFactory()) - result_1.add_error("foo") - result_2 = audit.ModelInstanceResult(factories.AccountFactory()) - result_2.add_error("bar") - results = [result_1, result_2] - table = audit.ErrorTable(results) - self.assertEqual(len(table.rows), 2) - - -class AnVILAuditTest(TestCase): - """Tests for the AnVILAudit abstract base class.""" - - def setUp(self): - super().setUp() - - class GenericAudit(audit.AnVILAudit): - TEST_ERROR_1 = "Test error 1" - TEST_ERROR_2 = "Test error 2" - - self.audit_results = GenericAudit() - # It doesn't matter what model we use at this point, so just pick Account. - self.model_factory = factories.AccountFactory - - def test_init(self): - """Init method works as expected.""" - self.assertEqual(len(self.audit_results._model_instance_results), 0) - self.assertEqual(len(self.audit_results._not_in_app_results), 0) - - def test_ok_no_results(self): - """ok() returns True when there are no results.""" - self.assertTrue(self.audit_results.ok()) - - def test_ok_one_result_ok(self): - """ok() returns True when there is one ok result.""" - model_instance_result = audit.ModelInstanceResult(self.model_factory()) - self.audit_results.add_result(model_instance_result) - self.assertTrue(self.audit_results.ok()) - - def test_ok_two_results_ok(self): - """ok() returns True when there is one ok result.""" - model_instance_result_1 = audit.ModelInstanceResult(self.model_factory()) - self.audit_results.add_result(model_instance_result_1) - model_instance_result_2 = audit.ModelInstanceResult(self.model_factory()) - self.audit_results.add_result(model_instance_result_2) - self.assertTrue(self.audit_results.ok()) - - def test_ok_one_result_with_errors(self): - """ok() returns True when there is one ok result.""" - model_instance_result = audit.ModelInstanceResult(self.model_factory()) - model_instance_result.add_error("foo") - self.audit_results.add_result(model_instance_result) - self.assertFalse(self.audit_results.ok()) - - def test_ok_one_not_in_app(self): - """ok() returns True when there are no results.""" - self.audit_results.add_result(audit.NotInAppResult("foo")) - self.assertFalse(self.audit_results.ok()) - - def test_run_audit_not_implemented(self): - with self.assertRaises(NotImplementedError): - self.audit_results.run_audit() - - def test_add_result_not_in_app(self): - """Can add a NotInAppResult.""" - not_in_app_result = audit.NotInAppResult("foo") - self.audit_results.add_result(not_in_app_result) - self.assertEqual(len(self.audit_results._not_in_app_results), 1) - - def test_add_result_wrong_class(self): - """Can add a NotInAppResult.""" - with self.assertRaises(ValueError): - self.audit_results.add_result("foo") - - def test_add_result_duplicate_not_in_app(self): - """Cannot add a duplicate NotInAppResult.""" - not_in_app_result = audit.NotInAppResult("foo") - self.audit_results.add_result(not_in_app_result) - # import ipdb; ipdb.set_trace() - with self.assertRaises(ValueError): - self.audit_results.add_result(not_in_app_result) - self.assertEqual(len(self.audit_results._not_in_app_results), 1) - - def test_add_result_not_in_app_same_record(self): - """Cannot add a duplicate NotInAppResult.""" - not_in_app_result = audit.NotInAppResult("foo") - self.audit_results.add_result(not_in_app_result) - # import ipdb; ipdb.set_trace() - with self.assertRaises(ValueError): - self.audit_results.add_result(audit.NotInAppResult("foo")) - self.assertEqual(len(self.audit_results._not_in_app_results), 1) - - def test_add_result_model_instance(self): - """Can add a model instance result.""" - model_instance_result = audit.ModelInstanceResult(self.model_factory()) - self.audit_results.add_result(model_instance_result) - self.assertEqual(len(self.audit_results._model_instance_results), 1) - - def test_add_result_duplicate_model_instance_result(self): - """Cannot add a duplicate model instance result.""" - model_instance_result = audit.ModelInstanceResult(self.model_factory()) - self.audit_results.add_result(model_instance_result) - # import ipdb; ipdb.set_trace() - with self.assertRaises(ValueError): - self.audit_results.add_result(model_instance_result) - self.assertEqual(len(self.audit_results._model_instance_results), 1) - - def test_add_result_second_result_for_same_model_instance(self): - obj = self.model_factory() - model_instance_result_1 = audit.ModelInstanceResult(obj) - self.audit_results.add_result(model_instance_result_1) - model_instance_result_2 = audit.ModelInstanceResult(obj) - # import ipdb; ipdb.set_trace() - with self.assertRaises(ValueError): - self.audit_results.add_result(model_instance_result_2) - self.assertEqual(len(self.audit_results._model_instance_results), 1) - self.assertEqual(self.audit_results._model_instance_results, [model_instance_result_1]) - - def test_add_result_second_result_for_same_model_instance_with_error(self): - obj = self.model_factory() - model_instance_result_1 = audit.ModelInstanceResult(obj) - self.audit_results.add_result(model_instance_result_1) - model_instance_result_2 = audit.ModelInstanceResult(obj) - model_instance_result_2.add_error("Foo") - with self.assertRaises(ValueError): - self.audit_results.add_result(model_instance_result_2) - self.assertEqual(len(self.audit_results._model_instance_results), 1) - self.assertEqual(self.audit_results._model_instance_results, [model_instance_result_1]) - - def test_get_result_for_model_instance_no_matches(self): - obj = self.model_factory() - audit.ModelInstanceResult(obj) - with self.assertRaises(ValueError): - self.audit_results.get_result_for_model_instance(obj) - - def test_get_result_for_model_instance_one_match(self): - obj = self.model_factory() - model_instance_result = audit.ModelInstanceResult(obj) - self.audit_results.add_result(model_instance_result) - result = self.audit_results.get_result_for_model_instance(obj) - self.assertIs(result, model_instance_result) - - def test_get_verified_results_no_results(self): - """get_verified_results returns an empty list when there are no results.""" - self.assertEqual(len(self.audit_results.get_verified_results()), 0) - - def test_get_verified_results_one_verified_result(self): - """get_verified_results returns a list when there is one result.""" - model_instance_result = audit.ModelInstanceResult(self.model_factory()) - self.audit_results.add_result(model_instance_result) - self.assertEqual(len(self.audit_results.get_verified_results()), 1) - self.assertIn(model_instance_result, self.audit_results.get_verified_results()) - - def test_get_error_results_two_verified_result(self): - """get_verified_results returns a list of lenght two when there are two verified results.""" - model_instance_result_1 = audit.ModelInstanceResult(self.model_factory()) - self.audit_results.add_result(model_instance_result_1) - model_instance_result_2 = audit.ModelInstanceResult(self.model_factory()) - self.audit_results.add_result(model_instance_result_2) - self.assertEqual(len(self.audit_results.get_verified_results()), 2) - self.assertIn(model_instance_result_1, self.audit_results.get_verified_results()) - self.assertIn(model_instance_result_2, self.audit_results.get_verified_results()) - - def test_get_verified_results_one_error_result(self): - """get_verified_results returns a list of lenght zero when there is one error result.""" - model_instance_result = audit.ModelInstanceResult(self.model_factory()) - model_instance_result.add_error("foo") - self.audit_results.add_result(model_instance_result) - self.assertEqual(len(self.audit_results.get_verified_results()), 0) - - def test_get_verified_results_one_not_in_app_result(self): - """get_verified_results returns a list of lenght zero when there is one not_in_app result.""" - self.audit_results.add_result(audit.NotInAppResult("foo")) - self.assertEqual(len(self.audit_results.get_verified_results()), 0) - - def test_get_error_results_no_results(self): - """get_error_results returns an empty list when there are no results.""" - self.assertEqual(len(self.audit_results.get_error_results()), 0) - - def test_get_error_results_one_verified_result(self): - """get_error_results returns a list of length zero when there is one verified result.""" - model_instance_result = audit.ModelInstanceResult(self.model_factory()) - self.audit_results.add_result(model_instance_result) - self.assertEqual(len(self.audit_results.get_error_results()), 0) - - def test_get_error_results_one_error_result(self): - """get_error_results returns a list of lenght one when there is one error result.""" - model_instance_result = audit.ModelInstanceResult(self.model_factory()) - model_instance_result.add_error("foo") - self.audit_results.add_result(model_instance_result) - self.assertEqual(len(self.audit_results.get_error_results()), 1) - self.assertIn(model_instance_result, self.audit_results.get_error_results()) - - def test_get_error_results_two_error_result(self): - """get_error_results returns a list of lenght two when there is one result.""" - model_instance_result_1 = audit.ModelInstanceResult(self.model_factory()) - model_instance_result_1.add_error("foo") - self.audit_results.add_result(model_instance_result_1) - model_instance_result_2 = audit.ModelInstanceResult(self.model_factory()) - model_instance_result_2.add_error("foo") - self.audit_results.add_result(model_instance_result_2) - self.assertEqual(len(self.audit_results.get_error_results()), 2) - self.assertIn(model_instance_result_1, self.audit_results.get_error_results()) - self.assertIn(model_instance_result_2, self.audit_results.get_error_results()) - - def test_get_error_results_one_not_in_app_result(self): - """get_error_results returns a list of length zero when there is one not_in_app result.""" - self.audit_results.add_result(audit.NotInAppResult("foo")) - self.assertEqual(len(self.audit_results.get_error_results()), 0) - - def test_get_not_in_app_results_no_results(self): - """get_not_in_app_results returns an empty list when there are no results.""" - self.assertEqual(len(self.audit_results.get_not_in_app_results()), 0) - - def test_get_not_in_app_results_one_verified_result(self): - """get_not_in_app_results returns a list of length zero when there is one verified result.""" - model_instance_result = audit.ModelInstanceResult(self.model_factory()) - self.audit_results.add_result(model_instance_result) - self.assertEqual(len(self.audit_results.get_not_in_app_results()), 0) - - def test_get_not_in_app_results_one_error_result(self): - """get_not_in_app_results returns a list of lenght one when there is one error result.""" - model_instance_result = audit.ModelInstanceResult(self.model_factory()) - model_instance_result.add_error("foo") - self.audit_results.add_result(model_instance_result) - self.assertEqual(len(self.audit_results.get_not_in_app_results()), 0) - - def test_get_not_in_app_results_one_not_in_app_result(self): - """get_not_in_app_results returns a list of length zero when there is one not_in_app result.""" - result = audit.NotInAppResult("foo") - self.audit_results.add_result(result) - self.assertEqual(len(self.audit_results.get_not_in_app_results()), 1) - self.assertIn(result, self.audit_results.get_not_in_app_results()) - - def test_get_not_in_app_results_two_not_in_app_results(self): - """get_not_in_app_results returns a list of lenght two when there is one result.""" - result_1 = audit.NotInAppResult("foo") - self.audit_results.add_result(result_1) - result_2 = audit.NotInAppResult("bar") - self.audit_results.add_result(result_2) - self.assertEqual(len(self.audit_results.get_not_in_app_results()), 2) - self.assertIn(result_1, self.audit_results.get_not_in_app_results()) - self.assertIn(result_2, self.audit_results.get_not_in_app_results()) - - def test_export(self): - # One Verified result. - verified_result = audit.ModelInstanceResult(self.model_factory()) - self.audit_results.add_result(verified_result) - # One error result. - error_result = audit.ModelInstanceResult(self.model_factory()) - error_result.add_error("foo") - self.audit_results.add_result(error_result) - # Not in app result. - not_in_app_result = audit.NotInAppResult("bar") - self.audit_results.add_result(not_in_app_result) - # Check export. - exported_data = self.audit_results.export() - self.assertIn("verified", exported_data) - self.assertEqual( - exported_data["verified"], - [ - { - "id": verified_result.model_instance.pk, - "instance": verified_result.model_instance, - } - ], - ) - self.assertIn("errors", exported_data) - self.assertEqual( - exported_data["errors"], - [ - { - "id": error_result.model_instance.pk, - "instance": error_result.model_instance, - "errors": ["foo"], - } - ], - ) - self.assertIn("not_in_app", exported_data) - self.assertEqual(exported_data["not_in_app"], ["bar"]) - - def test_export_include_verified_false(self): - exported_data = self.audit_results.export(include_verified=False) - self.assertNotIn("verified", exported_data) - self.assertIn("errors", exported_data) - self.assertIn("not_in_app", exported_data) - - def test_export_include_errors_false(self): - exported_data = self.audit_results.export(include_errors=False) - self.assertIn("verified", exported_data) - self.assertNotIn("errors", exported_data) - self.assertIn("not_in_app", exported_data) - - def test_export_include_not_in_app_false(self): - exported_data = self.audit_results.export(include_not_in_app=False) - self.assertIn("verified", exported_data) - self.assertIn("errors", exported_data) - self.assertNotIn("not_in_app", exported_data) - - def test_export_not_in_app_sorted(self): - """export sorts the not_in_app results.""" - self.audit_results.add_result(audit.NotInAppResult("foo")) - self.audit_results.add_result(audit.NotInAppResult("bar")) - exported_data = self.audit_results.export() - self.assertEqual(exported_data["not_in_app"], ["bar", "foo"]) - - -class BillingProjectAuditTest(AnVILAPIMockTestMixin, TestCase): - """Tests for the BillingProject.anvil_audit method.""" - - def get_api_url(self, billing_project_name): - return self.api_client.rawls_entry_point + "/api/billing/v2/" + billing_project_name - - def get_api_json_response(self): - return { - "roles": ["User"], - } - - def test_anvil_audit_no_billing_projects(self): - """anvil_audit works correct if there are no billing projects in the app.""" - audit_results = audit.BillingProjectAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - - def test_anvil_audit_one_billing_project_no_errors(self): - """anvil_audit works correct if one billing project exists in the app and in AnVIL.""" - billing_project = factories.BillingProjectFactory.create(has_app_as_user=True) - api_url = self.get_api_url(billing_project.name) - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=self.get_api_json_response(), - ) - audit_results = audit.BillingProjectAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(billing_project) - self.assertTrue(record_result.ok()) - - def test_anvil_audit_one_billing_project_not_on_anvil(self): - """anvil_audit raises exception with one billing project exists in the app but not on AnVIL.""" - billing_project = factories.BillingProjectFactory.create(has_app_as_user=True) - api_url = self.get_api_url(billing_project.name) - self.anvil_response_mock.add( - responses.GET, - api_url, - status=404, - json={"message": "other error"}, - ) - audit_results = audit.BillingProjectAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(billing_project) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) - - def test_anvil_audit_two_billing_projects_no_errors(self): - """anvil_audit returns None if there are two billing projects and both exist on AnVIL.""" - billing_project_1 = factories.BillingProjectFactory.create(has_app_as_user=True) - api_url_1 = self.get_api_url(billing_project_1.name) - self.anvil_response_mock.add( - responses.GET, - api_url_1, - status=200, - json=self.get_api_json_response(), - ) - billing_project_2 = factories.BillingProjectFactory.create(has_app_as_user=True) - api_url_2 = self.get_api_url(billing_project_2.name) - self.anvil_response_mock.add( - responses.GET, - api_url_2, - status=200, - json=self.get_api_json_response(), - ) - audit_results = audit.BillingProjectAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 2) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(billing_project_1) - self.assertTrue(record_result.ok()) - record_result = audit_results.get_result_for_model_instance(billing_project_2) - self.assertTrue(record_result.ok()) - - def test_anvil_audit_two_billing_projects_first_not_on_anvil(self): - """anvil_audit raises exception if two billing projects exist in the app but the first is not on AnVIL.""" - billing_project_1 = factories.BillingProjectFactory.create(has_app_as_user=True) - api_url_1 = self.get_api_url(billing_project_1.name) - self.anvil_response_mock.add( - responses.GET, - api_url_1, - status=404, - json={"message": "other error"}, - ) - billing_project_2 = factories.BillingProjectFactory.create(has_app_as_user=True) - api_url_2 = self.get_api_url(billing_project_2.name) - self.anvil_response_mock.add( - responses.GET, - api_url_2, - status=200, - json=self.get_api_json_response(), - ) - audit_results = audit.BillingProjectAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(billing_project_1) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) - record_result = audit_results.get_result_for_model_instance(billing_project_2) - self.assertTrue(record_result.ok()) - - def test_anvil_audit_two_billing_projects_both_missing(self): - """anvil_audit raises exception if there are two billing projects that exist in the app but not in AnVIL.""" - billing_project_1 = factories.BillingProjectFactory.create(has_app_as_user=True) - api_url_1 = self.get_api_url(billing_project_1.name) - self.anvil_response_mock.add( - responses.GET, - api_url_1, - status=404, - json={"message": "other error"}, - ) - billing_project_2 = factories.BillingProjectFactory.create(has_app_as_user=True) - api_url_2 = self.get_api_url(billing_project_2.name) - self.anvil_response_mock.add( - responses.GET, - api_url_2, - status=404, - json={"message": "other error"}, - ) - audit_results = audit.BillingProjectAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 2) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(billing_project_1) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) - record_result = audit_results.get_result_for_model_instance(billing_project_2) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) - - def test_anvil_audit_ignore_not_has_app_has_user(self): - """anvil_audit does not check AnVIL about billing projects that do not have the app as a user.""" - factories.BillingProjectFactory.create(has_app_as_user=False) - # No API calls made. - audit_results = audit.BillingProjectAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - - -class AccountAuditTest(AnVILAPIMockTestMixin, TestCase): - """Tests for the Account.anvil_audit method.""" - - def get_api_url(self, email): - return self.api_client.sam_entry_point + "/api/users/v1/" + email - - def get_api_json_response(self, email): - id = fake.bothify(text="#" * 21) - return { - "googleSubjectId": id, - "userEmail": email, - "userSubjectId": id, - } - - def test_anvil_audit_no_accounts(self): - """anvil_audit works correct if there are no Accounts in the app.""" - audit_results = audit.AccountAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - - def test_anvil_audit_one_account_no_errors(self): - """anvil_audit works correct if there is one account in the app and it exists on AnVIL.""" - account = factories.AccountFactory.create() - api_url = self.get_api_url(account.email) - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=self.get_api_json_response(account.email), - ) - audit_results = audit.AccountAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(account) - self.assertTrue(record_result.ok()) - - def test_anvil_audit_one_account_not_on_anvil(self): - """anvil_audit raises exception if one billing project exists in the app but not on AnVIL.""" - account = factories.AccountFactory.create() - api_url = self.get_api_url(account.email) - self.anvil_response_mock.add( - responses.GET, - api_url, - status=404, - json={"message": "other error"}, - ) - audit_results = audit.AccountAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(account) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) - - def test_anvil_audit_two_accounts_no_errors(self): - """anvil_audit returns None if if two accounts exist in both the app and AnVIL.""" - account_1 = factories.AccountFactory.create() - api_url_1 = self.get_api_url(account_1.email) - self.anvil_response_mock.add( - responses.GET, - api_url_1, - status=200, - json=self.get_api_json_response(account_1.email), - ) - account_2 = factories.AccountFactory.create() - api_url_2 = self.get_api_url(account_2.email) - self.anvil_response_mock.add( - responses.GET, - api_url_2, - status=200, - json=self.get_api_json_response(account_2.email), - ) - audit_results = audit.AccountAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 2) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(account_1) - self.assertTrue(record_result.ok()) - record_result = audit_results.get_result_for_model_instance(account_2) - self.assertTrue(record_result.ok()) - - def test_anvil_audit_two_accounts_first_not_on_anvil(self): - """anvil_audit raises exception if two accounts exist in the app but the first is not not on AnVIL.""" - account_1 = factories.AccountFactory.create() - api_url_1 = self.get_api_url(account_1.email) - self.anvil_response_mock.add( - responses.GET, - api_url_1, - status=404, - json={"message": "other error"}, - ) - account_2 = factories.AccountFactory.create() - api_url_2 = self.get_api_url(account_2.email) - self.anvil_response_mock.add( - responses.GET, - api_url_2, - status=200, - json=self.get_api_json_response(account_2.email), - ) - audit_results = audit.AccountAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(account_1) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) - record_result = audit_results.get_result_for_model_instance(account_2) - self.assertTrue(record_result.ok()) - - def test_anvil_audit_two_accounts_both_missing(self): - """anvil_audit raises exception if there are two accounts that exist in the app but not in AnVIL.""" - account_1 = factories.AccountFactory.create() - api_url_1 = self.get_api_url(account_1.email) - self.anvil_response_mock.add( - responses.GET, - api_url_1, - status=404, - json={"message": "other error"}, - ) - account_2 = factories.AccountFactory.create() - api_url_2 = self.get_api_url(account_2.email) - self.anvil_response_mock.add( - responses.GET, - api_url_2, - status=404, - json={"message": "other error"}, - ) - audit_results = audit.AccountAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 2) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(account_1) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) - record_result = audit_results.get_result_for_model_instance(account_2) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) - - def test_anvil_audit_deactivated_account(self): - """anvil_audit does not check AnVIL about accounts that are deactivated.""" - account = factories.AccountFactory.create() - account.deactivate() - # No API calls made. - audit_results = audit.AccountAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - - -class ManagedGroupAuditTest(AnVILAPIMockTestMixin, TestCase): - """Tests forthe ManagedGroup.anvil_audit method.""" - - def get_api_groups_url(self): - """Return the API url being called by the method.""" - return self.api_client.sam_entry_point + "/api/groups/v1" - - def get_api_url_members(self, group_name): - """Return the API url being called by the method.""" - return self.api_client.sam_entry_point + "/api/groups/v1/" + group_name + "/member" - - def get_api_url_admins(self, group_name): - """Return the API url being called by the method.""" - return self.api_client.sam_entry_point + "/api/groups/v1/" + group_name + "/admin" - - def test_anvil_audit_no_groups(self): - """anvil_audit works correct if there are no ManagedGroups in the app.""" - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=api_factories.GetGroupsResponseFactory().response, - ) - audit_results = audit.ManagedGroupAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - - def test_anvil_audit_one_group_managed_by_app_no_errors(self): - """anvil_audit works correct if there is one group in the app and it exists on AnVIL.""" - group = factories.ManagedGroupFactory.create(is_managed_by_app=True) - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=api_factories.GetGroupsResponseFactory( - response=[api_factories.GroupDetailsAdminFactory(groupName=group.name)] - ).response, - ) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(group) - self.assertTrue(record_result.ok()) - - def test_anvil_audit_one_group_managed_by_app_lowercase_role(self): - """anvil_audit works correct if there is one account in the app and it exists on AnVIL.""" - group = factories.ManagedGroupFactory.create(is_managed_by_app=True) - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=api_factories.GetGroupsResponseFactory( - response=[api_factories.GroupDetailsAdminFactory(groupName=group.name)] - ).response, - ) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(group) - self.assertTrue(record_result.ok()) - - def test_anvil_audit_one_group_not_managed_by_app_no_errors(self): - """anvil_audit works correct if there is one account in the app and it exists on AnVIL.""" - group = factories.ManagedGroupFactory.create(is_managed_by_app=False) - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=api_factories.GetGroupsResponseFactory( - response=[api_factories.GroupDetailsMemberFactory(groupName=group.name)] - ).response, - ) - audit_results = audit.ManagedGroupAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(group) - self.assertTrue(record_result.ok()) - - def test_anvil_audit_one_group_not_managed_by_app_no_errors_uppercase_role(self): - """anvil_audit works correct if there is one account in the app and it exists on AnVIL.""" - group = factories.ManagedGroupFactory.create(is_managed_by_app=False) - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=api_factories.GetGroupsResponseFactory( - response=[api_factories.GroupDetailsFactory(groupName=group.name, role="Member")] - ).response, - ) - audit_results = audit.ManagedGroupAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(group) - self.assertTrue(record_result.ok()) - - def test_anvil_audit_one_group_not_on_anvil(self): - """anvil_audit raises exception if one group exists in the app but not on AnVIL.""" - group = factories.ManagedGroupFactory.create() - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=api_factories.GetGroupsResponseFactory(n_groups=0).response, - ) - self.anvil_response_mock.add( - responses.GET, - "https://sam.dsde-prod.broadinstitute.org/api/groups/v1/" + group.name, - status=404, - json=api_factories.ErrorResponseFactory().response, - ) - audit_results = audit.ManagedGroupAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(group) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) - - def test_anvil_audit_one_group_on_anvil_but_app_not_in_group_not_managed_by_app( - self, - ): - """anvil_audit is correct if the group is not managed by the app.""" - group = factories.ManagedGroupFactory.create(is_managed_by_app=False) - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=api_factories.GetGroupsResponseFactory(n_groups=0).response, - ) - # Add the response. - self.anvil_response_mock.add( - responses.GET, - "https://sam.dsde-prod.broadinstitute.org/api/groups/v1/" + group.name, - status=200, - json="FOO@BAR.COM", - ) - audit_results = audit.ManagedGroupAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(group) - self.assertTrue(record_result.ok()) - - def test_anvil_audit_one_group_managed_by_app_on_anvil_but_app_not_in_group(self): - """anvil_audit raises exception if one group exists in the app but not on AnVIL.""" - group = factories.ManagedGroupFactory.create(is_managed_by_app=True) - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=api_factories.GetGroupsResponseFactory(n_groups=0).response, - ) - # Add the response. - self.anvil_response_mock.add( - responses.GET, - "https://sam.dsde-prod.broadinstitute.org/api/groups/v1/" + group.name, - status=200, - json="FOO@BAR.COM", - ) - audit_results = audit.ManagedGroupAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(group) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_ROLE])) - - def test_anvil_audit_one_group_admin_in_app_member_on_anvil(self): - """anvil_audit raises exception if one group exists in the app as an admin but the role on AnVIL is member.""" - group = factories.ManagedGroupFactory.create(is_managed_by_app=True) - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=api_factories.GetGroupsResponseFactory( - response=[api_factories.GroupDetailsMemberFactory(groupName=group.name)] - ).response, - ) - audit_results = audit.ManagedGroupAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(group) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_ROLE])) - - def test_anvil_audit_one_group_member_in_app_admin_on_anvil(self): - """anvil_audit raises exception if one group exists in the app as an member but the role on AnVIL is admin.""" - group = factories.ManagedGroupFactory.create(is_managed_by_app=False) - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=api_factories.GetGroupsResponseFactory( - response=[api_factories.GroupDetailsAdminFactory(groupName=group.name)] - ).response, - ) - audit_results = audit.ManagedGroupAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(group) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_ROLE])) - - def test_anvil_audit_two_groups_no_errors(self): - """anvil_audit works correctly if if two groups exist in both the app and AnVIL.""" - group_1 = factories.ManagedGroupFactory.create() - group_2 = factories.ManagedGroupFactory.create(is_managed_by_app=False) - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=api_factories.GetGroupsResponseFactory( - response=[ - api_factories.GroupDetailsAdminFactory(groupName=group_1.name), - api_factories.GroupDetailsMemberFactory(groupName=group_2.name), - ] - ).response, - ) - api_url_members = self.get_api_url_members(group_1.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group_1.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 2) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(group_1) - self.assertTrue(record_result.ok()) - record_result = audit_results.get_result_for_model_instance(group_2) - self.assertTrue(record_result.ok()) - - def test_anvil_audit_two_groups_json_response_order_does_not_matter(self): - """Order of groups in the json response does not matter.""" - group_1 = factories.ManagedGroupFactory.create() - group_2 = factories.ManagedGroupFactory.create(is_managed_by_app=False) - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=api_factories.GetGroupsResponseFactory( - response=[ - api_factories.GroupDetailsMemberFactory(groupName=group_2.name), - api_factories.GroupDetailsAdminFactory(groupName=group_1.name), - ] - ).response, - ) - api_url_members = self.get_api_url_members(group_1.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group_1.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 2) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(group_1) - self.assertTrue(record_result.ok()) - record_result = audit_results.get_result_for_model_instance(group_2) - self.assertTrue(record_result.ok()) - - def test_anvil_audit_two_groups_first_not_on_anvil(self): - """anvil_audit raises exception if two groups exist in the app but the first is not not on AnVIL.""" - group_1 = factories.ManagedGroupFactory.create() - group_2 = factories.ManagedGroupFactory.create() - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=api_factories.GetGroupsResponseFactory( - response=[ - api_factories.GroupDetailsAdminFactory(groupName=group_2.name), - ] - ).response, - ) - # Add response for the group that is not in the app. - self.anvil_response_mock.add( - responses.GET, - "https://sam.dsde-prod.broadinstitute.org/api/groups/v1/" + group_1.name, - status=404, - json=api_factories.ErrorResponseFactory().response, - ) - # Add responses for the group that is in the app. - api_url_members = self.get_api_url_members(group_2.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group_2.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(group_1) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) - record_result = audit_results.get_result_for_model_instance(group_2) - self.assertTrue(record_result.ok()) - - def test_anvil_audit_two_groups_both_missing(self): - """anvil_audit raises exception if there are two groups that exist in the app but not in AnVIL.""" - group_1 = factories.ManagedGroupFactory.create() - group_2 = factories.ManagedGroupFactory.create() - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=api_factories.GetGroupsResponseFactory().response, - ) - # Add response for the group that is not in the app. - self.anvil_response_mock.add( - responses.GET, - "https://sam.dsde-prod.broadinstitute.org/api/groups/v1/" + group_1.name, - status=404, - json=api_factories.ErrorResponseFactory().response, - ) - # Add response for the group that is not in the app. - self.anvil_response_mock.add( - responses.GET, - "https://sam.dsde-prod.broadinstitute.org/api/groups/v1/" + group_2.name, - status=404, - json=api_factories.ErrorResponseFactory().response, - ) - audit_results = audit.ManagedGroupAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 2) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(group_1) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) - record_result = audit_results.get_result_for_model_instance(group_2) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) - - def test_anvil_audit_one_group_member_missing_in_app(self): - """Groups that the app is a member of are not reported in the app.""" - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=api_factories.GetGroupsResponseFactory( - response=[api_factories.GroupDetailsMemberFactory(groupName="test-group")] - ).response, - ) - audit_results = audit.ManagedGroupAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - - def test_anvil_audit_one_group_admin_missing_in_app(self): - """anvil_audit works correctly if the service account is an admin of a group not in the app.""" - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=api_factories.GetGroupsResponseFactory( - response=[api_factories.GroupDetailsAdminFactory(groupName="test-group")] - ).response, - ) - audit_results = audit.ManagedGroupAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 1) - record_result = audit_results.get_not_in_app_results()[0] - self.assertEqual(record_result.record, "test-group") - - def test_anvil_audit_two_groups_admin_missing_in_app(self): - """anvil_audit works correctly if there are two groups in AnVIL that aren't in the app.""" - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=api_factories.GetGroupsResponseFactory( - response=[ - api_factories.GroupDetailsAdminFactory(groupName="test-group-1"), - api_factories.GroupDetailsAdminFactory(groupName="test-group-2"), - ] - ).response, - ) - audit_results = audit.ManagedGroupAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 2) - record_result = audit_results.get_not_in_app_results()[0] - self.assertEqual(record_result.record, "test-group-1") - record_result = audit_results.get_not_in_app_results()[1] - self.assertEqual(record_result.record, "test-group-2") - - def test_fails_membership_audit(self): - """Error is reported when a group fails the membership audit.""" - group = factories.ManagedGroupFactory.create() - factories.GroupAccountMembershipFactory.create(group=group) - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=api_factories.GetGroupsResponseFactory( - response=[api_factories.GroupDetailsAdminFactory(groupName=group.name)] - ).response, - ) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(group) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_GROUP_MEMBERSHIP])) - - def test_admin_in_app_both_member_and_admin_on_anvil(self): - """anvil_audit works correctly when the app is an admin and AnVIL returns both a member and admin record.""" - group = factories.ManagedGroupFactory.create(is_managed_by_app=True) - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=api_factories.GetGroupsResponseFactory( - response=[ - api_factories.GroupDetailsAdminFactory(groupName=group.name), - api_factories.GroupDetailsMemberFactory(groupName=group.name), - ] - ).response, - ) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(group) - self.assertTrue(record_result.ok()) - - def test_admin_in_app_both_member_and_admin_different_order_on_anvil(self): - """anvil_audit works correctly when the app is an admin and AnVIL returns both a member and admin record.""" - group = factories.ManagedGroupFactory.create(is_managed_by_app=True) - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=api_factories.GetGroupsResponseFactory( - response=[ - api_factories.GroupDetailsMemberFactory(groupName=group.name), - api_factories.GroupDetailsAdminFactory(groupName=group.name), - ] - ).response, - ) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(group) - self.assertTrue(record_result.ok()) - - def test_member_in_app_both_member_and_admin_on_anvil(self): - """anvil_audit works correctly when the app is a member and AnVIL returns both a member and admin record.""" - group = factories.ManagedGroupFactory.create(is_managed_by_app=False) - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=api_factories.GetGroupsResponseFactory( - response=[ - api_factories.GroupDetailsMemberFactory(groupName=group.name), - api_factories.GroupDetailsAdminFactory(groupName=group.name), - ] - ).response, - ) - audit_results = audit.ManagedGroupAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(group) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_ROLE])) - - def test_member_in_app_both_member_and_admin_different_order_on_anvil(self): - """anvil_audit works correctly when the app is a member and AnVIL returns both a member and admin record.""" - group = factories.ManagedGroupFactory.create(is_managed_by_app=False) - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=api_factories.GetGroupsResponseFactory( - response=[ - api_factories.GroupDetailsAdminFactory(groupName=group.name), - api_factories.GroupDetailsMemberFactory(groupName=group.name), - ] - ).response, - ) - audit_results = audit.ManagedGroupAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(group) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_ROLE])) - - -class ManagedGroupMembershipAuditTest(AnVILAPIMockTestMixin, TestCase): - """Tests forthe ManagedGroupMembershipAudit class.""" - - def get_api_url_members(self, group_name): - """Return the API url being called by the method.""" - return self.api_client.sam_entry_point + "/api/groups/v1/" + group_name + "/member" - - def get_api_url_admins(self, group_name): - """Return the API url being called by the method.""" - return self.api_client.sam_entry_point + "/api/groups/v1/" + group_name + "/admin" - - def test_group_not_managed_by_app(self): - group = factories.ManagedGroupFactory.create(is_managed_by_app=False) - with self.assertRaises(exceptions.AnVILNotGroupAdminError): - audit.ManagedGroupMembershipAudit(group) - - def test_no_members(self): - """audit works correctly if this group has no members.""" - group = factories.ManagedGroupFactory.create() - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - - def test_one_account_members(self): - """audit works correctly if this group has one account member.""" - group = factories.ManagedGroupFactory.create() - membership = factories.GroupAccountMembershipFactory.create(group=group) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory(response=[membership.account.email]).response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - model_result = audit_results.get_result_for_model_instance(membership) - self.assertIsInstance(model_result, audit.ModelInstanceResult) - self.assertTrue(model_result.ok()) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - - def test_two_account_members(self): - """audit works correctly if this group has two account members.""" - group = factories.ManagedGroupFactory.create() - membership_1 = factories.GroupAccountMembershipFactory.create(group=group) - membership_2 = factories.GroupAccountMembershipFactory.create(group=group) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory( - response=[membership_1.account.email, membership_2.account.email] - ).response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 2) - model_result = audit_results.get_result_for_model_instance(membership_1) - self.assertIsInstance(model_result, audit.ModelInstanceResult) - self.assertTrue(model_result.ok()) - model_result = audit_results.get_result_for_model_instance(membership_2) - self.assertIsInstance(model_result, audit.ModelInstanceResult) - self.assertTrue(model_result.ok()) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - - def test_one_account_members_not_in_anvil(self): - """audit works correctly if this group has one account member not in anvil.""" - group = factories.ManagedGroupFactory.create() - membership = factories.GroupAccountMembershipFactory.create(group=group) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(membership) - self.assertFalse(model_result.ok()) - self.assertEqual(model_result.errors, set([audit_results.ERROR_ACCOUNT_MEMBER_NOT_IN_ANVIL])) - - def test_two_account_members_not_in_anvil(self): - """anvil_audit works correctly if this group has two account member not in anvil.""" - group = factories.ManagedGroupFactory.create() - membership_1 = factories.GroupAccountMembershipFactory.create(group=group) - membership_2 = factories.GroupAccountMembershipFactory.create(group=group) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 2) - model_result = audit_results.get_result_for_model_instance(membership_1) - self.assertIsInstance(model_result, audit.ModelInstanceResult) - self.assertFalse(model_result.ok()) - self.assertEqual(model_result.errors, set([audit_results.ERROR_ACCOUNT_MEMBER_NOT_IN_ANVIL])) - model_result = audit_results.get_result_for_model_instance(membership_2) - self.assertIsInstance(model_result, audit.ModelInstanceResult) - self.assertFalse(model_result.ok()) - self.assertEqual(model_result.errors, set([audit_results.ERROR_ACCOUNT_MEMBER_NOT_IN_ANVIL])) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - - def test_one_account_members_not_in_app(self): - """anvil_audit works correctly if this group has one account member not in the app.""" - group = factories.ManagedGroupFactory.create() - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory(response=["test-member@example.com"]).response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 1) - # Check individual records. - record_result = audit_results.get_not_in_app_results()[0] - self.assertIsInstance(record_result, audit.NotInAppResult) - self.assertEqual(record_result.record, "MEMBER: test-member@example.com") - - def test_two_account_members_not_in_app(self): - """anvil_audit works correctly if this group has two account member not in the app.""" - group = factories.ManagedGroupFactory.create() - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory( - response=["test-member-1@example.com", "test-member-2@example.com"] - ).response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 2) - # Check individual records. - record_result = audit_results.get_not_in_app_results()[0] - self.assertIsInstance(record_result, audit.NotInAppResult) - self.assertEqual(record_result.record, "MEMBER: test-member-1@example.com") - record_result = audit_results.get_not_in_app_results()[1] - self.assertIsInstance(record_result, audit.NotInAppResult) - self.assertEqual(record_result.record, "MEMBER: test-member-2@example.com") - - def test_one_account_members_case_insensitive(self): - """anvil_audit works correctly if this group has one account member not in the app.""" - group = factories.ManagedGroupFactory.create() - membership = factories.GroupAccountMembershipFactory.create( - group=group, account__email="tEsT-mEmBeR@example.com" - ) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory(response=["Test-Member@example.com"]).response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(membership) - self.assertTrue(record_result.ok()) - - def test_one_account_admin(self): - """anvil_audit works correctly if this group has one account admin.""" - group = factories.ManagedGroupFactory.create() - membership = factories.GroupAccountMembershipFactory.create( - group=group, role=models.GroupAccountMembership.ADMIN - ) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory(response=[membership.account.email]).response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(membership) - self.assertTrue(record_result.ok()) - - def test_two_account_admin(self): - """anvil_audit works correctly if this group has two account members.""" - group = factories.ManagedGroupFactory.create() - membership_1 = factories.GroupAccountMembershipFactory.create( - group=group, role=models.GroupAccountMembership.ADMIN - ) - membership_2 = factories.GroupAccountMembershipFactory.create( - group=group, role=models.GroupAccountMembership.ADMIN - ) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory( - response=[membership_1.account.email, membership_2.account.email] - ).response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 2) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(membership_1) - self.assertTrue(record_result.ok()) - record_result = audit_results.get_result_for_model_instance(membership_2) - self.assertTrue(record_result.ok()) - - def test_one_account_admin_not_in_anvil(self): - """anvil_audit works correctly if this group has one account member not in anvil.""" - group = factories.ManagedGroupFactory.create() - membership = factories.GroupAccountMembershipFactory.create( - group=group, role=models.GroupAccountMembership.ADMIN - ) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(membership) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_ACCOUNT_ADMIN_NOT_IN_ANVIL])) - - def test_two_account_admins_not_in_anvil(self): - """anvil_audit works correctly if this group has two account member not in anvil.""" - group = factories.ManagedGroupFactory.create() - membership_1 = factories.GroupAccountMembershipFactory.create( - group=group, role=models.GroupAccountMembership.ADMIN - ) - membership_2 = factories.GroupAccountMembershipFactory.create( - group=group, role=models.GroupAccountMembership.ADMIN - ) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 2) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(membership_1) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_ACCOUNT_ADMIN_NOT_IN_ANVIL])) - record_result = audit_results.get_result_for_model_instance(membership_2) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_ACCOUNT_ADMIN_NOT_IN_ANVIL])) - - def test_one_account_admin_not_in_app(self): - """anvil_audit works correctly if this group has one account member not in the app.""" - group = factories.ManagedGroupFactory.create() - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory(response=["test-admin@example.com"]).response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 1) - record_result = audit_results.get_not_in_app_results()[0] - self.assertEqual(record_result.record, "ADMIN: test-admin@example.com") - - def test_two_account_admin_not_in_app(self): - """anvil_audit works correctly if this group has two account admin not in the app.""" - group = factories.ManagedGroupFactory.create() - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory( - response=["test-admin-1@example.com", "test-admin-2@example.com"] - ).response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 2) - record_result = audit_results.get_not_in_app_results()[0] - self.assertEqual(record_result.record, "ADMIN: test-admin-1@example.com") - record_result = audit_results.get_not_in_app_results()[1] - self.assertEqual(record_result.record, "ADMIN: test-admin-2@example.com") - - def test_one_account_admin_case_insensitive(self): - """anvil_audit works correctly if this group has one account member not in the app.""" - group = factories.ManagedGroupFactory.create() - membership = factories.GroupAccountMembershipFactory.create( - group=group, - account__email="tEsT-aDmIn@example.com", - role=models.GroupAccountMembership.ADMIN, - ) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory(response=["Test-Admin@example.com"]).response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(membership) - self.assertTrue(record_result.ok()) - - def test_account_different_role_member_in_app_admin_in_anvil(self): - """anvil_audit works correctly if an account has a different role in AnVIL.""" - group = factories.ManagedGroupFactory.create() - membership = factories.GroupAccountMembershipFactory.create( - group=group, role=models.GroupAccountMembership.MEMBER - ) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory(response=[membership.account.email]).response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 1) - record_result = audit_results.get_result_for_model_instance(membership) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_ACCOUNT_MEMBER_NOT_IN_ANVIL])) - record_result = audit_results.get_not_in_app_results()[0] - self.assertEqual(record_result.record, "ADMIN: " + membership.account.email) - - def test_one_group_members(self): - """anvil_audit works correctly if this group has one group member.""" - group = factories.ManagedGroupFactory.create() - membership = factories.GroupGroupMembershipFactory.create(parent_group=group) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory(response=[membership.child_group.email]).response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(membership) - self.assertTrue(record_result.ok()) - - def test_two_group_members(self): - """anvil_audit works correctly if this group has two account members.""" - group = factories.ManagedGroupFactory.create() - membership_1 = factories.GroupGroupMembershipFactory.create(parent_group=group) - membership_2 = factories.GroupGroupMembershipFactory.create(parent_group=group) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory( - response=[ - membership_1.child_group.email, - membership_2.child_group.email, - ] - ).response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 2) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(membership_1) - self.assertTrue(record_result.ok()) - record_result = audit_results.get_result_for_model_instance(membership_2) - self.assertTrue(record_result.ok()) - - def test_one_group_members_not_in_anvil(self): - """anvil_audit works correctly if this group has one group member not in anvil.""" - group = factories.ManagedGroupFactory.create() - membership = factories.GroupGroupMembershipFactory.create(parent_group=group) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(membership) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_GROUP_MEMBER_NOT_IN_ANVIL])) - - def test_two_group_members_not_in_anvil(self): - """anvil_audit works correctly if this group has two group member not in anvil.""" - group = factories.ManagedGroupFactory.create() - membership_1 = factories.GroupGroupMembershipFactory.create(parent_group=group) - membership_2 = factories.GroupGroupMembershipFactory.create(parent_group=group) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 2) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(membership_1) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_GROUP_MEMBER_NOT_IN_ANVIL])) - record_result = audit_results.get_result_for_model_instance(membership_2) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_GROUP_MEMBER_NOT_IN_ANVIL])) - - def test_one_group_members_not_in_app(self): - """anvil_audit works correctly if this group has one group member not in the app.""" - group = factories.ManagedGroupFactory.create() - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory(response=["test-member@firecloud.org"]).response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 1) - record_result = audit_results.get_not_in_app_results()[0] - self.assertEqual(record_result.record, "MEMBER: test-member@firecloud.org") - - def test_two_group_members_not_in_app(self): - """anvil_audit works correctly if this group has two group member not in the app.""" - group = factories.ManagedGroupFactory.create() - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory( - response=["test-member-1@firecloud.org", "test-member-2@firecloud.org"] - ).response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 2) - record_result = audit_results.get_not_in_app_results()[0] - self.assertEqual(record_result.record, "MEMBER: test-member-1@firecloud.org") - record_result = audit_results.get_not_in_app_results()[1] - self.assertEqual(record_result.record, "MEMBER: test-member-2@firecloud.org") - - def test_one_group_members_case_insensitive(self): - """anvil_audit works correctly if this group has one group member not in the app.""" - group = factories.ManagedGroupFactory.create() - membership = factories.GroupGroupMembershipFactory.create(parent_group=group, child_group__name="tEsT-mEmBeR") - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory(response=["Test-Member@firecloud.org"]).response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(membership) - self.assertTrue(record_result.ok()) - - def test_one_group_admin(self): - """anvil_audit works correctly if this group has one group admin.""" - group = factories.ManagedGroupFactory.create() - membership = factories.GroupGroupMembershipFactory.create( - parent_group=group, role=models.GroupGroupMembership.ADMIN - ) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory(response=[membership.child_group.email]).response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(membership) - self.assertTrue(record_result.ok()) - - def test_two_group_admin(self): - """anvil_audit works correctly if this group has two group admin.""" - group = factories.ManagedGroupFactory.create() - membership_1 = factories.GroupGroupMembershipFactory.create( - parent_group=group, role=models.GroupGroupMembership.ADMIN - ) - membership_2 = factories.GroupGroupMembershipFactory.create( - parent_group=group, role=models.GroupGroupMembership.ADMIN - ) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory( - response=[ - membership_1.child_group.email, - membership_2.child_group.email, - ] - ).response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 2) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(membership_1) - self.assertTrue(record_result.ok()) - record_result = audit_results.get_result_for_model_instance(membership_2) - self.assertTrue(record_result.ok()) - - def test_one_group_admin_not_in_anvil(self): - """anvil_audit works correctly if this group has one group member not in anvil.""" - group = factories.ManagedGroupFactory.create() - membership = factories.GroupGroupMembershipFactory.create( - parent_group=group, role=models.GroupGroupMembership.ADMIN - ) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(membership) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_GROUP_ADMIN_NOT_IN_ANVIL])) - - def test_two_group_admins_not_in_anvil(self): - """anvil_audit works correctly if this group has two group member not in anvil.""" - group = factories.ManagedGroupFactory.create() - membership_1 = factories.GroupGroupMembershipFactory.create( - parent_group=group, role=models.GroupGroupMembership.ADMIN - ) - membership_2 = factories.GroupGroupMembershipFactory.create( - parent_group=group, role=models.GroupGroupMembership.ADMIN - ) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 2) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(membership_1) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_GROUP_ADMIN_NOT_IN_ANVIL])) - record_result = audit_results.get_result_for_model_instance(membership_2) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_GROUP_ADMIN_NOT_IN_ANVIL])) - - def test_one_group_admin_not_in_app(self): - """anvil_audit works correctly if this group has one group member not in the app.""" - group = factories.ManagedGroupFactory.create() - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory(response=["test-admin@firecloud.org"]).response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 1) - record_result = audit_results.get_not_in_app_results()[0] - self.assertEqual(record_result.record, "ADMIN: test-admin@firecloud.org") - - def test_two_group_admin_not_in_app(self): - """anvil_audit works correctly if this group has two group admin not in the app.""" - group = factories.ManagedGroupFactory.create() - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory( - response=["test-admin-1@firecloud.org", "test-admin-2@firecloud.org"] - ).response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 2) - record_result = audit_results.get_not_in_app_results()[0] - self.assertEqual(record_result.record, "ADMIN: test-admin-1@firecloud.org") - record_result = audit_results.get_not_in_app_results()[1] - self.assertEqual(record_result.record, "ADMIN: test-admin-2@firecloud.org") - - def test_one_group_admin_case_insensitive(self): - """anvil_audit works correctly if this group has one group member not in the app.""" - group = factories.ManagedGroupFactory.create() - membership = factories.GroupGroupMembershipFactory.create( - parent_group=group, - child_group__name="tEsT-aDmIn", - role=models.GroupGroupMembership.ADMIN, - ) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory(response=["Test-Admin@firecloud.org"]).response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(membership) - self.assertTrue(record_result.ok()) - - def test_group_different_role_member_in_app_admin_in_anvil(self): - """anvil_audit works correctly if an group has a different role in AnVIL.""" - group = factories.ManagedGroupFactory.create() - membership = factories.GroupGroupMembershipFactory.create( - parent_group=group, role=models.GroupGroupMembership.MEMBER - ) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory(response=[membership.child_group.email]).response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 1) - record_result = audit_results.get_result_for_model_instance(membership) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_GROUP_MEMBER_NOT_IN_ANVIL])) - record_result = audit_results.get_not_in_app_results()[0] - self.assertEqual(record_result.record, "ADMIN: " + membership.child_group.email) - - def test_service_account_is_both_admin_and_member(self): - """No errors are reported when the service account is both a member and an admin of a group.""" - group = factories.ManagedGroupFactory.create() - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory(response=[self.service_account_email]).response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - - def test_different_group_member_email(self): - """anvil_audit works correctly if this group has one group member with a different email.""" - group = factories.ManagedGroupFactory.create() - membership = factories.GroupGroupMembershipFactory.create(parent_group=group, child_group__email="foo@bar.com") - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory(response=[membership.child_group.email]).response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(membership) - self.assertTrue(record_result.ok()) - - def test_different_group_member_email_case_insensitive(self): - """anvil_audit works correctly if this group has one group member with a different email, case insensitive.""" - group = factories.ManagedGroupFactory.create() - membership = factories.GroupGroupMembershipFactory.create(parent_group=group, child_group__email="foo@bar.com") - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory(response=["Foo@Bar.com"]).response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(membership) - self.assertTrue(record_result.ok()) - - def test_service_account_is_not_directly_admin(self): - """Audit works when the service account is not directly an admin of a group (but is via a group admin).""" - group = factories.ManagedGroupFactory.create() - membership = factories.GroupGroupMembershipFactory.create( - parent_group=group, - child_group__email="foo@bar.com", - role=models.GroupGroupMembership.ADMIN, - ) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - # Use the Membership factory because it doesn't add the service account as a direct admin. - json=api_factories.GetGroupMembershipResponseFactory(response=["foo@bar.com"]).response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(membership) - self.assertTrue(record_result.ok()) - - def test_group_is_both_admin_and_member(self): - group = factories.ManagedGroupFactory.create() - membership = factories.GroupGroupMembershipFactory.create( - parent_group=group, role=models.GroupGroupMembership.ADMIN - ) - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory(response=[membership.child_group.email]).response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory(response=[membership.child_group.email]).response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(membership) - self.assertTrue(record_result.ok()) - - def test_deactivated_account_not_member_in_anvil(self): - """Audit fails if a deactivated account is not in the group on AnVIL.""" - group = factories.ManagedGroupFactory.create() - # Create an inactive account that is a member of this group. - membership = factories.GroupAccountMembershipFactory.create( - group=group, account__status=models.Account.INACTIVE_STATUS - ) - # The Account is not a member in AnVIL - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(membership) - self.assertFalse(model_result.ok()) - self.assertEqual( - model_result.errors, - set([audit_results.ERROR_ACCOUNT_MEMBER_NOT_IN_ANVIL, audit_results.ERROR_DEACTIVATED_ACCOUNT]), - ) - - def test_deactivated_account_member_in_anvil(self): - """Audit is not ok if a deactivated account is in the group on AnVIL.""" - group = factories.ManagedGroupFactory.create() - # Create an inactive account that is a member of this group. - membership = factories.GroupAccountMembershipFactory.create( - group=group, account__status=models.Account.INACTIVE_STATUS - ) - # The Account is not a member in AnVIL - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory(response=[membership.account.email]).response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(membership) - self.assertEqual( - record_result.errors, - set([audit_results.ERROR_DEACTIVATED_ACCOUNT]), - ) - - def test_deactivated_account_not_admin_in_anvil(self): - """Audit is not ok if a deactivated account is not in the group on AnVIL.""" - group = factories.ManagedGroupFactory.create() - # Create an inactive account that is a member of this group. - membership = factories.GroupAccountMembershipFactory.create( - group=group, - account__status=models.Account.INACTIVE_STATUS, - role=models.GroupAccountMembership.ADMIN, - ) - # The Account is not a member in AnVIL - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory().response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(membership) - self.assertFalse(model_result.ok()) - self.assertEqual( - model_result.errors, - set([audit_results.ERROR_ACCOUNT_ADMIN_NOT_IN_ANVIL, audit_results.ERROR_DEACTIVATED_ACCOUNT]), - ) - - def test_deactivated_account_admin_in_anvil(self): - """Audit is not ok if a deactivated account is in the group on AnVIL.""" - group = factories.ManagedGroupFactory.create() - # Create an inactive account that is a member of this group. - membership = factories.GroupAccountMembershipFactory.create( - group=group, - account__status=models.Account.INACTIVE_STATUS, - role=models.GroupAccountMembership.ADMIN, - ) - # The Account is not a member in AnVIL - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=api_factories.GetGroupMembershipResponseFactory().response, - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=api_factories.GetGroupMembershipAdminResponseFactory(response=[membership.account.email]).response, - ) - audit_results = audit.ManagedGroupMembershipAudit(group) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(membership) - self.assertEqual( - record_result.errors, - set([audit_results.ERROR_DEACTIVATED_ACCOUNT]), - ) - - -class WorkspaceAuditTest(AnVILAPIMockTestMixin, TestCase): - """Tests for the Workspace.anvil_audit method.""" - - def get_api_url(self): - return self.api_client.rawls_entry_point + "/api/workspaces" - - def get_api_workspace_json( - self, - billing_project_name, - workspace_name, - access, - auth_domains=[], - is_locked=False, - ): - """Return the json dictionary for a single workspace on AnVIL.""" - return { - "accessLevel": access, - "workspace": { - "name": workspace_name, - "namespace": billing_project_name, - "authorizationDomain": [{"membersGroupName": x} for x in auth_domains], - "isLocked": is_locked, - }, - } - - def get_api_workspace_acl_url(self, billing_project_name, workspace_name): - return ( - self.api_client.rawls_entry_point - + "/api/workspaces/" - + billing_project_name - + "/" - + workspace_name - + "/acl" - ) - - def get_api_workspace_acl_response(self): - """Return a json for the workspace/acl method where no one else can access.""" - return { - "acl": { - self.service_account_email: { - "accessLevel": "OWNER", - "canCompute": True, - "canShare": True, - "pending": False, - } - } - } - - def get_api_bucket_options_url(self, billing_project_name, workspace_name): - return self.api_client.rawls_entry_point + "/api/workspaces/" + billing_project_name + "/" + workspace_name - - def get_api_bucket_options_response(self): - """Return a json for the workspace/acl method that is not requester pays.""" - return {"bucketOptions": {"requesterPays": False}} - - def test_anvil_audit_no_workspaces(self): - """anvil_audit works correct if there are no Workspaces in the app.""" - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[], - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - - def test_anvil_audit_one_workspace_no_errors(self): - """anvil_audit works correct if there is one workspace in the app and it exists on AnVIL.""" - workspace = factories.WorkspaceFactory.create() - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[self.get_api_workspace_json(workspace.billing_project.name, workspace.name, "OWNER")], - ) - # Response to check workspace access. - workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(workspace) - self.assertTrue(record_result.ok()) - - def test_anvil_audit_one_workspace_not_on_anvil(self): - """anvil_audit raises exception if one group exists in the app but not on AnVIL.""" - workspace = factories.WorkspaceFactory.create() - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[], - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(workspace) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) - - def test_anvil_audit_one_workspace_owner_in_app_reader_on_anvil(self): - """anvil_audit raises exception if one workspace exists in the app but the access on AnVIL is READER.""" - workspace = factories.WorkspaceFactory.create() - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[self.get_api_workspace_json(workspace.billing_project.name, workspace.name, "READER")], - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(workspace) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_OWNER_ON_ANVIL])) - - def test_anvil_audit_one_workspace_owner_in_app_writer_on_anvil(self): - """anvil_audit raises exception if one workspace exists in the app but the access on AnVIL is WRITER.""" - workspace = factories.WorkspaceFactory.create() - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[self.get_api_workspace_json(workspace.billing_project.name, workspace.name, "WRITER")], - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(workspace) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_OWNER_ON_ANVIL])) - - def test_anvil_audit_one_workspace_is_locked_in_app_not_on_anvil(self): - """anvil_audit raises exception if workspace is locked in the app but not on AnVIL.""" - workspace = factories.WorkspaceFactory.create(is_locked=True) - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[ - self.get_api_workspace_json( - workspace.billing_project.name, - workspace.name, - "OWNER", - is_locked=False, - ) - ], - ) - # Response to check workspace access. - workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(workspace) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_LOCK])) - - def test_anvil_audit_one_workspace_is_not_locked_in_app_but_is_on_anvil(self): - """anvil_audit raises exception if workspace is locked in the app but not on AnVIL.""" - workspace = factories.WorkspaceFactory.create(is_locked=False) - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[ - self.get_api_workspace_json( - workspace.billing_project.name, - workspace.name, - "OWNER", - is_locked=True, - ) - ], - ) - # Response to check workspace access. - workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(workspace) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_LOCK])) - - def test_anvil_audit_one_workspace_is_requester_pays_in_app_not_on_anvil(self): - """anvil_audit raises exception if workspace is requester_pays in the app but not on AnVIL.""" - workspace = factories.WorkspaceFactory.create(is_requester_pays=True) - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[ - self.get_api_workspace_json( - workspace.billing_project.name, - workspace.name, - "OWNER", - is_locked=False, - ) - ], - ) - # Response to check workspace access. - workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(workspace) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_REQUESTER_PAYS])) - - def test_anvil_audit_one_workspace_is_not_requester_pays_in_app_but_is_on_anvil(self): - """anvil_audit raises exception if workspace is requester_pays in the app but not on AnVIL.""" - workspace = factories.WorkspaceFactory.create(is_requester_pays=False) - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[ - self.get_api_workspace_json( - workspace.billing_project.name, - workspace.name, - "OWNER", - is_locked=False, - ) - ], - ) - # Response to check workspace access. - workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) - response = self.get_api_bucket_options_response() - response["bucketOptions"]["requesterPays"] = True - self.anvil_response_mock.add(responses.GET, workspace_acl_url, status=200, json=response) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(workspace) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_REQUESTER_PAYS])) - - def test_anvil_audit_two_workspaces_no_errors(self): - """anvil_audit returns None if if two workspaces exist in both the app and AnVIL.""" - workspace_1 = factories.WorkspaceFactory.create() - workspace_2 = factories.WorkspaceFactory.create() - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[ - self.get_api_workspace_json(workspace_1.billing_project.name, workspace_1.name, "OWNER"), - self.get_api_workspace_json(workspace_2.billing_project.name, workspace_2.name, "OWNER"), - ], - ) - # Response to check workspace access. - workspace_acl_url_1 = self.get_api_workspace_acl_url(workspace_1.billing_project.name, workspace_1.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url_1, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace access. - workspace_acl_url_2 = self.get_api_workspace_acl_url(workspace_2.billing_project.name, workspace_2.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url_2, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace_1.billing_project.name, workspace_1.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace_2.billing_project.name, workspace_2.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 2) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(workspace_1) - self.assertTrue(record_result.ok()) - record_result = audit_results.get_result_for_model_instance(workspace_2) - self.assertTrue(record_result.ok()) - - def test_anvil_audit_two_groups_json_response_order_does_not_matter(self): - """Order of groups in the json response does not matter.""" - workspace_1 = factories.WorkspaceFactory.create() - workspace_2 = factories.WorkspaceFactory.create() - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[ - self.get_api_workspace_json(workspace_2.billing_project.name, workspace_2.name, "OWNER"), - self.get_api_workspace_json(workspace_1.billing_project.name, workspace_1.name, "OWNER"), - ], - ) - # Response to check workspace access. - workspace_acl_url_1 = self.get_api_workspace_acl_url(workspace_1.billing_project.name, workspace_1.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url_1, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace access. - workspace_acl_url_2 = self.get_api_workspace_acl_url(workspace_2.billing_project.name, workspace_2.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url_2, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace_1.billing_project.name, workspace_1.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace_2.billing_project.name, workspace_2.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 2) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(workspace_1) - self.assertTrue(record_result.ok()) - record_result = audit_results.get_result_for_model_instance(workspace_2) - self.assertTrue(record_result.ok()) - - def test_anvil_audit_two_workspaces_first_not_on_anvil(self): - """anvil_audit raises exception if two workspaces exist in the app but the first is not not on AnVIL.""" - workspace_1 = factories.WorkspaceFactory.create() - workspace_2 = factories.WorkspaceFactory.create() - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[ - self.get_api_workspace_json(workspace_2.billing_project.name, workspace_2.name, "OWNER"), - ], - ) - # Response to check workspace access. - workspace_acl_url_2 = self.get_api_workspace_acl_url(workspace_2.billing_project.name, workspace_2.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url_2, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace_2.billing_project.name, workspace_2.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(workspace_1) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) - record_result = audit_results.get_result_for_model_instance(workspace_2) - self.assertTrue(record_result.ok()) - - def test_anvil_audit_two_workspaces_first_different_access(self): - """anvil_audit when if two workspaces exist in the app but access to the first is different on AnVIL.""" - workspace_1 = factories.WorkspaceFactory.create() - workspace_2 = factories.WorkspaceFactory.create() - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[ - self.get_api_workspace_json(workspace_1.billing_project.name, workspace_1.name, "READER"), - self.get_api_workspace_json(workspace_2.billing_project.name, workspace_2.name, "OWNER"), - ], - ) - # Response to check workspace access. - workspace_acl_url_2 = self.get_api_workspace_acl_url(workspace_2.billing_project.name, workspace_2.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url_2, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace_1.billing_project.name, workspace_1.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace_2.billing_project.name, workspace_2.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(workspace_1) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_OWNER_ON_ANVIL])) - record_result = audit_results.get_result_for_model_instance(workspace_2) - self.assertTrue(record_result.ok()) - - def test_anvil_audit_two_workspaces_both_missing_in_anvil(self): - """anvil_audit when there are two workspaces that exist in the app but not in AnVIL.""" - workspace_1 = factories.WorkspaceFactory.create() - workspace_2 = factories.WorkspaceFactory.create() - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[], - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 2) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(workspace_1) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) - record_result = audit_results.get_result_for_model_instance(workspace_2) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) - - def test_anvil_audit_one_workspace_missing_in_app(self): - """anvil_audit returns not_in_app info if a workspace exists on AnVIL but not in the app.""" - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[self.get_api_workspace_json("test-bp", "test-ws", "OWNER")], - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 1) - record_result = audit_results.get_not_in_app_results()[0] - self.assertEqual(record_result.record, "test-bp/test-ws") - - def test_anvil_audit_two_workspaces_missing_in_app(self): - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[ - self.get_api_workspace_json("test-bp-1", "test-ws-1", "OWNER"), - self.get_api_workspace_json("test-bp-2", "test-ws-2", "OWNER"), - ], - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 2) - record_result = audit_results.get_not_in_app_results()[0] - self.assertEqual(record_result.record, "test-bp-1/test-ws-1") - record_result = audit_results.get_not_in_app_results()[1] - self.assertEqual(record_result.record, "test-bp-2/test-ws-2") - - def test_different_billing_project(self): - """A workspace is reported as missing if it has the same name but a different billing project in app.""" - workspace = factories.WorkspaceFactory.create(billing_project__name="test-bp-app", name="test-ws") - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[self.get_api_workspace_json("test-bp-anvil", "test-ws", "OWNER")], - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 1) - record_result = audit_results.get_result_for_model_instance(workspace) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_NOT_IN_ANVIL])) - record_result = audit_results.get_not_in_app_results()[0] - self.assertEqual(record_result.record, "test-bp-anvil/test-ws") - - def test_ignores_workspaces_where_app_is_reader_on_anvil(self): - """Audit ignores workspaces on AnVIL where app is a READER on AnVIL.""" - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[self.get_api_workspace_json("test-bp", "test-ws", "READER")], - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - - def test_ignores_workspaces_where_app_is_writer_on_anvil(self): - """Audit ignores workspaces on AnVIL where app is a WRITER on AnVIL.""" - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[self.get_api_workspace_json("test-bp", "test-ws", "WRITER")], - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - - def test_one_workspace_one_auth_domain(self): - """anvil_audit works properly when there is one workspace with one auth domain.""" - auth_domain = factories.WorkspaceAuthorizationDomainFactory.create() - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[ - self.get_api_workspace_json( - auth_domain.workspace.billing_project.name, - auth_domain.workspace.name, - "OWNER", - auth_domains=[auth_domain.group.name], - ) - ], - ) - # Response to check workspace access. - workspace_acl_url = self.get_api_workspace_acl_url( - auth_domain.workspace.billing_project.name, auth_domain.workspace.name - ) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url( - auth_domain.workspace.billing_project.name, auth_domain.workspace.name - ) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(auth_domain.workspace) - self.assertTrue(record_result.ok()) - - def test_one_workspace_two_auth_domains(self): - """anvil_audit works properly when there is one workspace with two auth domains.""" - workspace = factories.WorkspaceFactory.create() - auth_domain_1 = factories.WorkspaceAuthorizationDomainFactory.create(workspace=workspace) - auth_domain_2 = factories.WorkspaceAuthorizationDomainFactory.create(workspace=workspace) - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[ - self.get_api_workspace_json( - workspace.billing_project.name, - workspace.name, - "OWNER", - auth_domains=[auth_domain_1.group.name, auth_domain_2.group.name], - ) - ], - ) - # Response to check workspace access. - workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(workspace) - self.assertTrue(record_result.ok()) - - def test_one_workspace_two_auth_domains_order_does_not_matter(self): - """anvil_audit works properly when there is one workspace with two auth domains.""" - workspace = factories.WorkspaceFactory.create() - auth_domain_1 = factories.WorkspaceAuthorizationDomainFactory.create(workspace=workspace, group__name="aa") - auth_domain_2 = factories.WorkspaceAuthorizationDomainFactory.create(workspace=workspace, group__name="zz") - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[ - self.get_api_workspace_json( - workspace.billing_project.name, - workspace.name, - "OWNER", - auth_domains=[auth_domain_2.group.name, auth_domain_1.group.name], - ) - ], - ) - # Response to check workspace access. - workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(workspace) - self.assertTrue(record_result.ok()) - - def test_one_workspace_no_auth_domain_in_app_one_auth_domain_on_anvil(self): - """anvil_audit works properly when there is one workspace with no auth domain in the app but one on AnVIL.""" - workspace = factories.WorkspaceFactory.create() - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[ - self.get_api_workspace_json( - workspace.billing_project.name, - workspace.name, - "OWNER", - auth_domains=["auth-anvil"], - ) - ], - ) - # Response to check workspace access. - workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(workspace) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_AUTH_DOMAINS])) - - def test_one_workspace_one_auth_domain_in_app_no_auth_domain_on_anvil(self): - """anvil_audit works properly when there is one workspace with one auth domain in the app but none on AnVIL.""" - auth_domain = factories.WorkspaceAuthorizationDomainFactory.create() - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[ - self.get_api_workspace_json( - auth_domain.workspace.billing_project.name, - auth_domain.workspace.name, - "OWNER", - auth_domains=[], - ) - ], - ) - # Response to check workspace access. - workspace_acl_url = self.get_api_workspace_acl_url( - auth_domain.workspace.billing_project.name, auth_domain.workspace.name - ) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url( - auth_domain.workspace.billing_project.name, auth_domain.workspace.name - ) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(auth_domain.workspace) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_AUTH_DOMAINS])) - - def test_one_workspace_no_auth_domain_in_app_two_auth_domains_on_anvil(self): - """anvil_audit works properly when there is one workspace with no auth domain in the app but two on AnVIL.""" - workspace = factories.WorkspaceFactory.create() - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[ - self.get_api_workspace_json( - workspace.billing_project.name, - workspace.name, - "OWNER", - auth_domains=["auth-domain"], - ) - ], - ) - # Response to check workspace access. - workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(workspace) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_AUTH_DOMAINS])) - - def test_one_workspace_two_auth_domains_in_app_no_auth_domain_on_anvil(self): - """anvil_audit works properly when there is one workspace with two auth domains in the app but none on AnVIL.""" - workspace = factories.WorkspaceFactory.create() - factories.WorkspaceAuthorizationDomainFactory.create(workspace=workspace) - factories.WorkspaceAuthorizationDomainFactory.create(workspace=workspace) - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[ - self.get_api_workspace_json( - workspace.billing_project.name, - workspace.name, - "OWNER", - auth_domains=[], - ) - ], - ) - # Response to check workspace access. - workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(workspace) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_AUTH_DOMAINS])) - - def test_one_workspace_two_auth_domains_in_app_one_auth_domain_on_anvil(self): - """anvil_audit works properly when there is one workspace with two auth domains in the app but one on AnVIL.""" - workspace = factories.WorkspaceFactory.create() - auth_domain_1 = factories.WorkspaceAuthorizationDomainFactory.create(workspace=workspace) - factories.WorkspaceAuthorizationDomainFactory.create(workspace=workspace) - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[ - self.get_api_workspace_json( - workspace.billing_project.name, - workspace.name, - "OWNER", - auth_domains=[auth_domain_1.group.name], - ) - ], - ) - # Response to check workspace access. - workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(workspace) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_AUTH_DOMAINS])) - - def test_one_workspace_different_auth_domains(self): - """anvil_audit works properly when the app and AnVIL have different auth domains for the same workspace.""" - auth_domain = factories.WorkspaceAuthorizationDomainFactory.create(group__name="app") - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[ - self.get_api_workspace_json( - auth_domain.workspace.billing_project.name, - auth_domain.workspace.name, - "OWNER", - auth_domains=["anvil"], - ) - ], - ) - # Response to check workspace access. - workspace_acl_url = self.get_api_workspace_acl_url( - auth_domain.workspace.billing_project.name, auth_domain.workspace.name - ) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url( - auth_domain.workspace.billing_project.name, auth_domain.workspace.name - ) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(auth_domain.workspace) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_AUTH_DOMAINS])) - - def test_two_workspaces_first_auth_domains_do_not_match(self): - """anvil_audit works properly when there are two workspaces in the app and the first has auth domain issues.""" - workspace_1 = factories.WorkspaceFactory.create() - workspace_2 = factories.WorkspaceFactory.create() - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[ - self.get_api_workspace_json( - workspace_1.billing_project.name, - workspace_1.name, - "OWNER", - auth_domains=["anvil"], - ), - self.get_api_workspace_json( - workspace_2.billing_project.name, - workspace_2.name, - "OWNER", - auth_domains=[], - ), - ], - ) - # Response to check workspace access. - workspace_acl_url_1 = self.get_api_workspace_acl_url(workspace_1.billing_project.name, workspace_1.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url_1, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace access. - workspace_acl_url_2 = self.get_api_workspace_acl_url(workspace_2.billing_project.name, workspace_2.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url_2, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace_1.billing_project.name, workspace_1.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace_2.billing_project.name, workspace_2.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(workspace_1) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_AUTH_DOMAINS])) - record_result = audit_results.get_result_for_model_instance(workspace_2) - self.assertTrue(record_result.ok()) - - def test_two_workspaces_auth_domains_do_not_match_for_both(self): - """anvil_audit works properly when there are two workspaces in the app and both have auth domain issues.""" - workspace_1 = factories.WorkspaceFactory.create() - workspace_2 = factories.WorkspaceFactory.create() - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[ - self.get_api_workspace_json( - workspace_1.billing_project.name, - workspace_1.name, - "OWNER", - auth_domains=["anvil-1"], - ), - self.get_api_workspace_json( - workspace_2.billing_project.name, - workspace_2.name, - "OWNER", - auth_domains=["anvil-2"], - ), - ], - ) - # Response to check workspace access. - workspace_acl_url_1 = self.get_api_workspace_acl_url(workspace_1.billing_project.name, workspace_1.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url_1, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace access. - workspace_acl_url_2 = self.get_api_workspace_acl_url(workspace_2.billing_project.name, workspace_2.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url_2, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace_1.billing_project.name, workspace_1.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace_2.billing_project.name, workspace_2.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 2) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(workspace_1) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_AUTH_DOMAINS])) - record_result = audit_results.get_result_for_model_instance(workspace_2) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_DIFFERENT_AUTH_DOMAINS])) - - def test_one_workspace_with_two_errors(self): - """One workspace has two errors: different auth domains and not owner.""" - workspace = factories.WorkspaceFactory.create() - factories.WorkspaceAuthorizationDomainFactory.create(workspace=workspace) - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[self.get_api_workspace_json(workspace.billing_project.name, workspace.name, "READER")], - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(workspace) - self.assertFalse(record_result.ok()) - self.assertEqual( - record_result.errors, - set( - [ - audit_results.ERROR_NOT_OWNER_ON_ANVIL, - audit_results.ERROR_DIFFERENT_AUTH_DOMAINS, - ] - ), - ) - - def test_fails_sharing_audit(self): - """anvil_audit works properly when one workspace fails its sharing audit.""" - workspace = factories.WorkspaceFactory.create() - factories.WorkspaceGroupSharingFactory.create(workspace=workspace) - # Response for the main call about workspaces. - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[ - self.get_api_workspace_json( - workspace.billing_project.name, - workspace.name, - "OWNER", - ) - ], - ) - # Response to check workspace access. - workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - audit_results = audit.WorkspaceAudit() - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - record_result = audit_results.get_result_for_model_instance(workspace) - self.assertFalse(record_result.ok()) - self.assertEqual(record_result.errors, set([audit_results.ERROR_WORKSPACE_SHARING])) - - -class WorkspaceSharingAuditTest(AnVILAPIMockTestMixin, TestCase): - """Tests for the WorkspaceSharingAudit class.""" - - def setUp(self): - super().setUp() - # Set this variable here because it will include the service account. - # Tests can update it with the update_api_response method. - self.api_response = {"acl": {}} - # Create a workspace for use in tests. - self.workspace = factories.WorkspaceFactory.create() - self.api_url = ( - self.api_client.rawls_entry_point - + "/api/workspaces/" - + self.workspace.billing_project.name - + "/" - + self.workspace.name - + "/acl" - ) - - def update_api_response(self, email, access, can_compute=False, can_share=False): - """Return a paired down json for a single ACL, including the service account.""" - self.api_response["acl"].update( - { - email: { - "accessLevel": access, - "canCompute": can_compute, - "canShare": can_share, - "pending": False, - } - } - ) - - def test_no_access(self): - """anvil_audit works correctly if this workspace is not shared with any groups.""" - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - - def test_one_group_reader(self): - """anvil_audit works correctly if this group has one group member.""" - access = factories.WorkspaceGroupSharingFactory.create(workspace=self.workspace) - self.update_api_response(access.group.email, access.access) - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(access) - self.assertTrue(model_result.ok()) - - def test_two_group_readers(self): - """anvil_audit works correctly if this workspace has two group readers.""" - access_1 = factories.WorkspaceGroupSharingFactory.create(workspace=self.workspace) - access_2 = factories.WorkspaceGroupSharingFactory.create(workspace=self.workspace) - self.update_api_response(access_1.group.email, "READER") - self.update_api_response(access_2.group.email, "READER") - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 2) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(access_1) - self.assertTrue(model_result.ok()) - model_result = audit_results.get_result_for_model_instance(access_2) - self.assertTrue(model_result.ok()) - - def test_one_group_reader_not_in_anvil(self): - """anvil_audit works correctly if this workspace has one group reader not in anvil.""" - access = factories.WorkspaceGroupSharingFactory.create(workspace=self.workspace) - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(access) - self.assertFalse(model_result.ok()) - self.assertEqual(model_result.errors, set([audit_results.ERROR_NOT_SHARED_IN_ANVIL])) - - def test_two_group_readers_not_in_anvil(self): - """anvil_audit works correctly if this workspace has two group readers not in anvil.""" - access_1 = factories.WorkspaceGroupSharingFactory.create(workspace=self.workspace) - access_2 = factories.WorkspaceGroupSharingFactory.create(workspace=self.workspace) - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 2) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(access_1) - self.assertFalse(model_result.ok()) - self.assertEqual(model_result.errors, set([audit_results.ERROR_NOT_SHARED_IN_ANVIL])) - model_result = audit_results.get_result_for_model_instance(access_2) - self.assertFalse(model_result.ok()) - self.assertEqual(model_result.errors, set([audit_results.ERROR_NOT_SHARED_IN_ANVIL])) - - def test_one_group_readers_not_in_app(self): - """anvil_audit works correctly if this workspace has one group reader not in the app.""" - self.update_api_response("test-member@firecloud.org", "READER") - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 1) - model_result = audit_results.get_not_in_app_results()[0] - self.assertEqual(model_result.record, "READER: test-member@firecloud.org") - - def test_two_group_readers_not_in_app(self): - """anvil_audit works correctly if this workspace has two group readers not in the app.""" - self.update_api_response("test-member-1@firecloud.org", "READER") - self.update_api_response("test-member-2@firecloud.org", "READER") - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 2) - model_result = audit_results.get_not_in_app_results()[0] - self.assertEqual(model_result.record, "READER: test-member-1@firecloud.org") - model_result = audit_results.get_not_in_app_results()[1] - self.assertEqual(model_result.record, "READER: test-member-2@firecloud.org") - - def test_one_group_members_case_insensitive(self): - """anvil_audit ignores case.""" - access = factories.WorkspaceGroupSharingFactory.create(workspace=self.workspace, group__name="tEsT-mEmBeR") - self.update_api_response("Test-Member@firecloud.org", "READER") - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(access) - self.assertTrue(model_result.ok()) - - def test_one_group_writer(self): - """anvil_audit works correctly if this workspace has one group writer.""" - access = factories.WorkspaceGroupSharingFactory.create( - workspace=self.workspace, access=models.WorkspaceGroupSharing.WRITER - ) - self.update_api_response(access.group.email, "WRITER") - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(access) - self.assertTrue(model_result.ok()) - - def test_two_group_writers(self): - """anvil_audit works correctly if this workspace has two group writers.""" - access_1 = factories.WorkspaceGroupSharingFactory.create( - workspace=self.workspace, access=models.WorkspaceGroupSharing.WRITER - ) - access_2 = factories.WorkspaceGroupSharingFactory.create( - workspace=self.workspace, access=models.WorkspaceGroupSharing.WRITER - ) - self.update_api_response(access_1.group.email, "WRITER") - self.update_api_response(access_2.group.email, "WRITER") - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 2) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(access_1) - self.assertTrue(model_result.ok()) - model_result = audit_results.get_result_for_model_instance(access_2) - self.assertTrue(model_result.ok()) - - def test_one_group_writer_not_in_anvil(self): - """anvil_audit works correctly if this workspace has one group writer not in anvil.""" - access = factories.WorkspaceGroupSharingFactory.create( - workspace=self.workspace, access=models.WorkspaceGroupSharing.WRITER - ) - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(access) - self.assertFalse(model_result.ok()) - self.assertEqual(model_result.errors, set([audit_results.ERROR_NOT_SHARED_IN_ANVIL])) - - def test_two_group_writers_not_in_anvil(self): - """anvil_audit works correctly if this workspace has two group writers not in anvil.""" - access_1 = factories.WorkspaceGroupSharingFactory.create( - workspace=self.workspace, access=models.WorkspaceGroupSharing.WRITER - ) - access_2 = factories.WorkspaceGroupSharingFactory.create( - workspace=self.workspace, access=models.WorkspaceGroupSharing.WRITER - ) - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 2) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(access_1) - self.assertFalse(model_result.ok()) - self.assertEqual(model_result.errors, set([audit_results.ERROR_NOT_SHARED_IN_ANVIL])) - model_result = audit_results.get_result_for_model_instance(access_2) - self.assertFalse(model_result.ok()) - self.assertEqual(model_result.errors, set([audit_results.ERROR_NOT_SHARED_IN_ANVIL])) - - def test_one_group_writer_not_in_app(self): - """anvil_audit works correctly if this workspace has one group writer not in the app.""" - self.update_api_response("test-writer@firecloud.org", "WRITER") - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 1) - model_result = audit_results.get_not_in_app_results()[0] - self.assertEqual(model_result.record, "WRITER: test-writer@firecloud.org") - - def test_two_group_writers_not_in_app(self): - """anvil_audit works correctly if this workspace has two group writers not in the app.""" - self.update_api_response("test-writer-1@firecloud.org", "WRITER") - self.update_api_response("test-writer-2@firecloud.org", "WRITER") - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 2) - model_result = audit_results.get_not_in_app_results()[0] - self.assertEqual(model_result.record, "WRITER: test-writer-1@firecloud.org") - model_result = audit_results.get_not_in_app_results()[1] - self.assertEqual(model_result.record, "WRITER: test-writer-2@firecloud.org") - - def test_one_group_admin_case_insensitive(self): - """anvil_audit works correctly if this workspace has one group member not in the app.""" - access = factories.WorkspaceGroupSharingFactory.create( - workspace=self.workspace, - group__name="tEsT-wRiTeR", - access=models.WorkspaceGroupSharing.WRITER, - ) - self.update_api_response("Test-Writer@firecloud.org", "WRITER") - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(access) - self.assertTrue(model_result.ok()) - - def test_one_group_owner(self): - """anvil_audit works correctly if this workspace has one group owner.""" - access = factories.WorkspaceGroupSharingFactory.create( - workspace=self.workspace, access=models.WorkspaceGroupSharing.OWNER - ) - self.update_api_response(access.group.email, "OWNER", can_share=True) - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(access) - self.assertTrue(model_result.ok()) - - def test_two_group_owners(self): - """anvil_audit works correctly if this workspace has two group owners.""" - access_1 = factories.WorkspaceGroupSharingFactory.create( - workspace=self.workspace, access=models.WorkspaceGroupSharing.OWNER - ) - access_2 = factories.WorkspaceGroupSharingFactory.create( - workspace=self.workspace, access=models.WorkspaceGroupSharing.OWNER - ) - self.update_api_response(access_1.group.email, "OWNER", can_share=True) - self.update_api_response(access_2.group.email, "OWNER", can_share=True) - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 2) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(access_1) - self.assertTrue(model_result.ok()) - model_result = audit_results.get_result_for_model_instance(access_2) - self.assertTrue(model_result.ok()) - - def test_one_group_owner_not_in_anvil(self): - """anvil_audit works correctly if this workspace has one group owners not in anvil.""" - access = factories.WorkspaceGroupSharingFactory.create( - workspace=self.workspace, access=models.WorkspaceGroupSharing.OWNER - ) - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(access) - self.assertFalse(model_result.ok()) - self.assertEqual(model_result.errors, set([audit_results.ERROR_NOT_SHARED_IN_ANVIL])) - - def test_two_group_owners_not_in_anvil(self): - """anvil_audit works correctly if this workspace has two group owners not in anvil.""" - access_1 = factories.WorkspaceGroupSharingFactory.create( - workspace=self.workspace, access=models.WorkspaceGroupSharing.OWNER - ) - access_2 = factories.WorkspaceGroupSharingFactory.create( - workspace=self.workspace, access=models.WorkspaceGroupSharing.OWNER - ) - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 2) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(access_1) - self.assertFalse(model_result.ok()) - self.assertEqual(model_result.errors, set([audit_results.ERROR_NOT_SHARED_IN_ANVIL])) - model_result = audit_results.get_result_for_model_instance(access_2) - self.assertFalse(model_result.ok()) - self.assertEqual(model_result.errors, set([audit_results.ERROR_NOT_SHARED_IN_ANVIL])) - - def test_one_group_owner_not_in_app(self): - """anvil_audit works correctly if this workspace has one group owner not in the app.""" - self.update_api_response("test-writer@firecloud.org", "OWNER") - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 1) - model_result = audit_results.get_not_in_app_results()[0] - self.assertEqual(model_result.record, "OWNER: test-writer@firecloud.org") - - def test_two_group_owners_not_in_app(self): - """anvil_audit works correctly if this workspace has two group owners not in the app.""" - self.update_api_response("test-writer-1@firecloud.org", "OWNER") - self.update_api_response("test-writer-2@firecloud.org", "OWNER") - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 2) - model_result = audit_results.get_not_in_app_results()[0] - self.assertEqual(model_result.record, "OWNER: test-writer-1@firecloud.org") - model_result = audit_results.get_not_in_app_results()[1] - self.assertEqual(model_result.record, "OWNER: test-writer-2@firecloud.org") - - def test_one_group_owner_case_insensitive(self): - """anvil_audit works correctly with different cases for owner emails.""" - access = factories.WorkspaceGroupSharingFactory.create( - workspace=self.workspace, - group__name="tEsT-oWnEr", - access=models.WorkspaceGroupSharing.OWNER, - ) - self.update_api_response("Test-Owner@firecloud.org", "OWNER", can_share=True) - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(access) - self.assertTrue(model_result.ok()) - - def test_group_different_access_reader_in_app_writer_in_anvil(self): - """anvil_audit works correctly if a group has different access to a workspace in AnVIL.""" - access = factories.WorkspaceGroupSharingFactory.create( - workspace=self.workspace, access=models.WorkspaceGroupSharing.READER - ) - self.update_api_response(access.group.email, "WRITER") - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(access) - self.assertFalse(model_result.ok()) - self.assertEqual(model_result.errors, set([audit_results.ERROR_DIFFERENT_ACCESS])) - - def test_group_different_access_reader_in_app_owner_in_anvil(self): - """anvil_audit works correctly if a group has different access to a workspace in AnVIL.""" - access = factories.WorkspaceGroupSharingFactory.create( - workspace=self.workspace, access=models.WorkspaceGroupSharing.READER - ) - self.update_api_response(access.group.email, "OWNER", can_compute=True, can_share=True) - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(access) - self.assertFalse(model_result.ok()) - self.assertEqual( - model_result.errors, - set( - [ - audit_results.ERROR_DIFFERENT_ACCESS, - audit_results.ERROR_DIFFERENT_CAN_COMPUTE, - audit_results.ERROR_DIFFERENT_CAN_SHARE, - ] - ), - ) - - def test_group_different_can_compute(self): - """anvil_audit works correctly if can_compute is different between the app and AnVIL.""" - access = factories.WorkspaceGroupSharingFactory.create( - workspace=self.workspace, - access=models.WorkspaceGroupSharing.WRITER, - can_compute=True, - ) - self.update_api_response(access.group.email, "WRITER", can_compute=False) - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(access) - self.assertFalse(model_result.ok()) - self.assertEqual(model_result.errors, set([audit_results.ERROR_DIFFERENT_CAN_COMPUTE])) - - def test_group_different_can_share(self): - """anvil_audit works correctly if can_share is True in AnVIL.""" - access = factories.WorkspaceGroupSharingFactory.create( - workspace=self.workspace, access=models.WorkspaceGroupSharing.WRITER - ) - self.update_api_response(access.group.email, "WRITER", can_share=True) - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(access) - self.assertFalse(model_result.ok()) - self.assertEqual(model_result.errors, set([audit_results.ERROR_DIFFERENT_CAN_SHARE])) - - def test_removes_service_account(self): - """Removes the service account from acl if it exists.""" - self.update_api_response(self.service_account_email, "OWNER", can_compute=True, can_share=True) - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - - def test_group_owner_can_share_true(self): - """Owners must have can_share=True.""" - access = factories.WorkspaceGroupSharingFactory.create( - workspace=self.workspace, - access=models.WorkspaceGroupSharing.OWNER, - can_compute=True, - ) - self.update_api_response(access.group.email, "OWNER", can_compute=True, can_share=True) - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertTrue(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 1) - self.assertEqual(len(audit_results.get_error_results()), 0) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - - def test_group_writer_can_share_false(self): - """Writers must have can_share=False.""" - access = factories.WorkspaceGroupSharingFactory.create( - workspace=self.workspace, - access=models.WorkspaceGroupSharing.WRITER, - can_compute=True, - ) - self.update_api_response(access.group.email, "WRITER", can_compute=True, can_share=True) - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(access) - self.assertFalse(model_result.ok()) - self.assertEqual(model_result.errors, set([audit_results.ERROR_DIFFERENT_CAN_SHARE])) - - def test_group_reader_can_share_false(self): - """Readers must have can_share=False.""" - access = factories.WorkspaceGroupSharingFactory.create( - workspace=self.workspace, - access=models.WorkspaceGroupSharing.READER, - can_compute=False, - ) - self.update_api_response(access.group.email, "READER", can_compute=False, can_share=True) - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - audit_results = audit.WorkspaceSharingAudit(self.workspace) - audit_results.run_audit() - self.assertFalse(audit_results.ok()) - self.assertEqual(len(audit_results.get_verified_results()), 0) - self.assertEqual(len(audit_results.get_error_results()), 1) - self.assertEqual(len(audit_results.get_not_in_app_results()), 0) - model_result = audit_results.get_result_for_model_instance(access) - self.assertFalse(model_result.ok()) - self.assertEqual(model_result.errors, set([audit_results.ERROR_DIFFERENT_CAN_SHARE])) diff --git a/anvil_consortium_manager/tests/test_commands.py b/anvil_consortium_manager/tests/test_commands.py index 8ce9a3ca..e2e9887d 100644 --- a/anvil_consortium_manager/tests/test_commands.py +++ b/anvil_consortium_manager/tests/test_commands.py @@ -1,305 +1,11 @@ """Tests for management commands in `anvil_consortium_manager`.""" -import pprint from io import StringIO -from unittest import skip, skipUnless +from unittest import skipUnless -import responses from django.conf import settings -from django.contrib.sites.models import Site -from django.core import mail -from django.core.management import CommandError, call_command -from django.test import TestCase, TransactionTestCase - -from ..audit import audit -from ..management.commands.run_anvil_audit import ErrorTableWithLink -from . import factories -from .utils import AnVILAPIMockTestMixin - - -class RunAnvilAuditTest(AnVILAPIMockTestMixin, TestCase): - """Tests for the run_anvil_audit command""" - - def get_api_url_billing_project(self, billing_project_name): - return self.api_client.rawls_entry_point + "/api/billing/v2/" + billing_project_name - - def test_command_output_invalid_model(self): - """Appropriate error is returned when an invalid model is specified.""" - out = StringIO() - with self.assertRaises(CommandError) as e: - # Call with the "--models=foo" so it goes through the argparse validation. - # Calling with models=["foo"] does not throw an exception. - call_command("run_anvil_audit", "--models=foo", stdout=out) - self.assertIn("invalid choice", str(e.exception)) - - def test_command_output_no_model_specified(self): - """Runs on all models if no models are specified.""" - # Add API call responses. - self.anvil_response_mock.add( - responses.GET, - self.api_client.sam_entry_point + "/api/groups/v1", - status=200, - json=[], - ) - self.anvil_response_mock.add( - responses.GET, - self.api_client.rawls_entry_point + "/api/workspaces", - status=200, - json=[], - ) - out = StringIO() - call_command("run_anvil_audit", "--no-color", stdout=out) - self.assertIn("BillingProjectAudit... ok!", out.getvalue()) - self.assertIn("AccountAudit... ok!", out.getvalue()) - self.assertIn("ManagedGroupAudit... ok!", out.getvalue()) - self.assertIn("WorkspaceAudit... ok!", out.getvalue()) - - def test_command_output_multiple_models(self): - """Can audit multiple models at the same time.""" - out = StringIO() - call_command( - "run_anvil_audit", - "--no-color", - models=["BillingProject", "Account"], - stdout=out, - ) - self.assertIn("BillingProjectAudit... ok!", out.getvalue()) - self.assertIn("AccountAudit... ok!", out.getvalue()) - - def test_command_output_billing_project_no_instances(self): - """Test command output.""" - out = StringIO() - call_command("run_anvil_audit", "--no-color", models=["BillingProject"], stdout=out) - self.assertIn("BillingProjectAudit... ok!", out.getvalue()) - - def test_command_output_account_no_instances(self): - """Test command output.""" - out = StringIO() - call_command("run_anvil_audit", "--no-color", models=["Account"], stdout=out) - self.assertIn("AccountAudit... ok!", out.getvalue()) - - def test_command_output_managed_group_no_instances(self): - """Test command output.""" - self.anvil_response_mock.add( - responses.GET, - self.api_client.sam_entry_point + "/api/groups/v1", - status=200, - json=[], - ) - out = StringIO() - call_command("run_anvil_audit", "--no-color", models=["ManagedGroup"], stdout=out) - self.assertIn("ManagedGroupAudit... ok!", out.getvalue()) - - def test_command_output_workspace_no_instances(self): - """Test command output.""" - self.anvil_response_mock.add( - responses.GET, - self.api_client.rawls_entry_point + "/api/workspaces", - status=200, - json=[], - ) - out = StringIO() - call_command("run_anvil_audit", "--no-color", models=["Workspace"], stdout=out) - self.assertIn("WorkspaceAudit... ok!", out.getvalue()) - - def test_command_run_audit_one_instance_ok(self): - """Test command output.""" - billing_project = factories.BillingProjectFactory.create() - # Add a response. - api_url = self.get_api_url_billing_project(billing_project.name) - self.anvil_response_mock.add(responses.GET, api_url, status=200) - out = StringIO() - call_command("run_anvil_audit", "--no-color", models=["BillingProject"], stdout=out) - self.assertIn("BillingProjectAudit... ok!", out.getvalue()) - self.assertNotIn("errors", out.getvalue()) - self.assertNotIn("not_in_app", out.getvalue()) - - def test_command_run_audit_ok_email(self): - """Test command output.""" - billing_project = factories.BillingProjectFactory.create() - # Add a response. - api_url = self.get_api_url_billing_project(billing_project.name) - self.anvil_response_mock.add(responses.GET, api_url, status=200) - out = StringIO() - call_command( - "run_anvil_audit", - "--no-color", - models=["BillingProject"], - email="test@example.com", - stdout=out, - ) - self.assertIn("BillingProjectAudit... ok!", out.getvalue()) - # One message has been sent by default. - self.assertEqual(len(mail.outbox), 1) - email = mail.outbox[0] - self.assertEqual(email.to, ["test@example.com"]) - self.assertIn("ok", email.subject) - # Text body. - audit_results = audit.BillingProjectAudit() - audit_results.run_audit() - self.assertEqual(pprint.pformat(audit_results.export()), email.body) - # HTML body. - self.assertEqual(len(email.alternatives), 1) - # Check that the number of "ok" instances is correct in email body. - self.assertIn("1 instance(s) verified", email.alternatives[0][0]) - - def test_command_run_audit_ok_email_errors_only(self): - """Test command output when email and errors_only is set.""" - billing_project = factories.BillingProjectFactory.create() - # Add a response. - api_url = self.get_api_url_billing_project(billing_project.name) - self.anvil_response_mock.add(responses.GET, api_url, status=200) - out = StringIO() - call_command( - "run_anvil_audit", - "--no-color", - models=["BillingProject"], - email="test@example.com", - errors_only=True, - stdout=out, - ) - self.assertIn("BillingProjectAudit... ok!", out.getvalue()) - # No message has been sent by default. - self.assertEqual(len(mail.outbox), 0) - - def test_command_run_audit_not_ok(self): - """Test command output when BillingProject audit is not ok.""" - billing_project = factories.BillingProjectFactory.create() - # Add a response. - api_url = self.get_api_url_billing_project(billing_project.name) - self.anvil_response_mock.add(responses.GET, api_url, status=404, json={"message": "error"}) - out = StringIO() - call_command("run_anvil_audit", "--no-color", models=["BillingProject"], stdout=out) - self.assertIn("BillingProjectAudit... problems found.", out.getvalue()) - self.assertIn("""'errors':""", out.getvalue()) - self.assertIn(audit.BillingProjectAudit.ERROR_NOT_IN_ANVIL, out.getvalue()) - - def test_command_run_audit_not_ok_email(self): - """Test command output when BillingProject audit is not ok with email specified.""" - billing_project = factories.BillingProjectFactory.create() - # Add a response. - api_url = self.get_api_url_billing_project(billing_project.name) - self.anvil_response_mock.add(responses.GET, api_url, status=404, json={"message": "error"}) - out = StringIO() - call_command( - "run_anvil_audit", - "--no-color", - models=["BillingProject"], - email="test@example.com", - stdout=out, - ) - self.assertIn("BillingProjectAudit... problems found.", out.getvalue()) - # Not printed to stdout. - self.assertIn("""'errors':""", out.getvalue()) - # One message has been sent. - self.assertEqual(len(mail.outbox), 1) - email = mail.outbox[0] - self.assertEqual(email.to, ["test@example.com"]) - self.assertIn("errors", email.subject) - # Text body. - audit_results = audit.BillingProjectAudit() - audit_results.run_audit() - # HTML body. - self.assertEqual(len(email.alternatives), 1) - - def test_command_run_audit_not_ok_email_has_html_link(self): - """Test command output when BillingProject audit is not ok with email specified.""" - billing_project = factories.BillingProjectFactory.create() - # Add a response. - api_url = self.get_api_url_billing_project(billing_project.name) - self.anvil_response_mock.add(responses.GET, api_url, status=404, json={"message": "error"}) - out = StringIO() - call_command( - "run_anvil_audit", - "--no-color", - models=["BillingProject"], - email="test@example.com", - stdout=out, - ) - email = mail.outbox[0] - self.assertEqual(len(email.alternatives), 1) - html_fragment = """{obj}""".format( - obj=str(billing_project), url=billing_project.get_absolute_url() - ) - self.assertInHTML(html_fragment, email.alternatives[0][0]) - - def test_command_run_audit_not_ok_email_has_html_link_different_domain(self): - """Test command output when BillingProject audit is not ok with email specified.""" - site = Site.objects.create(domain="foobar.com", name="test") - site.save() - with self.settings(SITE_ID=site.id): - billing_project = factories.BillingProjectFactory.create() - # Add a response. - api_url = self.get_api_url_billing_project(billing_project.name) - self.anvil_response_mock.add(responses.GET, api_url, status=404, json={"message": "error"}) - out = StringIO() - call_command( - "run_anvil_audit", - "--no-color", - models=["BillingProject"], - email="test@example.com", - stdout=out, - ) - email = mail.outbox[0] - self.assertEqual(len(email.alternatives), 1) - html_fragment = """{obj}""".format( - obj=str(billing_project), url=billing_project.get_absolute_url() - ) - self.assertInHTML(html_fragment, email.alternatives[0][0]) - - def test_command_run_audit_api_error(self): - """Test command output when BillingProject audit is not ok.""" - billing_project = factories.BillingProjectFactory.create() - # Add a response. - api_url = self.get_api_url_billing_project(billing_project.name) - self.anvil_response_mock.add(responses.GET, api_url, status=500, json={"message": "error"}) - out = StringIO() - with self.assertRaises(CommandError): - call_command("run_anvil_audit", "--no-color", models=["BillingProject"], stdout=out) - - # This test is complicated so skipping for now. - # When trying to change the settings, the test attempts to repopulate the - # workspace registry. This then causes an error. Skip it until we figure out - # how best to handle this situation. - @skip - def test_command_without_sites(self): - """Test command behavior without the Sites framework enabled.""" - out = StringIO() - with self.modify_settings(INSTALLED_APPS={"remove": ["django.contrib.sites"]}): - # with self.assertRaises(ImproperlyConfigured): - call_command( - "run_anvil_audit", - "--no-color", - models=["BillingProject"], - email="test@example.com", - stdout=out, - ) - - -class RunAnVILAuditTablesTest(TestCase): - def setUp(self): - super().setUp() - - class GenericAuditResults(audit.AnVILAudit): - TEST_ERROR_1 = "Test error 1" - TEST_ERROR_2 = "Test error 2" - - self.audit_results = GenericAuditResults() - # It doesn't matter what model we use at this point, so just pick Account. - self.model_factory = factories.AccountFactory - - def test_errors_table(self): - """Errors table is correct.""" - obj_verified = self.model_factory.create() - self.audit_results.add_result(audit.ModelInstanceResult(obj_verified)) - obj_error = self.model_factory.create() - error_result = audit.ModelInstanceResult(obj_error) - error_result.add_error(self.audit_results.TEST_ERROR_1) - error_result.add_error(self.audit_results.TEST_ERROR_2) - self.audit_results.add_result(error_result) - self.audit_results.add_result(audit.NotInAppResult("foo")) - table = ErrorTableWithLink(self.audit_results.get_error_results()) - self.assertEqual(table.rows[0].get_cell("errors"), "Test error 1, Test error 2") +from django.core.management import call_command +from django.test import TransactionTestCase class ConvertMariaDbUUIDFieldsTest(TransactionTestCase): diff --git a/anvil_consortium_manager/tests/test_viewmixins.py b/anvil_consortium_manager/tests/test_viewmixins.py index 558d1ff6..7405d190 100644 --- a/anvil_consortium_manager/tests/test_viewmixins.py +++ b/anvil_consortium_manager/tests/test_viewmixins.py @@ -1,4 +1,3 @@ -from django.core.exceptions import ImproperlyConfigured from django.test import RequestFactory, TestCase from .. import viewmixins @@ -7,14 +6,6 @@ from .test_app.adapters import TestWorkspaceAdapter -class AnVILAuditMixinTest(TestCase): - """ManagedGroupGraphMixin tests that aren't covered elsewhere.""" - - def test_run_audit_not_implemented(self): - with self.assertRaises(ImproperlyConfigured): - viewmixins.AnVILAuditMixin().run_audit() - - class ManagedGroupGraphMixinTest(TestCase): """ManagedGroupGraphMixin tests that aren't covered elsewhere.""" diff --git a/anvil_consortium_manager/tests/test_views.py b/anvil_consortium_manager/tests/test_views.py index a95791ec..25761d47 100644 --- a/anvil_consortium_manager/tests/test_views.py +++ b/anvil_consortium_manager/tests/test_views.py @@ -20,10 +20,9 @@ from faker import Faker from freezegun import freeze_time -from .. import __version__, anvil_api, filters, forms, models, tables, views +from .. import __version__, filters, forms, models, tables, views from ..adapters.default import DefaultWorkspaceAdapter from ..adapters.workspace import workspace_adapter_registry -from ..audit import audit from ..tokens import account_verification_token from . import factories from .test_app import forms as app_forms @@ -109,15 +108,15 @@ class ViewEditUrlTest(TestCase): reverse("anvil_consortium_manager:index"), reverse("anvil_consortium_manager:status"), reverse("anvil_consortium_manager:accounts:list"), - reverse("anvil_consortium_manager:accounts:audit"), + reverse("anvil_consortium_manager:auditor:accounts:all"), reverse("anvil_consortium_manager:billing_projects:list"), - reverse("anvil_consortium_manager:billing_projects:audit"), + reverse("anvil_consortium_manager:auditor:billing_projects:all"), reverse("anvil_consortium_manager:managed_groups:list"), reverse("anvil_consortium_manager:managed_groups:visualization"), - reverse("anvil_consortium_manager:managed_groups:audit"), + reverse("anvil_consortium_manager:auditor:managed_groups:all"), reverse("anvil_consortium_manager:workspaces:landing_page"), reverse("anvil_consortium_manager:workspaces:list_all"), - reverse("anvil_consortium_manager:workspaces:audit"), + reverse("anvil_consortium_manager:auditor:workspaces:all"), ) # other_urls = ( @@ -132,8 +131,8 @@ class ViewEditUrlTest(TestCase): # reverse("anvil_consortium_manager:managed_groups:member_groups:delete"), # reverse("anvil_consortium_manager:workspaces:sharing:update"), # reverse("anvil_consortium_manager:workspaces:delete"), - # reverse("anvil_consortium_manager:managed_groups:audit_membership"), - # reverse("anvil_consortium_manager:workspaces:audit_access"), + # reverse("anvil_consortium_manager:auditor:managed_groups:membership:by_group:all"), + # reverse("anvil_consortium_manager:auditor:workspaces:all_access"), # ) edit_urls = ( @@ -1042,156 +1041,6 @@ def test_does_not_return_billing_projects_where_app_is_not_user(self): self.assertEqual(json.loads(response.content.decode("utf-8"))["results"], []) -class BillingProjectAuditTest(AnVILAPIMockTestMixin, TestCase): - """Tests for the BillingProjectAudit view.""" - - def setUp(self): - """Set up test class.""" - super().setUp() - self.factory = RequestFactory() - # Create a user with only view permission. - self.user = User.objects.create_user(username="test", password="test") - self.user.user_permissions.add( - Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) - ) - - def get_url(self, *args): - """Get the url for the view being tested.""" - return reverse("anvil_consortium_manager:billing_projects:audit", args=args) - - def get_api_url(self, billing_project_name): - return self.api_client.rawls_entry_point + "/api/billing/v2/" + billing_project_name - - def get_api_json_response(self): - return { - "roles": ["User"], - } - - def get_view(self): - """Return the view being tested.""" - return views.BillingProjectAudit.as_view() - - def test_view_redirect_not_logged_in(self): - "View redirects to login view when user is not logged in." - # Need a client for redirects. - response = self.client.get(self.get_url()) - self.assertRedirects(response, resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url()) - - def test_status_code_with_user_permission(self): - """Returns successful response code.""" - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertEqual(response.status_code, 200) - - def test_access_without_user_permission(self): - """Raises permission denied if user has no permissions.""" - user_no_perms = User.objects.create_user(username="test-none", password="test-none") - request = self.factory.get(self.get_url()) - request.user = user_no_perms - with self.assertRaises(PermissionDenied): - self.get_view()(request) - - def test_access_with_limited_view_permission(self): - """Raises permission denied if user has limited view permission.""" - user = User.objects.create_user(username="test-limited", password="test-limited") - user.user_permissions.add( - Permission.objects.get(codename=models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME) - ) - request = self.factory.get(self.get_url()) - request.user = user - with self.assertRaises(PermissionDenied): - self.get_view()(request) - - def test_template(self): - """Template loads successfully.""" - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertEqual(response.status_code, 200) - - def test_audit_verified(self): - """audit_verified is in the context data.""" - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("verified_table", response.context_data) - self.assertIsInstance(response.context_data["verified_table"], audit.VerifiedTable) - self.assertEqual(len(response.context_data["verified_table"].rows), 0) - - def test_audit_verified_one_record(self): - """audit_verified with one verified record.""" - billing_project = factories.BillingProjectFactory.create() - api_url = self.get_api_url(billing_project.name) - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=self.get_api_json_response(), - ) - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("verified_table", response.context_data) - self.assertEqual(len(response.context_data["verified_table"].rows), 1) - - def test_audit_errors(self): - """audit_errors is in the context data.""" - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("error_table", response.context_data) - self.assertIsInstance(response.context_data["error_table"], audit.ErrorTable) - self.assertEqual(len(response.context_data["error_table"].rows), 0) - - def test_audit_errors_one_record(self): - """audit_errors with one verified record.""" - billing_project = factories.BillingProjectFactory.create() - api_url = self.get_api_url(billing_project.name) - self.anvil_response_mock.add( - responses.GET, - api_url, - status=404, - json={"message": "other error"}, - ) - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("error_table", response.context_data) - self.assertEqual(len(response.context_data["error_table"].rows), 1) - - def test_audit_not_in_app(self): - """audit_errors is in the context data.""" - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("not_in_app_table", response.context_data) - self.assertIsInstance(response.context_data["not_in_app_table"], audit.NotInAppTable) - self.assertEqual(len(response.context_data["not_in_app_table"].rows), 0) - - def test_audit_ok_is_ok(self): - """audit_ok when audit_results.ok() is True.""" - billing_project = factories.BillingProjectFactory.create() - api_url = self.get_api_url(billing_project.name) - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - ) - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("audit_ok", response.context_data) - self.assertEqual(response.context_data["audit_ok"], True) - - def test_audit_ok_is_not_ok(self): - """audit_ok when audit_results.ok() is True.""" - billing_project = factories.BillingProjectFactory.create() - api_url = self.get_api_url(billing_project.name) - self.anvil_response_mock.add( - responses.GET, - api_url, - status=404, - json={"message": "other error"}, - ) - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("audit_ok", response.context_data) - self.assertEqual(response.context_data["audit_ok"], False) - - class AccountDetailTest(TestCase): def setUp(self): """Set up test class.""" @@ -4591,159 +4440,6 @@ def test_adapter_labels(self): self.assertEqual(view.get_selected_result_label(account), "TEST test@bar.com") -class AccountAuditTest(AnVILAPIMockTestMixin, TestCase): - """Tests for the AccountAudit view.""" - - def setUp(self): - """Set up test class.""" - super().setUp() - self.factory = RequestFactory() - # Create a user with only view permission. - self.user = User.objects.create_user(username="test", password="test") - self.user.user_permissions.add( - Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) - ) - - def get_url(self, *args): - """Get the url for the view being tested.""" - return reverse("anvil_consortium_manager:accounts:audit", args=args) - - def get_api_url(self, email): - return self.api_client.sam_entry_point + "/api/users/v1/" + email - - def get_api_json_response(self, email): - id = fake.bothify(text="#" * 21) - return { - "googleSubjectId": id, - "userEmail": email, - "userSubjectId": id, - } - - def get_view(self): - """Return the view being tested.""" - return views.AccountAudit.as_view() - - def test_view_redirect_not_logged_in(self): - "View redirects to login view when user is not logged in." - # Need a client for redirects. - response = self.client.get(self.get_url()) - self.assertRedirects(response, resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url()) - - def test_status_code_with_user_permission(self): - """Returns successful response code.""" - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertEqual(response.status_code, 200) - - def test_access_with_limited_view_permission(self): - """Raises permission denied if user has limited view permission.""" - user = User.objects.create_user(username="test-limited", password="test-limited") - user.user_permissions.add( - Permission.objects.get(codename=models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME) - ) - request = self.factory.get(self.get_url()) - request.user = user - with self.assertRaises(PermissionDenied): - self.get_view()(request) - - def test_access_without_user_permission(self): - """Raises permission denied if user has no permissions.""" - user_no_perms = User.objects.create_user(username="test-none", password="test-none") - request = self.factory.get(self.get_url()) - request.user = user_no_perms - with self.assertRaises(PermissionDenied): - self.get_view()(request) - - def test_template(self): - """Template loads successfully.""" - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertEqual(response.status_code, 200) - - def test_audit_verified(self): - """audit_verified is in the context data.""" - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("verified_table", response.context_data) - self.assertIsInstance(response.context_data["verified_table"], audit.VerifiedTable) - self.assertEqual(len(response.context_data["verified_table"].rows), 0) - - def test_audit_verified_one_verified(self): - """audit_verified with one verified record.""" - account = factories.AccountFactory.create() - api_url = self.get_api_url(account.email) - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=self.get_api_json_response(account.email), - ) - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("verified_table", response.context_data) - self.assertEqual(len(response.context_data["verified_table"].rows), 1) - - def test_audit_errors(self): - """audit_errors is in the context data.""" - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("error_table", response.context_data) - self.assertIsInstance(response.context_data["error_table"], audit.ErrorTable) - self.assertEqual(len(response.context_data["error_table"].rows), 0) - - def test_audit_errors_one_verified(self): - """audit_errors with one verified record.""" - account = factories.AccountFactory.create() - api_url = self.get_api_url(account.email) - self.anvil_response_mock.add( - responses.GET, - api_url, - status=404, - json={"message": "other error"}, - ) - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("error_table", response.context_data) - self.assertEqual(len(response.context_data["error_table"].rows), 1) - - def test_audit_not_in_app(self): - """audit_errors is in the context data.""" - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("not_in_app_table", response.context_data) - self.assertEqual(len(response.context_data["error_table"].rows), 0) - - def test_audit_ok_is_ok(self): - """audit_ok when audit_results.ok() is True.""" - account = factories.AccountFactory.create() - api_url = self.get_api_url(account.email) - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=self.get_api_json_response(account.email), - ) - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("audit_ok", response.context_data) - self.assertEqual(response.context_data["audit_ok"], True) - - def test_audit_ok_is_not_ok(self): - """audit_ok when audit_results.ok() is True.""" - account = factories.AccountFactory.create() - api_url = self.get_api_url(account.email) - self.anvil_response_mock.add( - responses.GET, - api_url, - status=404, - json={"message": "other error"}, - ) - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("audit_ok", response.context_data) - self.assertEqual(response.context_data["audit_ok"], False) - - class ManagedGroupDetailTest(TestCase): def setUp(self): """Set up test class.""" @@ -6117,14 +5813,11 @@ def test_does_not_return_groups_not_managed_by_app_when_specified(self): self.assertEqual(json.loads(response.content.decode("utf-8"))["results"], []) -class ManagedGroupAuditTest(AnVILAPIMockTestMixin, TestCase): - """Tests for the ManagedGroupAudit view.""" - +class ManagedGroupVisualizationTest(TestCase): def setUp(self): """Set up test class.""" - super().setUp() self.factory = RequestFactory() - # Create a user with only view permission. + # Create a user with both view and edit permission. self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) @@ -6132,40 +5825,7 @@ def setUp(self): def get_url(self, *args): """Get the url for the view being tested.""" - return reverse("anvil_consortium_manager:managed_groups:audit", args=args) - - def get_api_groups_url(self): - """Return the API url being called by the method.""" - return self.api_client.sam_entry_point + "/api/groups/v1" - - def get_api_group_json(self, group_name, role): - """Return json data about groups in the API format.""" - json_data = { - "groupEmail": group_name + "@firecloud.org", - "groupName": group_name, - "role": role, - } - return json_data - - def get_api_url_members(self, group_name): - """Return the API url being called by the method.""" - return self.api_client.sam_entry_point + "/api/groups/v1/" + group_name + "/member" - - def get_api_url_admins(self, group_name): - """Return the API url being called by the method.""" - return self.api_client.sam_entry_point + "/api/groups/v1/" + group_name + "/admin" - - def get_api_json_response_admins(self, emails=[]): - """Return json data about groups in the API format.""" - return [anvil_api.AnVILAPIClient().auth_session.credentials.service_account_email] + emails - - def get_api_json_response_members(self, emails=[]): - """Return json data about groups in the API format.""" - return emails - - def get_view(self): - """Return the view being tested.""" - return views.ManagedGroupAudit.as_view() + return reverse("anvil_consortium_manager:managed_groups:visualization", args=args) def test_view_redirect_not_logged_in(self): "View redirects to login view when user is not logged in." @@ -6175,13 +5835,6 @@ def test_view_redirect_not_logged_in(self): def test_status_code_with_user_permission(self): """Returns successful response code.""" - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[], - ) self.client.force_login(self.user) response = self.client.get(self.get_url()) self.assertEqual(response.status_code, 200) @@ -6195,949 +5848,577 @@ def test_access_with_limited_view_permission(self): request = self.factory.get(self.get_url()) request.user = user with self.assertRaises(PermissionDenied): - self.get_view()(request) + views.ManagedGroupVisualization.as_view()(request) - def test_access_without_user_permission(self): - """Raises permission denied if user has no permissions.""" - user_no_perms = User.objects.create_user(username="test-none", password="test-none") - request = self.factory.get(self.get_url()) - request.user = user_no_perms - with self.assertRaises(PermissionDenied): - self.get_view()(request) + def test_view_status_code_with_existing_object_not_managed(self): + """Returns a successful status code for an existing object pk.""" + self.client.force_login(self.user) + response = self.client.get(self.get_url()) + self.assertEqual(response.status_code, 200) - def test_template(self): - """Template loads successfully.""" - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[], - ) + def test_no_groups(self): + """Visualization when there are no groups.""" self.client.force_login(self.user) response = self.client.get(self.get_url()) - self.assertEqual(response.status_code, 200) + self.assertIn("graph", response.context_data) - def test_audit_verified(self): - """audit_verified is in the context data.""" - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[], - ) + def test_one_group(self): + """Visualization when there is one group.""" + factories.ManagedGroupFactory.create() self.client.force_login(self.user) response = self.client.get(self.get_url()) - self.assertIn("verified_table", response.context_data) - self.assertIsInstance(response.context_data["verified_table"], audit.VerifiedTable) - self.assertEqual(len(response.context_data["verified_table"].rows), 0) + self.assertIn("graph", response.context_data) - def test_audit_verified_one_record(self): - """audit_verified with one verified record.""" - group = factories.ManagedGroupFactory.create() - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[self.get_api_group_json(group.name, "Admin")], - ) - # Group membership API call. - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=self.get_api_json_response_members(emails=[]), - ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=self.get_api_json_response_admins(emails=[]), - ) + def test_two_groups(self): + """Visualization when there are two groups.""" + factories.ManagedGroupFactory.create(name="g1") + factories.ManagedGroupFactory.create(name="g2") self.client.force_login(self.user) response = self.client.get(self.get_url()) - self.assertIn("verified_table", response.context_data) - self.assertEqual(len(response.context_data["verified_table"].rows), 1) + self.assertIn("graph", response.context_data) - def test_audit_errors(self): - """audit_errors is in the context data.""" - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[], + def test_group_visualization(self): + factories.ManagedGroupFactory.create() + grandparent = factories.ManagedGroupFactory.create() + parent_1 = factories.ManagedGroupFactory.create() + parent_2 = factories.ManagedGroupFactory.create() + child = factories.ManagedGroupFactory.create() + factories.GroupGroupMembershipFactory.create(parent_group=grandparent, child_group=parent_1) + factories.GroupGroupMembershipFactory.create(parent_group=grandparent, child_group=parent_2) + factories.GroupGroupMembershipFactory.create( + parent_group=parent_1, + child_group=child, + role=models.GroupGroupMembership.ADMIN, ) self.client.force_login(self.user) response = self.client.get(self.get_url()) - self.assertIn("error_table", response.context_data) - self.assertIsInstance(response.context_data["error_table"], audit.ErrorTable) - self.assertEqual(len(response.context_data["error_table"].rows), 0) + self.assertIn("graph", response.context_data) - def test_audit_errors_one_record(self): - """audit_errors with one error record.""" - group = factories.ManagedGroupFactory.create() - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - # Error - we are not the admin - json=[self.get_api_group_json(group.name, "Member")], + +class WorkspaceLandingPageTest(TestCase): + def setUp(self): + """Set up test class.""" + self.factory = RequestFactory() + # Create a user with view permission. + self.view_user = User.objects.create_user(username="test_view", password="view") + self.view_user.user_permissions.add( + Permission.objects.get(codename=models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME) + ) + # Create a user with staff view permission. + self.staff_view_user = User.objects.create_user(username="test_staff_view", password="view") + self.staff_view_user.user_permissions.add( + Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) + ) + # Create a user with edit permission. + self.edit_user = User.objects.create_user(username="test_edit", password="test") + self.edit_user.user_permissions.add( + Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME), + Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME), ) - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("error_table", response.context_data) - self.assertEqual(len(response.context_data["error_table"].rows), 1) - def test_audit_not_in_app(self): - """audit_not_in_app is in the context data.""" - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[], + def tearDown(self): + """Clean up after tests.""" + # Unregister all adapters. + workspace_adapter_registry._registry = {} + # Register the default adapter. + workspace_adapter_registry.register(DefaultWorkspaceAdapter) + super().tearDown() + + def get_url(self): + """Get the url for the view being tested.""" + return reverse("anvil_consortium_manager:workspaces:landing_page") + + def test_view_redirect_not_logged_in(self): + "View redirects to login view when user is not logged in." + # Need a client for redirects. + response = self.client.get(self.get_url()) + self.assertRedirects( + response, + resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url(), ) - self.client.force_login(self.user) + + def test_status_code_with_staff_view_permission(self): + """Returns successful response code if user has staff_view permission.""" + self.client.force_login(self.staff_view_user) response = self.client.get(self.get_url()) - self.assertIn("not_in_app_table", response.context_data) - self.assertIsInstance(response.context_data["not_in_app_table"], audit.NotInAppTable) - self.assertEqual(len(response.context_data["not_in_app_table"].rows), 0) + self.assertEqual(response.status_code, 200) - def test_audit_not_in_app_one_record(self): - """audit_not_in_app with one record not in app.""" - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[self.get_api_group_json("foo", "Admin")], - ) - self.client.force_login(self.user) + def test_access_with_view_permission(self): + """Returns successful response code if user has view permission.""" + self.client.force_login(self.view_user) response = self.client.get(self.get_url()) - self.assertIn("not_in_app_table", response.context_data) - self.assertEqual(len(response.context_data["not_in_app_table"].rows), 1) + self.assertEqual(response.status_code, 200) - def test_audit_ok_is_ok(self): - """audit_ok when audit_results.ok() is True.""" - group = factories.ManagedGroupFactory.create() - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[self.get_api_group_json(group.name, "Admin")], + def test_staff_view_permission(self): + """Links to edit required do not appear in the page when user only has staff_view permission.""" + self.client.force_login(self.staff_view_user) + response = self.client.get(self.get_url()) + self.assertIn("show_edit_links", response.context_data) + self.assertFalse(response.context_data["show_edit_links"]) + self.assertNotContains( + response, + reverse( + "anvil_consortium_manager:workspaces:import", + kwargs={"workspace_type": "workspace"}, + ), ) - # Group membership API call. - api_url_members = self.get_api_url_members(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=self.get_api_json_response_members(emails=[]), + self.assertNotContains( + response, + reverse( + "anvil_consortium_manager:workspaces:new", + kwargs={"workspace_type": "workspace"}, + ), ) - api_url_admins = self.get_api_url_admins(group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=self.get_api_json_response_admins(emails=[]), + self.assertContains( + response, + reverse( + "anvil_consortium_manager:workspaces:list", + kwargs={"workspace_type": "workspace"}, + ), ) - self.client.force_login(self.user) + + def test_view_permission(self): + """Links to edit required do not appear in the page when user only has view permission.""" + self.client.force_login(self.view_user) response = self.client.get(self.get_url()) - self.assertIn("audit_ok", response.context_data) - self.assertEqual(response.context_data["audit_ok"], True) + self.assertIn("show_edit_links", response.context_data) + self.assertFalse(response.context_data["show_edit_links"]) + self.assertNotContains( + response, + reverse( + "anvil_consortium_manager:workspaces:import", + kwargs={"workspace_type": "workspace"}, + ), + ) + self.assertNotContains( + response, + reverse( + "anvil_consortium_manager:workspaces:new", + kwargs={"workspace_type": "workspace"}, + ), + ) + self.assertContains( + response, + reverse( + "anvil_consortium_manager:workspaces:list", + kwargs={"workspace_type": "workspace"}, + ), + ) - def test_audit_ok_is_not_ok(self): - """audit_ok when audit_results.ok() is False.""" - group = factories.ManagedGroupFactory.create() - api_url = self.get_api_groups_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - # Error - we are not admin. - json=[self.get_api_group_json(group.name, "Member")], + def test_edit_permission(self): + """Links to edit required appear in the page when user also has edit permission.""" + self.client.force_login(self.edit_user) + response = self.client.get(self.get_url()) + self.assertIn("show_edit_links", response.context_data) + self.assertTrue(response.context_data["show_edit_links"]) + self.assertContains( + response, + reverse( + "anvil_consortium_manager:workspaces:import", + kwargs={"workspace_type": "workspace"}, + ), ) - self.client.force_login(self.user) + self.assertContains( + response, + reverse( + "anvil_consortium_manager:workspaces:new", + kwargs={"workspace_type": "workspace"}, + ), + ) + self.assertContains( + response, + reverse( + "anvil_consortium_manager:workspaces:list", + kwargs={"workspace_type": "workspace"}, + ), + ) + + def test_one_registered_workspace_in_context(self): + """One registered workspace in context when only DefaultWorkspaceAdapter is registered""" + self.client.force_login(self.view_user) + response = self.client.get(self.get_url()) + self.assertIn("registered_workspace_adapters", response.context_data) + self.assertEqual(len(response.context_data["registered_workspace_adapters"]), 1) + + def test_two_registered_workspaces_in_context(self): + """Two registered workspaces in context when two workspace adapters are registered""" + workspace_adapter_registry.register(TestWorkspaceAdapter) + self.client.force_login(self.view_user) response = self.client.get(self.get_url()) - self.assertIn("audit_ok", response.context_data) - self.assertEqual(response.context_data["audit_ok"], False) + self.assertIn("registered_workspace_adapters", response.context_data) + self.assertEqual(len(response.context_data["registered_workspace_adapters"]), 2) -class ManagedGroupMembershipAuditTest(AnVILAPIMockTestMixin, TestCase): - """Tests for the ManagedGroupAudit view.""" +class WorkspaceDetailTest(TestCase): + """Tests for the WorkspaceDetail view.""" def setUp(self): """Set up test class.""" - super().setUp() self.factory = RequestFactory() # Create a user with both view and edit permission. self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) ) - self.group = factories.ManagedGroupFactory.create() - - def get_url(self, *args): - """Get the url for the view being tested.""" - return reverse("anvil_consortium_manager:managed_groups:audit_membership", args=args) - - def get_api_url_members(self, group_name): - """Return the API url being called by the method.""" - return self.api_client.sam_entry_point + "/api/groups/v1/" + group_name + "/member" - - def get_api_url_admins(self, group_name): - """Return the API url being called by the method.""" - return self.api_client.sam_entry_point + "/api/groups/v1/" + group_name + "/admin" - def get_api_json_response_admins(self, emails=[]): - """Return json data about groups in the API format.""" - return [anvil_api.AnVILAPIClient().auth_session.credentials.service_account_email] + emails - - def get_api_json_response_members(self, emails=[]): - """Return json data about groups in the API format.""" - return emails + def tearDown(self): + """Clean up after tests.""" + # Unregister all adapters. + workspace_adapter_registry._registry = {} + # Register the default adapter. + workspace_adapter_registry.register(DefaultWorkspaceAdapter) + super().tearDown() def get_view(self): """Return the view being tested.""" - return views.ManagedGroupMembershipAudit.as_view() + return views.WorkspaceDetail.as_view() + + def get_url(self, *args): + """Get the url for the view being tested.""" + return reverse("anvil_consortium_manager:workspaces:detail", args=args) def test_view_redirect_not_logged_in(self): "View redirects to login view when user is not logged in." # Need a client for redirects. - response = self.client.get(self.get_url("foo")) - self.assertRedirects(response, resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url("foo")) - - def test_status_code_with_user_permission(self): + url = reverse("anvil_consortium_manager:workspaces:detail", args=["foo1", "foo2"]) + response = self.client.get(url) + self.assertRedirects(response, resolve_url(settings.LOGIN_URL) + "?next=" + url) + + def test_status_code_with_staff_view_permission(self): """Returns successful response code.""" - api_url_members = self.get_api_url_members(self.group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=self.get_api_json_response_members(emails=[]), - ) - api_url_admins = self.get_api_url_admins(self.group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=self.get_api_json_response_admins(emails=[]), - ) + obj = factories.DefaultWorkspaceDataFactory.create() self.client.force_login(self.user) - response = self.client.get(self.get_url(self.group.name)) + response = self.client.get(obj.get_absolute_url()) self.assertEqual(response.status_code, 200) - def test_access_with_limited_view_permission(self): + def test_access_with_view_permission(self): """Raises permission denied if user has limited view permission.""" user = User.objects.create_user(username="test-limited", password="test-limited") user.user_permissions.add( Permission.objects.get(codename=models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME) ) - request = self.factory.get(self.get_url("foo")) - request.user = user - with self.assertRaises(PermissionDenied): - self.get_view()(request) + obj = factories.DefaultWorkspaceDataFactory.create() + self.client.force_login(user) + response = self.client.get(obj.get_absolute_url()) + self.assertEqual(response.status_code, 200) def test_access_without_user_permission(self): """Raises permission denied if user has no permissions.""" user_no_perms = User.objects.create_user(username="test-none", password="test-none") - request = self.factory.get(self.get_url("foo")) + request = self.factory.get(self.get_url("foo", "bar")) request.user = user_no_perms with self.assertRaises(PermissionDenied): - self.get_view()(request, slug="foo") - - def test_template(self): - """Template loads successfully.""" - api_url_members = self.get_api_url_members(self.group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=self.get_api_json_response_members(emails=[]), - ) - api_url_admins = self.get_api_url_admins(self.group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=self.get_api_json_response_admins(emails=[]), - ) - self.client.force_login(self.user) - response = self.client.get(self.get_url(self.group.name)) - self.assertEqual(response.status_code, 200) - - def test_audit_verified(self): - """audit_verified is in the context data.""" - api_url_members = self.get_api_url_members(self.group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=self.get_api_json_response_members(emails=[]), - ) - api_url_admins = self.get_api_url_admins(self.group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=self.get_api_json_response_admins(emails=[]), - ) - self.client.force_login(self.user) - response = self.client.get(self.get_url(self.group.name)) - self.assertIn("verified_table", response.context_data) - self.assertIsInstance(response.context_data["verified_table"], audit.VerifiedTable) - self.assertEqual(len(response.context_data["verified_table"].rows), 0) - - def test_audit_verified_one_record(self): - """audit_verified with one verified record.""" - membership = factories.GroupAccountMembershipFactory.create(group=self.group) - api_url_members = self.get_api_url_members(self.group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=self.get_api_json_response_members(emails=[membership.account.email]), - ) - api_url_admins = self.get_api_url_admins(self.group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=self.get_api_json_response_admins(emails=[]), - ) - self.client.force_login(self.user) - response = self.client.get(self.get_url(self.group.name)) - self.assertIn("verified_table", response.context_data) - self.assertEqual(len(response.context_data["verified_table"].rows), 1) + self.get_view()(request) - def test_audit_errors(self): - """audit_errors is in the context data.""" - api_url_members = self.get_api_url_members(self.group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=self.get_api_json_response_members(emails=[]), - ) - api_url_admins = self.get_api_url_admins(self.group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=self.get_api_json_response_admins(emails=[]), - ) - self.client.force_login(self.user) - response = self.client.get(self.get_url(self.group.name)) - self.assertIn("error_table", response.context_data) - self.assertIsInstance(response.context_data["error_table"], audit.ErrorTable) - self.assertEqual(len(response.context_data["error_table"].rows), 0) - - def test_audit_errors_one_record(self): - """audit_errors with one error record.""" - membership = factories.GroupAccountMembershipFactory.create(group=self.group) - # Group membership API call. - api_url_members = self.get_api_url_members(self.group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=self.get_api_json_response_members(emails=[]), - ) - api_url_admins = self.get_api_url_admins(self.group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=self.get_api_json_response_admins(emails=[membership.account.email]), - ) - self.client.force_login(self.user) - response = self.client.get(self.get_url(self.group.name)) - self.assertIn("error_table", response.context_data) - self.assertEqual(len(response.context_data["error_table"].rows), 1) + def test_view_status_code_with_invalid_pk(self): + """Raises a 404 error with an invalid object pk.""" + obj = factories.DefaultWorkspaceDataFactory.create() + request = self.factory.get(obj.get_absolute_url()) + request.user = self.user + with self.assertRaises(Http404): + self.get_view()(request, billing_project_slug="foo1", workspace_slug="foo2") - def test_audit_not_in_app(self): - """audit_not_in_app is in the context data.""" - api_url_members = self.get_api_url_members(self.group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=self.get_api_json_response_members(emails=[]), - ) - api_url_admins = self.get_api_url_admins(self.group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=self.get_api_json_response_admins(emails=[]), - ) + def test_context_workspace_data(self): + """The view adds the workspace_data object to the context.""" + obj = factories.DefaultWorkspaceDataFactory.create() self.client.force_login(self.user) - response = self.client.get(self.get_url(self.group.name)) - self.assertIn("not_in_app_table", response.context_data) - self.assertIsInstance(response.context_data["not_in_app_table"], audit.NotInAppTable) - self.assertEqual(len(response.context_data["not_in_app_table"].rows), 0) + response = self.client.get(obj.get_absolute_url()) + response.context_data + self.assertIn("workspace_data_object", response.context_data) + self.assertEqual(response.context_data["workspace_data_object"], obj) - def test_audit_not_in_app_one_record(self): - """audit_not_in_app with one record not in app.""" - api_url_members = self.get_api_url_members(self.group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=self.get_api_json_response_members(emails=["foo@bar.com"]), - ) - api_url_admins = self.get_api_url_admins(self.group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=self.get_api_json_response_admins(emails=[]), - ) + def test_group_sharing_table(self): + """The workspace group access table exists.""" + obj = factories.DefaultWorkspaceDataFactory.create() self.client.force_login(self.user) - response = self.client.get(self.get_url(self.group.name)) - self.assertIn("not_in_app_table", response.context_data) - self.assertEqual(len(response.context_data["not_in_app_table"].rows), 1) - - def test_audit_ok_is_ok(self): - """audit_ok when audit_results.ok() is True.""" - api_url_members = self.get_api_url_members(self.group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=self.get_api_json_response_members(emails=[]), - ) - api_url_admins = self.get_api_url_admins(self.group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=self.get_api_json_response_admins(emails=[]), + response = self.client.get(obj.get_absolute_url()) + self.assertIn("group_sharing_table", response.context_data) + self.assertIsInstance( + response.context_data["group_sharing_table"], + tables.WorkspaceGroupSharingStaffTable, ) - self.client.force_login(self.user) - response = self.client.get(self.get_url(self.group.name)) - self.assertIn("audit_ok", response.context_data) - self.assertEqual(response.context_data["audit_ok"], True) - def test_audit_ok_is_not_ok(self): - """audit_ok when audit_results.ok() is False.""" - api_url_members = self.get_api_url_members(self.group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_members, - status=200, - json=self.get_api_json_response_members(emails=["foo@bar.com"]), - ) - api_url_admins = self.get_api_url_admins(self.group.name) - self.anvil_response_mock.add( - responses.GET, - api_url_admins, - status=200, - json=self.get_api_json_response_admins(emails=[]), - ) + def test_group_sharing_table_none(self): + """No groups are shown if the workspace has not been shared with any groups.""" + workspace = factories.DefaultWorkspaceDataFactory.create() self.client.force_login(self.user) - response = self.client.get(self.get_url(self.group.name)) - self.assertIn("audit_ok", response.context_data) - self.assertEqual(response.context_data["audit_ok"], False) + response = self.client.get(workspace.get_absolute_url()) + self.assertIn("group_sharing_table", response.context_data) + self.assertEqual(len(response.context_data["group_sharing_table"].rows), 0) - def test_group_not_managed_by_app(self): - """Redirects with a message when group is not managed by app.""" - group = factories.ManagedGroupFactory.create(is_managed_by_app=False) - # Only clients load the template. + def test_group_sharing_table_one(self): + """One group is shown if the workspace has been shared with one group.""" + workspace = factories.DefaultWorkspaceDataFactory.create() + factories.WorkspaceGroupSharingFactory.create(workspace=workspace.workspace) self.client.force_login(self.user) - response = self.client.get(self.get_url(group.name), follow=True) - self.assertRedirects(response, group.get_absolute_url()) - messages = [m.message for m in get_messages(response.wsgi_request)] - self.assertEqual(len(messages), 1) - self.assertEqual( - str(messages[0]), - views.ManagedGroupMembershipAudit.message_not_managed_by_app, - ) - - def test_group_does_not_exist_in_app(self): - """Raises a 404 error with an invalid object pk.""" - factories.ManagedGroupFactory.create() - request = self.factory.get(self.get_url("foo")) - request.user = self.user - with self.assertRaises(Http404): - self.get_view()(request, slug="foo") - - -class ManagedGroupVisualizationTest(TestCase): - def setUp(self): - """Set up test class.""" - self.factory = RequestFactory() - # Create a user with both view and edit permission. - self.user = User.objects.create_user(username="test", password="test") - self.user.user_permissions.add( - Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) - ) - - def get_url(self, *args): - """Get the url for the view being tested.""" - return reverse("anvil_consortium_manager:managed_groups:visualization", args=args) - - def test_view_redirect_not_logged_in(self): - "View redirects to login view when user is not logged in." - # Need a client for redirects. - response = self.client.get(self.get_url()) - self.assertRedirects(response, resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url()) + response = self.client.get(workspace.get_absolute_url()) + self.assertIn("group_sharing_table", response.context_data) + self.assertEqual(len(response.context_data["group_sharing_table"].rows), 1) - def test_status_code_with_user_permission(self): - """Returns successful response code.""" + def test_group_sharing_table_two(self): + """Two groups are shown if the workspace has been shared with two groups.""" + workspace = factories.DefaultWorkspaceDataFactory.create() + factories.WorkspaceGroupSharingFactory.create(group__name="g1", workspace=workspace.workspace) + factories.WorkspaceGroupSharingFactory.create(group__name="g2", workspace=workspace.workspace) self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertEqual(response.status_code, 200) + response = self.client.get(workspace.get_absolute_url()) + self.assertIn("group_sharing_table", response.context_data) + self.assertEqual(len(response.context_data["group_sharing_table"].rows), 2) - def test_access_with_limited_view_permission(self): - """Raises permission denied if user has limited view permission.""" + def test_group_sharing_table_view_permission(self): + """Workspace-group sharing table is not present in context when user has view permission only.""" user = User.objects.create_user(username="test-limited", password="test-limited") user.user_permissions.add( Permission.objects.get(codename=models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME) ) - request = self.factory.get(self.get_url()) - request.user = user - with self.assertRaises(PermissionDenied): - views.ManagedGroupVisualization.as_view()(request) + workspace = factories.DefaultWorkspaceDataFactory.create() + self.client.force_login(user) + response = self.client.get(workspace.get_absolute_url()) + self.assertNotIn("group_sharing_table", response.context_data) + self.assertNotContains(response, "View groups that this workspace is shared with") - def test_view_status_code_with_existing_object_not_managed(self): - """Returns a successful status code for an existing object pk.""" + def test_shows_workspace_group_sharing_for_only_that_workspace(self): + """Only shows groups that this workspace has been shared with.""" + workspace = factories.DefaultWorkspaceDataFactory.create(workspace__name="workspace-1") + other_workspace = factories.WorkspaceFactory.create(name="workspace-2") + factories.WorkspaceGroupSharingFactory.create(workspace=other_workspace) self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertEqual(response.status_code, 200) + response = self.client.get(workspace.get_absolute_url()) + self.assertIn("group_sharing_table", response.context_data) + self.assertEqual(len(response.context_data["group_sharing_table"].rows), 0) - def test_no_groups(self): - """Visualization when there are no groups.""" + def test_auth_domain_table(self): + """The workspace auth domain table exists.""" + obj = factories.DefaultWorkspaceDataFactory.create() self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("graph", response.context_data) + response = self.client.get(obj.get_absolute_url()) + self.assertIn("authorization_domain_table", response.context_data) + self.assertIsInstance( + response.context_data["authorization_domain_table"], + tables.ManagedGroupStaffTable, + ) - def test_one_group(self): - """Visualization when there is one group.""" - factories.ManagedGroupFactory.create() + def test_auth_domain_table_none(self): + """No groups are shown if the workspace has no auth domains.""" + workspace = factories.DefaultWorkspaceDataFactory.create() self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("graph", response.context_data) + response = self.client.get(workspace.get_absolute_url()) + self.assertIn("authorization_domain_table", response.context_data) + self.assertEqual(len(response.context_data["authorization_domain_table"].rows), 0) - def test_two_groups(self): - """Visualization when there are two groups.""" - factories.ManagedGroupFactory.create(name="g1") - factories.ManagedGroupFactory.create(name="g2") + def test_auth_domain_table_one(self): + """One group is shown if the workspace has one auth domain.""" + workspace = factories.DefaultWorkspaceDataFactory.create() + group = factories.ManagedGroupFactory.create() + workspace.workspace.authorization_domains.add(group) self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("graph", response.context_data) + response = self.client.get(workspace.get_absolute_url()) + self.assertIn("authorization_domain_table", response.context_data) + table = response.context_data["authorization_domain_table"] + self.assertEqual(len(table.rows), 1) + self.assertIn(group, table.data) - def test_group_visualization(self): - factories.ManagedGroupFactory.create() - grandparent = factories.ManagedGroupFactory.create() - parent_1 = factories.ManagedGroupFactory.create() - parent_2 = factories.ManagedGroupFactory.create() - child = factories.ManagedGroupFactory.create() - factories.GroupGroupMembershipFactory.create(parent_group=grandparent, child_group=parent_1) - factories.GroupGroupMembershipFactory.create(parent_group=grandparent, child_group=parent_2) - factories.GroupGroupMembershipFactory.create( - parent_group=parent_1, - child_group=child, - role=models.GroupGroupMembership.ADMIN, - ) + def test_auth_domain_table_two(self): + """Two groups are shown if the workspace has two auth domains.""" + workspace = factories.DefaultWorkspaceDataFactory.create() + group_1 = factories.ManagedGroupFactory.create(name="g1") + workspace.workspace.authorization_domains.add(group_1) + group_2 = factories.ManagedGroupFactory.create(name="g2") + workspace.workspace.authorization_domains.add(group_2) self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("graph", response.context_data) - + response = self.client.get(workspace.get_absolute_url()) + self.assertIn("authorization_domain_table", response.context_data) + table = response.context_data["authorization_domain_table"] + self.assertEqual(len(table.rows), 2) + self.assertIn(group_1, table.data) + self.assertIn(group_2, table.data) -class WorkspaceLandingPageTest(TestCase): - def setUp(self): - """Set up test class.""" - self.factory = RequestFactory() - # Create a user with view permission. - self.view_user = User.objects.create_user(username="test_view", password="view") - self.view_user.user_permissions.add( + def test_auth_domain_table_view_permission(self): + """Auth domain table has correct class when user has view permission only.""" + user = User.objects.create_user(username="test-limited", password="test-limited") + user.user_permissions.add( Permission.objects.get(codename=models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME) ) - # Create a user with staff view permission. - self.staff_view_user = User.objects.create_user(username="test_staff_view", password="view") - self.staff_view_user.user_permissions.add( - Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) - ) - # Create a user with edit permission. - self.edit_user = User.objects.create_user(username="test_edit", password="test") - self.edit_user.user_permissions.add( + workspace = factories.DefaultWorkspaceDataFactory.create() + factories.WorkspaceAuthorizationDomainFactory.create(workspace=workspace.workspace) + self.client.force_login(user) + response = self.client.get(workspace.get_absolute_url()) + self.assertIn("authorization_domain_table", response.context_data) + self.assertIsInstance(response.context_data["authorization_domain_table"], tables.ManagedGroupUserTable) + + def test_shows_auth_domains_for_only_that_workspace(self): + """Only shows auth domains for this workspace.""" + workspace = factories.DefaultWorkspaceDataFactory.create(workspace__name="workspace-1") + other_workspace = factories.WorkspaceFactory.create(name="workspace-2") + group = factories.ManagedGroupFactory.create() + other_workspace.authorization_domains.add(group) + self.client.force_login(self.user) + response = self.client.get(workspace.get_absolute_url()) + self.assertIn("authorization_domain_table", response.context_data) + self.assertEqual(len(response.context_data["authorization_domain_table"].rows), 0) + + def test_staff_edit_permission(self): + """Links in template when user has staff edit permission.""" + edit_user = User.objects.create_user(username="edit", password="test") + edit_user.user_permissions.add( Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME), Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME), ) - - def tearDown(self): - """Clean up after tests.""" - # Unregister all adapters. - workspace_adapter_registry._registry = {} - # Register the default adapter. - workspace_adapter_registry.register(DefaultWorkspaceAdapter) - super().tearDown() - - def get_url(self): - """Get the url for the view being tested.""" - return reverse("anvil_consortium_manager:workspaces:landing_page") - - def test_view_redirect_not_logged_in(self): - "View redirects to login view when user is not logged in." - # Need a client for redirects. - response = self.client.get(self.get_url()) - self.assertRedirects( + self.client.force_login(edit_user) + obj = factories.DefaultWorkspaceDataFactory.create() + response = self.client.get(obj.get_absolute_url()) + self.assertIn("show_edit_links", response.context_data) + self.assertTrue(response.context_data["show_edit_links"]) + self.assertContains( response, - resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url(), + reverse( + "anvil_consortium_manager:workspaces:delete", + kwargs={ + "billing_project_slug": obj.workspace.billing_project.name, + "workspace_slug": obj.workspace.name, + }, + ), ) - - def test_status_code_with_staff_view_permission(self): - """Returns successful response code if user has staff_view permission.""" - self.client.force_login(self.staff_view_user) - response = self.client.get(self.get_url()) - self.assertEqual(response.status_code, 200) - - def test_access_with_view_permission(self): - """Returns successful response code if user has view permission.""" - self.client.force_login(self.view_user) - response = self.client.get(self.get_url()) - self.assertEqual(response.status_code, 200) - - def test_staff_view_permission(self): - """Links to edit required do not appear in the page when user only has staff_view permission.""" - self.client.force_login(self.staff_view_user) - response = self.client.get(self.get_url()) - self.assertIn("show_edit_links", response.context_data) - self.assertFalse(response.context_data["show_edit_links"]) - self.assertNotContains( + # Billing project link + self.assertContains( response, reverse( - "anvil_consortium_manager:workspaces:import", - kwargs={"workspace_type": "workspace"}, + "anvil_consortium_manager:billing_projects:detail", + kwargs={ + "slug": obj.workspace.billing_project.name, + }, ), ) - self.assertNotContains( + # Action buttons + self.assertContains( response, reverse( - "anvil_consortium_manager:workspaces:new", - kwargs={"workspace_type": "workspace"}, + "anvil_consortium_manager:workspaces:update", + kwargs={ + "billing_project_slug": obj.workspace.billing_project.name, + "workspace_slug": obj.workspace.name, + }, ), ) self.assertContains( response, reverse( - "anvil_consortium_manager:workspaces:list", - kwargs={"workspace_type": "workspace"}, + "anvil_consortium_manager:workspaces:sharing:new", + kwargs={ + "billing_project_slug": obj.workspace.billing_project.name, + "workspace_slug": obj.workspace.name, + }, + ), + ) + self.assertContains( + response, + reverse( + "anvil_consortium_manager:workspaces:clone", + kwargs={ + "billing_project_slug": obj.workspace.billing_project.name, + "workspace_slug": obj.workspace.name, + "workspace_type": "workspace", + }, + ), + ) + self.assertContains( + response, + reverse( + "anvil_consortium_manager:auditor:workspaces:sharing:all", + kwargs={ + "billing_project_slug": obj.workspace.billing_project.name, + "workspace_slug": obj.workspace.name, + }, ), ) - def test_view_permission(self): - """Links to edit required do not appear in the page when user only has view permission.""" - self.client.force_login(self.view_user) - response = self.client.get(self.get_url()) + def test_staff_view_permission(self): + """Links in template when user has staff view permission.""" + view_user = User.objects.create_user(username="view", password="test") + view_user.user_permissions.add( + Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME), + ) + self.client.force_login(view_user) + obj = factories.DefaultWorkspaceDataFactory.create() + response = self.client.get(obj.get_absolute_url()) self.assertIn("show_edit_links", response.context_data) self.assertFalse(response.context_data["show_edit_links"]) self.assertNotContains( response, reverse( - "anvil_consortium_manager:workspaces:import", - kwargs={"workspace_type": "workspace"}, + "anvil_consortium_manager:workspaces:delete", + kwargs={ + "billing_project_slug": obj.workspace.billing_project.name, + "workspace_slug": obj.workspace.name, + }, ), ) - self.assertNotContains( + # Billing project link + self.assertContains( response, reverse( - "anvil_consortium_manager:workspaces:new", - kwargs={"workspace_type": "workspace"}, + "anvil_consortium_manager:billing_projects:detail", + kwargs={ + "slug": obj.workspace.billing_project.name, + }, ), ) - self.assertContains( + # Action buttons + self.assertNotContains( response, reverse( - "anvil_consortium_manager:workspaces:list", - kwargs={"workspace_type": "workspace"}, + "anvil_consortium_manager:workspaces:update", + kwargs={ + "billing_project_slug": obj.workspace.billing_project.name, + "workspace_slug": obj.workspace.name, + }, ), ) - - def test_edit_permission(self): - """Links to edit required appear in the page when user also has edit permission.""" - self.client.force_login(self.edit_user) - response = self.client.get(self.get_url()) - self.assertIn("show_edit_links", response.context_data) - self.assertTrue(response.context_data["show_edit_links"]) - self.assertContains( + self.assertNotContains( response, reverse( - "anvil_consortium_manager:workspaces:import", - kwargs={"workspace_type": "workspace"}, + "anvil_consortium_manager:workspaces:sharing:new", + kwargs={ + "billing_project_slug": obj.workspace.billing_project.name, + "workspace_slug": obj.workspace.name, + }, ), ) - self.assertContains( + self.assertNotContains( response, reverse( - "anvil_consortium_manager:workspaces:new", - kwargs={"workspace_type": "workspace"}, + "anvil_consortium_manager:workspaces:clone", + kwargs={ + "billing_project_slug": obj.workspace.billing_project.name, + "workspace_slug": obj.workspace.name, + "workspace_type": "workspace", + }, ), ) self.assertContains( response, reverse( - "anvil_consortium_manager:workspaces:list", - kwargs={"workspace_type": "workspace"}, + "anvil_consortium_manager:auditor:workspaces:sharing:all", + kwargs={ + "billing_project_slug": obj.workspace.billing_project.name, + "workspace_slug": obj.workspace.name, + }, ), ) - def test_one_registered_workspace_in_context(self): - """One registered workspace in context when only DefaultWorkspaceAdapter is registered""" - self.client.force_login(self.view_user) - response = self.client.get(self.get_url()) - self.assertIn("registered_workspace_adapters", response.context_data) - self.assertEqual(len(response.context_data["registered_workspace_adapters"]), 1) - - def test_two_registered_workspaces_in_context(self): - """Two registered workspaces in context when two workspace adapters are registered""" - workspace_adapter_registry.register(TestWorkspaceAdapter) - self.client.force_login(self.view_user) - response = self.client.get(self.get_url()) - self.assertIn("registered_workspace_adapters", response.context_data) - self.assertEqual(len(response.context_data["registered_workspace_adapters"]), 2) - - -class WorkspaceDetailTest(TestCase): - """Tests for the WorkspaceDetail view.""" - - def setUp(self): - """Set up test class.""" - self.factory = RequestFactory() - # Create a user with both view and edit permission. - self.user = User.objects.create_user(username="test", password="test") - self.user.user_permissions.add( - Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) - ) - - def tearDown(self): - """Clean up after tests.""" - # Unregister all adapters. - workspace_adapter_registry._registry = {} - # Register the default adapter. - workspace_adapter_registry.register(DefaultWorkspaceAdapter) - super().tearDown() - - def get_view(self): - """Return the view being tested.""" - return views.WorkspaceDetail.as_view() - - def get_url(self, *args): - """Get the url for the view being tested.""" - return reverse("anvil_consortium_manager:workspaces:detail", args=args) - - def test_view_redirect_not_logged_in(self): - "View redirects to login view when user is not logged in." - # Need a client for redirects. - url = reverse("anvil_consortium_manager:workspaces:detail", args=["foo1", "foo2"]) - response = self.client.get(url) - self.assertRedirects(response, resolve_url(settings.LOGIN_URL) + "?next=" + url) - - def test_status_code_with_staff_view_permission(self): - """Returns successful response code.""" - obj = factories.DefaultWorkspaceDataFactory.create() - self.client.force_login(self.user) - response = self.client.get(obj.get_absolute_url()) - self.assertEqual(response.status_code, 200) - - def test_access_with_view_permission(self): - """Raises permission denied if user has limited view permission.""" - user = User.objects.create_user(username="test-limited", password="test-limited") - user.user_permissions.add( - Permission.objects.get(codename=models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME) - ) - obj = factories.DefaultWorkspaceDataFactory.create() - self.client.force_login(user) - response = self.client.get(obj.get_absolute_url()) - self.assertEqual(response.status_code, 200) - - def test_access_without_user_permission(self): - """Raises permission denied if user has no permissions.""" - user_no_perms = User.objects.create_user(username="test-none", password="test-none") - request = self.factory.get(self.get_url("foo", "bar")) - request.user = user_no_perms - with self.assertRaises(PermissionDenied): - self.get_view()(request) - - def test_view_status_code_with_invalid_pk(self): - """Raises a 404 error with an invalid object pk.""" - obj = factories.DefaultWorkspaceDataFactory.create() - request = self.factory.get(obj.get_absolute_url()) - request.user = self.user - with self.assertRaises(Http404): - self.get_view()(request, billing_project_slug="foo1", workspace_slug="foo2") - - def test_context_workspace_data(self): - """The view adds the workspace_data object to the context.""" - obj = factories.DefaultWorkspaceDataFactory.create() - self.client.force_login(self.user) - response = self.client.get(obj.get_absolute_url()) - response.context_data - self.assertIn("workspace_data_object", response.context_data) - self.assertEqual(response.context_data["workspace_data_object"], obj) - - def test_group_sharing_table(self): - """The workspace group access table exists.""" - obj = factories.DefaultWorkspaceDataFactory.create() - self.client.force_login(self.user) - response = self.client.get(obj.get_absolute_url()) - self.assertIn("group_sharing_table", response.context_data) - self.assertIsInstance( - response.context_data["group_sharing_table"], - tables.WorkspaceGroupSharingStaffTable, - ) - - def test_group_sharing_table_none(self): - """No groups are shown if the workspace has not been shared with any groups.""" - workspace = factories.DefaultWorkspaceDataFactory.create() - self.client.force_login(self.user) - response = self.client.get(workspace.get_absolute_url()) - self.assertIn("group_sharing_table", response.context_data) - self.assertEqual(len(response.context_data["group_sharing_table"].rows), 0) - - def test_group_sharing_table_one(self): - """One group is shown if the workspace has been shared with one group.""" - workspace = factories.DefaultWorkspaceDataFactory.create() - factories.WorkspaceGroupSharingFactory.create(workspace=workspace.workspace) - self.client.force_login(self.user) - response = self.client.get(workspace.get_absolute_url()) - self.assertIn("group_sharing_table", response.context_data) - self.assertEqual(len(response.context_data["group_sharing_table"].rows), 1) - - def test_group_sharing_table_two(self): - """Two groups are shown if the workspace has been shared with two groups.""" - workspace = factories.DefaultWorkspaceDataFactory.create() - factories.WorkspaceGroupSharingFactory.create(group__name="g1", workspace=workspace.workspace) - factories.WorkspaceGroupSharingFactory.create(group__name="g2", workspace=workspace.workspace) - self.client.force_login(self.user) - response = self.client.get(workspace.get_absolute_url()) - self.assertIn("group_sharing_table", response.context_data) - self.assertEqual(len(response.context_data["group_sharing_table"].rows), 2) - - def test_group_sharing_table_view_permission(self): - """Workspace-group sharing table is not present in context when user has view permission only.""" - user = User.objects.create_user(username="test-limited", password="test-limited") - user.user_permissions.add( - Permission.objects.get(codename=models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME) - ) - workspace = factories.DefaultWorkspaceDataFactory.create() - self.client.force_login(user) - response = self.client.get(workspace.get_absolute_url()) - self.assertNotIn("group_sharing_table", response.context_data) - self.assertNotContains(response, "View groups that this workspace is shared with") - - def test_shows_workspace_group_sharing_for_only_that_workspace(self): - """Only shows groups that this workspace has been shared with.""" - workspace = factories.DefaultWorkspaceDataFactory.create(workspace__name="workspace-1") - other_workspace = factories.WorkspaceFactory.create(name="workspace-2") - factories.WorkspaceGroupSharingFactory.create(workspace=other_workspace) - self.client.force_login(self.user) - response = self.client.get(workspace.get_absolute_url()) - self.assertIn("group_sharing_table", response.context_data) - self.assertEqual(len(response.context_data["group_sharing_table"].rows), 0) - - def test_auth_domain_table(self): - """The workspace auth domain table exists.""" - obj = factories.DefaultWorkspaceDataFactory.create() - self.client.force_login(self.user) - response = self.client.get(obj.get_absolute_url()) - self.assertIn("authorization_domain_table", response.context_data) - self.assertIsInstance( - response.context_data["authorization_domain_table"], - tables.ManagedGroupStaffTable, - ) - - def test_auth_domain_table_none(self): - """No groups are shown if the workspace has no auth domains.""" - workspace = factories.DefaultWorkspaceDataFactory.create() - self.client.force_login(self.user) - response = self.client.get(workspace.get_absolute_url()) - self.assertIn("authorization_domain_table", response.context_data) - self.assertEqual(len(response.context_data["authorization_domain_table"].rows), 0) - - def test_auth_domain_table_one(self): - """One group is shown if the workspace has one auth domain.""" - workspace = factories.DefaultWorkspaceDataFactory.create() - group = factories.ManagedGroupFactory.create() - workspace.workspace.authorization_domains.add(group) - self.client.force_login(self.user) - response = self.client.get(workspace.get_absolute_url()) - self.assertIn("authorization_domain_table", response.context_data) - table = response.context_data["authorization_domain_table"] - self.assertEqual(len(table.rows), 1) - self.assertIn(group, table.data) - - def test_auth_domain_table_two(self): - """Two groups are shown if the workspace has two auth domains.""" - workspace = factories.DefaultWorkspaceDataFactory.create() - group_1 = factories.ManagedGroupFactory.create(name="g1") - workspace.workspace.authorization_domains.add(group_1) - group_2 = factories.ManagedGroupFactory.create(name="g2") - workspace.workspace.authorization_domains.add(group_2) - self.client.force_login(self.user) - response = self.client.get(workspace.get_absolute_url()) - self.assertIn("authorization_domain_table", response.context_data) - table = response.context_data["authorization_domain_table"] - self.assertEqual(len(table.rows), 2) - self.assertIn(group_1, table.data) - self.assertIn(group_2, table.data) - - def test_auth_domain_table_view_permission(self): - """Auth domain table has correct class when user has view permission only.""" - user = User.objects.create_user(username="test-limited", password="test-limited") - user.user_permissions.add( - Permission.objects.get(codename=models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME) - ) - workspace = factories.DefaultWorkspaceDataFactory.create() - factories.WorkspaceAuthorizationDomainFactory.create(workspace=workspace.workspace) - self.client.force_login(user) - response = self.client.get(workspace.get_absolute_url()) - self.assertIn("authorization_domain_table", response.context_data) - self.assertIsInstance(response.context_data["authorization_domain_table"], tables.ManagedGroupUserTable) - - def test_shows_auth_domains_for_only_that_workspace(self): - """Only shows auth domains for this workspace.""" - workspace = factories.DefaultWorkspaceDataFactory.create(workspace__name="workspace-1") - other_workspace = factories.WorkspaceFactory.create(name="workspace-2") - group = factories.ManagedGroupFactory.create() - other_workspace.authorization_domains.add(group) - self.client.force_login(self.user) - response = self.client.get(workspace.get_absolute_url()) - self.assertIn("authorization_domain_table", response.context_data) - self.assertEqual(len(response.context_data["authorization_domain_table"].rows), 0) - - def test_staff_edit_permission(self): - """Links in template when user has staff edit permission.""" - edit_user = User.objects.create_user(username="edit", password="test") - edit_user.user_permissions.add( - Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME), - Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME), + def test_view_permission(self): + """Links in template when user has view permission.""" + view_user = User.objects.create_user(username="view", password="test") + view_user.user_permissions.add( + Permission.objects.get(codename=models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME), ) - self.client.force_login(edit_user) + self.client.force_login(view_user) obj = factories.DefaultWorkspaceDataFactory.create() response = self.client.get(obj.get_absolute_url()) self.assertIn("show_edit_links", response.context_data) - self.assertTrue(response.context_data["show_edit_links"]) - self.assertContains( - response, - reverse( - "anvil_consortium_manager:workspaces:delete", - kwargs={ - "billing_project_slug": obj.workspace.billing_project.name, - "workspace_slug": obj.workspace.name, - }, - ), - ) + self.assertFalse(response.context_data["show_edit_links"]) # Billing project link - self.assertContains( + self.assertNotContains( response, reverse( "anvil_consortium_manager:billing_projects:detail", @@ -7147,158 +6428,20 @@ def test_staff_edit_permission(self): ), ) # Action buttons - self.assertContains( + self.assertNotContains( response, reverse( - "anvil_consortium_manager:workspaces:update", + "anvil_consortium_manager:workspaces:delete", kwargs={ "billing_project_slug": obj.workspace.billing_project.name, "workspace_slug": obj.workspace.name, }, ), ) - self.assertContains( + self.assertNotContains( response, reverse( - "anvil_consortium_manager:workspaces:sharing:new", - kwargs={ - "billing_project_slug": obj.workspace.billing_project.name, - "workspace_slug": obj.workspace.name, - }, - ), - ) - self.assertContains( - response, - reverse( - "anvil_consortium_manager:workspaces:clone", - kwargs={ - "billing_project_slug": obj.workspace.billing_project.name, - "workspace_slug": obj.workspace.name, - "workspace_type": "workspace", - }, - ), - ) - self.assertContains( - response, - reverse( - "anvil_consortium_manager:workspaces:audit_sharing", - kwargs={ - "billing_project_slug": obj.workspace.billing_project.name, - "workspace_slug": obj.workspace.name, - }, - ), - ) - - def test_staff_view_permission(self): - """Links in template when user has staff view permission.""" - view_user = User.objects.create_user(username="view", password="test") - view_user.user_permissions.add( - Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME), - ) - self.client.force_login(view_user) - obj = factories.DefaultWorkspaceDataFactory.create() - response = self.client.get(obj.get_absolute_url()) - self.assertIn("show_edit_links", response.context_data) - self.assertFalse(response.context_data["show_edit_links"]) - self.assertNotContains( - response, - reverse( - "anvil_consortium_manager:workspaces:delete", - kwargs={ - "billing_project_slug": obj.workspace.billing_project.name, - "workspace_slug": obj.workspace.name, - }, - ), - ) - # Billing project link - self.assertContains( - response, - reverse( - "anvil_consortium_manager:billing_projects:detail", - kwargs={ - "slug": obj.workspace.billing_project.name, - }, - ), - ) - # Action buttons - self.assertNotContains( - response, - reverse( - "anvil_consortium_manager:workspaces:update", - kwargs={ - "billing_project_slug": obj.workspace.billing_project.name, - "workspace_slug": obj.workspace.name, - }, - ), - ) - self.assertNotContains( - response, - reverse( - "anvil_consortium_manager:workspaces:sharing:new", - kwargs={ - "billing_project_slug": obj.workspace.billing_project.name, - "workspace_slug": obj.workspace.name, - }, - ), - ) - self.assertNotContains( - response, - reverse( - "anvil_consortium_manager:workspaces:clone", - kwargs={ - "billing_project_slug": obj.workspace.billing_project.name, - "workspace_slug": obj.workspace.name, - "workspace_type": "workspace", - }, - ), - ) - self.assertContains( - response, - reverse( - "anvil_consortium_manager:workspaces:audit_sharing", - kwargs={ - "billing_project_slug": obj.workspace.billing_project.name, - "workspace_slug": obj.workspace.name, - }, - ), - ) - - def test_view_permission(self): - """Links in template when user has view permission.""" - view_user = User.objects.create_user(username="view", password="test") - view_user.user_permissions.add( - Permission.objects.get(codename=models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME), - ) - self.client.force_login(view_user) - obj = factories.DefaultWorkspaceDataFactory.create() - response = self.client.get(obj.get_absolute_url()) - self.assertIn("show_edit_links", response.context_data) - self.assertFalse(response.context_data["show_edit_links"]) - # Billing project link - self.assertNotContains( - response, - reverse( - "anvil_consortium_manager:billing_projects:detail", - kwargs={ - "slug": obj.workspace.billing_project.name, - }, - ), - ) - # Action buttons - self.assertNotContains( - response, - reverse( - "anvil_consortium_manager:workspaces:delete", - kwargs={ - "billing_project_slug": obj.workspace.billing_project.name, - "workspace_slug": obj.workspace.name, - }, - ), - ) - self.assertNotContains( - response, - reverse( - "anvil_consortium_manager:workspaces:update", + "anvil_consortium_manager:workspaces:update", kwargs={ "billing_project_slug": obj.workspace.billing_project.name, "workspace_slug": obj.workspace.name, @@ -7329,7 +6472,7 @@ def test_view_permission(self): self.assertNotContains( response, reverse( - "anvil_consortium_manager:workspaces:audit_sharing", + "anvil_consortium_manager:auditor:workspaces:sharing:all", kwargs={ "billing_project_slug": obj.workspace.billing_project.name, "workspace_slug": obj.workspace.name, @@ -11787,726 +10930,321 @@ def test_view_with_filter_returns_one_object_exact(self): response = self.client.get(self.get_url(), {"name__icontains": "workspace1"}) self.assertEqual(response.status_code, 200) self.assertIn("table", response.context_data) - self.assertEqual(len(response.context_data["table"].rows), 1) - self.assertIn(instance, response.context_data["table"].data) - - def test_view_with_filter_returns_one_object_case_insensitive(self): - instance = factories.WorkspaceFactory.create(name="workspace1") - factories.WorkspaceFactory.create(name="workspace2") - self.client.force_login(self.view_user) - response = self.client.get(self.get_url(), {"name__icontains": "Workspace1"}) - self.assertEqual(response.status_code, 200) - self.assertIn("table", response.context_data) - self.assertEqual(len(response.context_data["table"].rows), 1) - self.assertIn(instance, response.context_data["table"].data) - - def test_view_with_filter_returns_one_object_case_contains(self): - instance = factories.WorkspaceFactory.create(name="workspace1") - factories.WorkspaceFactory.create(name="workspace2") - self.client.force_login(self.view_user) - response = self.client.get(self.get_url(), {"name__icontains": "orkspace1"}) - self.assertEqual(response.status_code, 200) - self.assertIn("table", response.context_data) - self.assertEqual(len(response.context_data["table"].rows), 1) - self.assertIn(instance, response.context_data["table"].data) - - def test_view_with_filter_returns_mutiple_objects(self): - factories.WorkspaceFactory.create(name="workspace1") - factories.WorkspaceFactory.create(name="wOrkspace1") - self.client.force_login(self.view_user) - response = self.client.get(self.get_url(), {"name__icontains": "Workspace"}) - self.assertEqual(response.status_code, 200) - self.assertIn("table", response.context_data) - self.assertEqual(len(response.context_data["table"].rows), 2) - - -class WorkspaceListByTypeTest(TestCase): - def setUp(self): - """Set up test class.""" - self.factory = RequestFactory() - # Create a user with staff view permission. - self.staff_view_user = User.objects.create_user(username="test-staff-view", password="test") - self.staff_view_user.user_permissions.add( - Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) - ) - # Create a user with view permission - self.view_user = User.objects.create_user(username="test-view", password="test") - self.view_user.user_permissions.add( - Permission.objects.get(codename=models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME) - ) - self.workspace_type = DefaultWorkspaceAdapter().get_type() - - def tearDown(self): - """Clean up after tests.""" - # Unregister all adapters. - workspace_adapter_registry._registry = {} - # Register the default adapter. - workspace_adapter_registry.register(DefaultWorkspaceAdapter) - super().tearDown() - - def get_url(self, *args): - """Get the url for the view being tested.""" - return reverse("anvil_consortium_manager:workspaces:list", args=args) - - def get_view(self): - """Return the view being tested.""" - return views.WorkspaceListByType.as_view() - - def test_view_redirect_not_logged_in(self): - "View redirects to login view when user is not logged in." - # Need a client for redirects. - response = self.client.get(self.get_url(self.workspace_type)) - self.assertRedirects( - response, - resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url(self.workspace_type), - ) - - def test_status_code_with_staff_view_permission(self): - """Returns successful response code.""" - self.client.force_login(self.staff_view_user) - response = self.client.get(self.get_url(self.workspace_type)) - self.assertEqual(response.status_code, 200) - - def test_access_with_view_permission(self): - """Raises permission denied if user has limited view permission.""" - self.client.force_login(self.view_user) - response = self.client.get(self.get_url(self.workspace_type)) - self.assertEqual(response.status_code, 200) - - def test_access_without_user_permission(self): - """Raises permission denied if user has no permissions.""" - user_no_perms = User.objects.create_user(username="test-none", password="test-none") - request = self.factory.get(self.get_url(self.workspace_type)) - request.user = user_no_perms - with self.assertRaises(PermissionDenied): - self.get_view()(request, workspace_type=self.workspace_type) - - def test_get_workspace_type_not_registered(self): - """Raises 404 with get request if workspace type is not registered with adapter.""" - request = self.factory.get(self.get_url("foo")) - request.user = self.view_user - with self.assertRaises(Http404): - self.get_view()(request, workspace_type="foo") - - def test_view_has_correct_table_class_staff_view(self): - self.client.force_login(self.staff_view_user) - response = self.client.get(self.get_url(self.workspace_type)) - self.assertIn("table", response.context_data) - self.assertIsInstance(response.context_data["table"], tables.WorkspaceStaffTable) - - def test_view_has_correct_table_class_view(self): - self.client.force_login(self.view_user) - response = self.client.get(self.get_url(self.workspace_type)) - self.assertIn("table", response.context_data) - self.assertIsInstance(response.context_data["table"], tables.WorkspaceUserTable) - - def test_view_with_no_objects(self): - self.client.force_login(self.view_user) - response = self.client.get(self.get_url(self.workspace_type)) - self.assertEqual(response.status_code, 200) - self.assertIn("table", response.context_data) - self.assertEqual(len(response.context_data["table"].rows), 0) - - def test_view_with_one_object(self): - factories.WorkspaceFactory() - self.client.force_login(self.view_user) - response = self.client.get(self.get_url(self.workspace_type)) - self.assertEqual(response.status_code, 200) - self.assertIn("table", response.context_data) - self.assertEqual(len(response.context_data["table"].rows), 1) - - def test_view_with_two_objects(self): - factories.WorkspaceFactory.create(name="w1") - factories.WorkspaceFactory.create(name="w2") - self.client.force_login(self.view_user) - response = self.client.get(self.get_url(self.workspace_type)) - self.assertEqual(response.status_code, 200) - self.assertIn("table", response.context_data) - self.assertEqual(len(response.context_data["table"].rows), 2) - - def test_adapter_table_class_staff_view(self): - """Displays the correct table if specified in the adapter.""" - # Overriding settings doesn't work, because appconfig.ready has already run and - # registered the default adapter. Instead, unregister the default and register the - # new adapter here. - workspace_adapter_registry.unregister(DefaultWorkspaceAdapter) - workspace_adapter_registry.register(TestWorkspaceAdapter) - self.workspace_type = TestWorkspaceAdapter().get_type() - self.client.force_login(self.staff_view_user) - response = self.client.get(self.get_url(self.workspace_type)) - self.assertIn("table", response.context_data) - self.assertIsInstance(response.context_data["table"], app_tables.TestWorkspaceDataStaffTable) - - def test_adapter_table_class_view(self): - """Displays the correct table if specified in the adapter.""" - # Overriding settings doesn't work, because appconfig.ready has already run and - # registered the default adapter. Instead, unregister the default and register the - # new adapter here. - workspace_adapter_registry.unregister(DefaultWorkspaceAdapter) - workspace_adapter_registry.register(TestWorkspaceAdapter) - self.workspace_type = TestWorkspaceAdapter().get_type() - self.client.force_login(self.view_user) - response = self.client.get(self.get_url(self.workspace_type)) - self.assertIn("table", response.context_data) - self.assertIsInstance(response.context_data["table"], app_tables.TestWorkspaceDataUserTable) - - def test_only_shows_workspaces_with_correct_type(self): - """Only workspaces with the same workspace_type are shown in the table.""" - workspace_adapter_registry.register(TestWorkspaceAdapter) - factories.WorkspaceFactory(workspace_type=TestWorkspaceAdapter().get_type()) - default_type = DefaultWorkspaceAdapter().get_type() - self.client.force_login(self.view_user) - response = self.client.get(self.get_url(default_type)) - self.assertEqual(response.status_code, 200) - self.assertIn("table", response.context_data) - self.assertEqual(len(response.context_data["table"].rows), 0) - - def test_view_with_filter_return_no_object(self): - factories.WorkspaceFactory.create(name="workspace1") - factories.WorkspaceFactory.create(name="workspace2") - self.client.force_login(self.view_user) - response = self.client.get(self.get_url(self.workspace_type), {"name__icontains": "abc"}) - self.assertEqual(response.status_code, 200) - self.assertIn("table", response.context_data) - self.assertEqual(len(response.context_data["table"].rows), 0) - - def test_view_with_filter_returns_one_object_exact(self): - instance = factories.WorkspaceFactory.create(name="workspace1") - factories.WorkspaceFactory.create(name="workspace2") - self.client.force_login(self.view_user) - response = self.client.get(self.get_url(self.workspace_type), {"name__icontains": "workspace1"}) - self.assertEqual(response.status_code, 200) - self.assertIn("table", response.context_data) - self.assertEqual(len(response.context_data["table"].rows), 1) - self.assertIn(instance, response.context_data["table"].data) - - def test_view_with_filter_returns_one_object_case_insensitive(self): - instance = factories.WorkspaceFactory.create(name="workspace1") - factories.WorkspaceFactory.create(name="workspace2") - self.client.force_login(self.view_user) - response = self.client.get(self.get_url(self.workspace_type), {"name__icontains": "Workspace1"}) - self.assertEqual(response.status_code, 200) - self.assertIn("table", response.context_data) - self.assertEqual(len(response.context_data["table"].rows), 1) - self.assertIn(instance, response.context_data["table"].data) - - def test_view_with_filter_returns_one_object_case_contains(self): - instance = factories.WorkspaceFactory.create(name="workspace1") - factories.WorkspaceFactory.create(name="workspace2") - self.client.force_login(self.view_user) - response = self.client.get(self.get_url(self.workspace_type), {"name__icontains": "orkspace1"}) - self.assertEqual(response.status_code, 200) - self.assertIn("table", response.context_data) - self.assertEqual(len(response.context_data["table"].rows), 1) - self.assertIn(instance, response.context_data["table"].data) - - def test_view_with_filter_workspace_type(self): - instance = factories.WorkspaceFactory.create(name="workspace1") - factories.WorkspaceFactory.create(name="workspace2", workspace_type=TestWorkspaceAdapter().get_type()) - self.client.force_login(self.view_user) - response = self.client.get(self.get_url(self.workspace_type), {"name__icontains": "workspace"}) - self.assertEqual(response.status_code, 200) - self.assertIn("table", response.context_data) - self.assertEqual(len(response.context_data["table"].rows), 1) - self.assertIn(instance, response.context_data["table"].data) - - def test_view_with_filter_returns_mutiple_objects(self): - factories.WorkspaceFactory.create(name="workspace1") - factories.WorkspaceFactory.create(name="wOrkspace1") - self.client.force_login(self.view_user) - response = self.client.get(self.get_url(self.workspace_type), {"name__icontains": "Workspace"}) - self.assertEqual(response.status_code, 200) - self.assertIn("table", response.context_data) - self.assertEqual(len(response.context_data["table"].rows), 2) - - def test_view_with_default_adapter_use_default_workspace_list_template(self): - default_type = DefaultWorkspaceAdapter().get_type() - self.client.force_login(self.view_user) - response = self.client.get(self.get_url(default_type)) - self.assertTemplateUsed(response, "anvil_consortium_manager/workspace_list.html") - - def test_view_with_custom_adapter_use_custom_workspace_list_template(self): - workspace_adapter_registry.unregister(DefaultWorkspaceAdapter) - workspace_adapter_registry.register(TestWorkspaceAdapter) - self.workspace_type = TestWorkspaceAdapter().get_type() - self.client.force_login(self.view_user) - response = self.client.get(self.get_url(self.workspace_type)) - self.assertTemplateUsed(response, "test_workspace_list.html") - - -class WorkspaceDeleteTest(AnVILAPIMockTestMixin, TestCase): - api_success_code = 202 - - def setUp(self): - """Set up test class.""" - # The superclass uses the responses package to mock API responses. - super().setUp() - self.factory = RequestFactory() - # Create a user with both view and edit permissions. - self.user = User.objects.create_user(username="test", password="test") - self.user.user_permissions.add( - Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) - ) - self.user.user_permissions.add( - Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME) - ) - - def tearDown(self): - """Clean up after tests.""" - # Unregister all adapters. - workspace_adapter_registry._registry = {} - # Register the default adapter. - workspace_adapter_registry.register(DefaultWorkspaceAdapter) - super().tearDown() - - def get_url(self, *args): - """Get the url for the view being tested.""" - return reverse("anvil_consortium_manager:workspaces:delete", args=args) - - def get_api_url(self, billing_project_name, workspace_name): - return self.api_client.rawls_entry_point + "/api/workspaces/" + billing_project_name + "/" + workspace_name - - def get_view(self): - """Return the view being tested.""" - return views.WorkspaceDelete.as_view() - - def test_view_redirect_not_logged_in(self): - "View redirects to login view when user is not logged in." - # Need a client for redirects. - url = self.get_url("foo1", "foo2") - response = self.client.get(url) - self.assertRedirects(response, resolve_url(settings.LOGIN_URL) + "?next=" + url) - - def test_status_code_with_user_permission(self): - """Returns successful response code.""" - obj = factories.WorkspaceFactory.create() - self.client.force_login(self.user) - response = self.client.get(self.get_url(obj.billing_project.name, obj.name)) - self.assertEqual(response.status_code, 200) - - def test_access_with_view_permission(self): - """Raises permission denied if user has only view permission.""" - user_with_view_perm = User.objects.create_user(username="test-other", password="test-other") - user_with_view_perm.user_permissions.add( - Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) - ) - request = self.factory.get(self.get_url("foo1", "foo2")) - request.user = user_with_view_perm - with self.assertRaises(PermissionDenied): - self.get_view()(request, pk=1) - - def test_access_with_limited_view_permission(self): - """Raises permission denied if user has limited view permission.""" - user = User.objects.create_user(username="test-limited", password="test-limited") - user.user_permissions.add( - Permission.objects.get(codename=models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME) - ) - request = self.factory.get(self.get_url("foo", "bar")) - request.user = user - with self.assertRaises(PermissionDenied): - self.get_view()(request) - - def test_access_without_user_permission(self): - """Raises permission denied if user has no permissions.""" - user_no_perms = User.objects.create_user(username="test-none", password="test-none") - request = self.factory.get(self.get_url("foo1", "foo2")) - request.user = user_no_perms - with self.assertRaises(PermissionDenied): - self.get_view()(request, pk=1) - - def test_view_with_invalid_pk(self): - """Returns a 404 when the object doesn't exist.""" - request = self.factory.get(self.get_url("foo1", "foo2")) - request.user = self.user - with self.assertRaises(Http404): - self.get_view()(request, pk=1) - - def test_view_deletes_object(self): - """Posting submit to the form successfully deletes the object.""" - billing_project = factories.BillingProjectFactory.create(name="test-billing-project") - object = factories.WorkspaceFactory.create(billing_project=billing_project, name="test-workspace") - api_url = self.get_api_url(object.billing_project.name, object.name) - self.anvil_response_mock.add(responses.DELETE, api_url, status=self.api_success_code) - self.client.force_login(self.user) - response = self.client.post(self.get_url(object.billing_project.name, object.name), {"submit": ""}) - self.assertEqual(response.status_code, 302) - self.assertEqual(models.Workspace.objects.count(), 0) - # History is added. - self.assertEqual(object.history.count(), 2) - self.assertEqual(object.history.latest().history_type, "-") - - def test_success_message(self): - """Response includes a success message if successful.""" - billing_project = factories.BillingProjectFactory.create(name="test-billing-project") - object = factories.WorkspaceFactory.create(billing_project=billing_project, name="test-workspace") - api_url = self.get_api_url(object.billing_project.name, object.name) - self.anvil_response_mock.add(responses.DELETE, api_url, status=self.api_success_code) - self.client.force_login(self.user) - response = self.client.post( - self.get_url(object.billing_project.name, object.name), - {"submit": ""}, - follow=True, - ) - messages = [m.message for m in get_messages(response.wsgi_request)] - self.assertEqual(len(messages), 1) - self.assertEqual(views.WorkspaceDelete.success_message, str(messages[0])) - - def test_only_deletes_specified_pk(self): - """View only deletes the specified pk.""" - object = factories.WorkspaceFactory.create() - other_object = factories.WorkspaceFactory.create() - api_url = self.get_api_url(object.billing_project.name, object.name) - self.anvil_response_mock.add(responses.DELETE, api_url, status=self.api_success_code) - self.client.force_login(self.user) - response = self.client.post(self.get_url(object.billing_project.name, object.name), {"submit": ""}) - self.assertEqual(response.status_code, 302) - self.assertEqual(models.Workspace.objects.count(), 1) - self.assertQuerySetEqual( - models.Workspace.objects.all(), - models.Workspace.objects.filter(pk=other_object.pk), - ) - - def test_can_delete_workspace_with_auth_domain(self): - """A workspace can be deleted if it has an auth domain, and the auth domain group is not deleted.""" - billing_project = factories.BillingProjectFactory.create(name="test-billing-project") - object = factories.WorkspaceFactory.create(billing_project=billing_project, name="test-workspace") - auth_domain = factories.ManagedGroupFactory.create(name="test-group") - wad = models.WorkspaceAuthorizationDomain.objects.create(workspace=object, group=auth_domain) - # object.authorization_domains.add(auth_domain) - api_url = self.get_api_url(object.billing_project.name, object.name) - self.anvil_response_mock.add(responses.DELETE, api_url, status=self.api_success_code) - self.client.force_login(self.user) - response = self.client.post(self.get_url(object.billing_project.name, object.name), {"submit": ""}) - self.assertEqual(response.status_code, 302) - self.assertEqual(models.Workspace.objects.count(), 0) - self.assertEqual(models.WorkspaceAuthorizationDomain.objects.count(), 0) - # The auth domain group still exists. - self.assertEqual(models.ManagedGroup.objects.count(), 1) - models.ManagedGroup.objects.get(pk=auth_domain.pk) - # History is added for workspace. - self.assertEqual(object.history.count(), 2) - self.assertEqual(object.history.latest().history_type, "-") - # History is added for auth domain. - self.assertEqual(wad.history.count(), 2) - self.assertEqual(wad.history.latest().history_type, "-") - - def test_can_delete_workspace_that_has_been_shared_with_group(self): - """A workspace can be deleted if it has been shared with a group, and the group is not deleted.""" - billing_project = factories.BillingProjectFactory.create(name="test-billing-project") - object = factories.WorkspaceFactory.create(billing_project=billing_project, name="test-workspace") - group = factories.ManagedGroupFactory.create(name="test-group") - factories.WorkspaceGroupSharingFactory.create(workspace=object, group=group) - api_url = self.get_api_url(object.billing_project.name, object.name) - self.anvil_response_mock.add(responses.DELETE, api_url, status=self.api_success_code) - self.client.force_login(self.user) - response = self.client.post(self.get_url(object.billing_project.name, object.name), {"submit": ""}) - self.assertEqual(response.status_code, 302) - self.assertEqual(models.Workspace.objects.count(), 0) - self.assertEqual(models.WorkspaceGroupSharing.objects.count(), 0) - # The group still exists. - self.assertEqual(models.ManagedGroup.objects.count(), 1) - models.ManagedGroup.objects.get(pk=group.pk) - # History is added for workspace. - self.assertEqual(object.history.count(), 2) - self.assertEqual(object.history.latest().history_type, "-") - # History is added for WorkspaceGroupSharing. - self.assertEqual(models.WorkspaceGroupSharing.history.count(), 2) - self.assertEqual(models.WorkspaceGroupSharing.history.latest().history_type, "-") - - def test_success_url(self): - """Redirects to the expected page.""" - object = factories.WorkspaceFactory.create() - # Need to use the client instead of RequestFactory to check redirection url. - api_url = self.get_api_url(object.billing_project.name, object.name) - self.anvil_response_mock.add(responses.DELETE, api_url, status=self.api_success_code) - self.client.force_login(self.user) - response = self.client.post(self.get_url(object.billing_project.name, object.name), {"submit": ""}) - self.assertEqual(response.status_code, 302) - self.assertRedirects( - response, - reverse( - "anvil_consortium_manager:workspaces:list", - args=[DefaultWorkspaceAdapter().get_type()], - ), - ) - - def test_adapter_success_url(self): - """Redirects to the expected page.""" - # Register a new adapter. - workspace_adapter_registry.register(TestWorkspaceAdapter) - object = factories.WorkspaceFactory.create(workspace_type=TestWorkspaceAdapter().get_type()) - # Need to use the client instead of RequestFactory to check redirection url. - api_url = self.get_api_url(object.billing_project.name, object.name) - self.anvil_response_mock.add(responses.DELETE, api_url, status=self.api_success_code) - self.client.force_login(self.user) - response = self.client.post(self.get_url(object.billing_project.name, object.name), {"submit": ""}) - self.assertEqual(response.status_code, 302) - self.assertRedirects( - response, - reverse( - "anvil_consortium_manager:workspaces:list", - args=[TestWorkspaceAdapter().get_type()], - ), - ) - - def test_api_error(self): - """Shows a message if an AnVIL API error occurs.""" - # Need a client to check messages. - object = factories.WorkspaceFactory.create() - api_url = self.get_api_url(object.billing_project.name, object.name) - self.anvil_response_mock.add( - responses.DELETE, - api_url, - status=500, - json={"message": "workspace delete test error"}, - ) - self.client.force_login(self.user) - response = self.client.post(self.get_url(object.billing_project.name, object.name), {"submit": ""}) - self.assertEqual(response.status_code, 200) - messages = [m.message for m in get_messages(response.wsgi_request)] - self.assertEqual(len(messages), 1) - self.assertIn("AnVIL API Error: workspace delete test error", str(messages[0])) - # Make sure that the object still exists. - self.assertEqual(models.Workspace.objects.count(), 1) - - def test_post_does_not_delete_when_protected_fk_to_another_model(self): - """Workspace is not deleted when there is a protected foreign key reference to the workspace.""" - object = factories.DefaultWorkspaceDataFactory.create() - app_models.ProtectedWorkspace.objects.create(workspace=object.workspace) - self.client.force_login(self.user) - response = self.client.post( - self.get_url(object.workspace.billing_project.name, object.workspace.name), - {"submit": ""}, - follow=True, - ) - self.assertRedirects(response, object.get_absolute_url()) - # A message is added. - messages = list(response.context["messages"]) - self.assertEqual(len(messages), 1) - self.assertEqual( - views.WorkspaceDelete.message_could_not_delete_workspace_from_app, - str(messages[0]), - ) - # Make sure the group still exists. - self.assertEqual(models.Workspace.objects.count(), 1) - self.assertEqual(models.DefaultWorkspaceData.objects.count(), 1) - object.refresh_from_db() - - def test_post_does_not_delete_when_workspace_data_has_protected_fk_to_another_model( - self, - ): - """Workspace is not deleted when there is a protected foreign key reference to the workspace data.""" - workspace_data = factories.DefaultWorkspaceDataFactory() - object = workspace_data.workspace - app_models.ProtectedWorkspaceData.objects.create(workspace_data=workspace_data) - self.client.force_login(self.user) - response = self.client.post( - self.get_url(object.billing_project.name, object.name), - {"submit": ""}, - follow=True, - ) - self.assertRedirects(response, object.get_absolute_url()) - # A message is added. - messages = list(response.context["messages"]) - self.assertEqual(len(messages), 1) - self.assertEqual( - views.WorkspaceDelete.message_could_not_delete_workspace_from_app, - str(messages[0]), - ) - # Make sure the group still exists. - self.assertEqual(models.Workspace.objects.count(), 1) - object.refresh_from_db() + self.assertEqual(len(response.context_data["table"].rows), 1) + self.assertIn(instance, response.context_data["table"].data) - def test_get_is_locked(self): - """View redirects with a get request if the workspace is locked.""" - billing_project = factories.BillingProjectFactory.create(name="test-billing-project") - object = factories.DefaultWorkspaceDataFactory.create( - workspace__billing_project=billing_project, - workspace__name="test-workspace", - workspace__is_locked=True, - ) - self.client.force_login(self.user) - response = self.client.get( - self.get_url(object.workspace.billing_project.name, object.workspace.name), - follow=True, - ) - # Make sure the workspace still exists. - self.assertIn(object.workspace, models.Workspace.objects.all()) - self.assertIn(object, models.DefaultWorkspaceData.objects.all()) - # Redirects to detail page. - self.assertRedirects(response, object.get_absolute_url()) - # With a message. - messages = [m.message for m in get_messages(response.wsgi_request)] - self.assertEqual(len(messages), 1) - self.assertEqual(views.WorkspaceDelete.message_workspace_locked, str(messages[0])) + def test_view_with_filter_returns_one_object_case_insensitive(self): + instance = factories.WorkspaceFactory.create(name="workspace1") + factories.WorkspaceFactory.create(name="workspace2") + self.client.force_login(self.view_user) + response = self.client.get(self.get_url(), {"name__icontains": "Workspace1"}) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 1) + self.assertIn(instance, response.context_data["table"].data) - def test_post_is_locked(self): - """View redirects with a post request if the workspace is locked.""" - billing_project = factories.BillingProjectFactory.create(name="test-billing-project") - object = factories.DefaultWorkspaceDataFactory.create( - workspace__billing_project=billing_project, - workspace__name="test-workspace", - workspace__is_locked=True, - ) - self.client.force_login(self.user) - response = self.client.post( - self.get_url(object.workspace.billing_project.name, object.workspace.name), - {"submit": ""}, - follow=True, - ) - # Make sure the workspace still exists. - self.assertIn(object.workspace, models.Workspace.objects.all()) - self.assertIn(object, models.DefaultWorkspaceData.objects.all()) - # Redirects to detail page. - self.assertRedirects(response, object.get_absolute_url()) - # With a message. - messages = [m.message for m in get_messages(response.wsgi_request)] - self.assertEqual(len(messages), 1) - self.assertEqual(views.WorkspaceDelete.message_workspace_locked, str(messages[0])) + def test_view_with_filter_returns_one_object_case_contains(self): + instance = factories.WorkspaceFactory.create(name="workspace1") + factories.WorkspaceFactory.create(name="workspace2") + self.client.force_login(self.view_user) + response = self.client.get(self.get_url(), {"name__icontains": "orkspace1"}) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 1) + self.assertIn(instance, response.context_data["table"].data) + + def test_view_with_filter_returns_mutiple_objects(self): + factories.WorkspaceFactory.create(name="workspace1") + factories.WorkspaceFactory.create(name="wOrkspace1") + self.client.force_login(self.view_user) + response = self.client.get(self.get_url(), {"name__icontains": "Workspace"}) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 2) -class WorkspaceAutocompleteTest(TestCase): +class WorkspaceListByTypeTest(TestCase): def setUp(self): """Set up test class.""" self.factory = RequestFactory() - # Create a user with the correct permissions. - self.user = User.objects.create_user(username="test", password="test") - self.user.user_permissions.add( + # Create a user with staff view permission. + self.staff_view_user = User.objects.create_user(username="test-staff-view", password="test") + self.staff_view_user.user_permissions.add( Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) ) + # Create a user with view permission + self.view_user = User.objects.create_user(username="test-view", password="test") + self.view_user.user_permissions.add( + Permission.objects.get(codename=models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME) + ) + self.workspace_type = DefaultWorkspaceAdapter().get_type() + + def tearDown(self): + """Clean up after tests.""" + # Unregister all adapters. + workspace_adapter_registry._registry = {} + # Register the default adapter. + workspace_adapter_registry.register(DefaultWorkspaceAdapter) + super().tearDown() def get_url(self, *args): """Get the url for the view being tested.""" - return reverse("anvil_consortium_manager:workspaces:autocomplete", args=args) + return reverse("anvil_consortium_manager:workspaces:list", args=args) def get_view(self): """Return the view being tested.""" - return views.WorkspaceAutocomplete.as_view() + return views.WorkspaceListByType.as_view() def test_view_redirect_not_logged_in(self): "View redirects to login view when user is not logged in." # Need a client for redirects. - response = self.client.get(self.get_url()) - self.assertRedirects(response, resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url()) + response = self.client.get(self.get_url(self.workspace_type)) + self.assertRedirects( + response, + resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url(self.workspace_type), + ) - def test_status_code_with_user_permission(self): + def test_status_code_with_staff_view_permission(self): """Returns successful response code.""" - self.client.force_login(self.user) - response = self.client.get(self.get_url()) + self.client.force_login(self.staff_view_user) + response = self.client.get(self.get_url(self.workspace_type)) self.assertEqual(response.status_code, 200) - def test_access_with_limited_view_permission(self): + def test_access_with_view_permission(self): """Raises permission denied if user has limited view permission.""" - user = User.objects.create_user(username="test-limited", password="test-limited") - user.user_permissions.add( - Permission.objects.get(codename=models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME) - ) - request = self.factory.get(self.get_url()) - request.user = user - with self.assertRaises(PermissionDenied): - self.get_view()(request) + self.client.force_login(self.view_user) + response = self.client.get(self.get_url(self.workspace_type)) + self.assertEqual(response.status_code, 200) def test_access_without_user_permission(self): """Raises permission denied if user has no permissions.""" user_no_perms = User.objects.create_user(username="test-none", password="test-none") - request = self.factory.get(self.get_url()) + request = self.factory.get(self.get_url(self.workspace_type)) request.user = user_no_perms with self.assertRaises(PermissionDenied): - self.get_view()(request) + self.get_view()(request, workspace_type=self.workspace_type) - def test_returns_all_objects(self): - """Queryset returns all objects when there is no query.""" - groups = factories.WorkspaceFactory.create_batch(10) - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] - self.assertEqual(len(returned_ids), 10) - self.assertEqual(sorted(returned_ids), sorted([group.pk for group in groups])) + def test_get_workspace_type_not_registered(self): + """Raises 404 with get request if workspace type is not registered with adapter.""" + request = self.factory.get(self.get_url("foo")) + request.user = self.view_user + with self.assertRaises(Http404): + self.get_view()(request, workspace_type="foo") + + def test_view_has_correct_table_class_staff_view(self): + self.client.force_login(self.staff_view_user) + response = self.client.get(self.get_url(self.workspace_type)) + self.assertIn("table", response.context_data) + self.assertIsInstance(response.context_data["table"], tables.WorkspaceStaffTable) + + def test_view_has_correct_table_class_view(self): + self.client.force_login(self.view_user) + response = self.client.get(self.get_url(self.workspace_type)) + self.assertIn("table", response.context_data) + self.assertIsInstance(response.context_data["table"], tables.WorkspaceUserTable) + + def test_view_with_no_objects(self): + self.client.force_login(self.view_user) + response = self.client.get(self.get_url(self.workspace_type)) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 0) + + def test_view_with_one_object(self): + factories.WorkspaceFactory() + self.client.force_login(self.view_user) + response = self.client.get(self.get_url(self.workspace_type)) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 1) + + def test_view_with_two_objects(self): + factories.WorkspaceFactory.create(name="w1") + factories.WorkspaceFactory.create(name="w2") + self.client.force_login(self.view_user) + response = self.client.get(self.get_url(self.workspace_type)) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 2) + + def test_adapter_table_class_staff_view(self): + """Displays the correct table if specified in the adapter.""" + # Overriding settings doesn't work, because appconfig.ready has already run and + # registered the default adapter. Instead, unregister the default and register the + # new adapter here. + workspace_adapter_registry.unregister(DefaultWorkspaceAdapter) + workspace_adapter_registry.register(TestWorkspaceAdapter) + self.workspace_type = TestWorkspaceAdapter().get_type() + self.client.force_login(self.staff_view_user) + response = self.client.get(self.get_url(self.workspace_type)) + self.assertIn("table", response.context_data) + self.assertIsInstance(response.context_data["table"], app_tables.TestWorkspaceDataStaffTable) + + def test_adapter_table_class_view(self): + """Displays the correct table if specified in the adapter.""" + # Overriding settings doesn't work, because appconfig.ready has already run and + # registered the default adapter. Instead, unregister the default and register the + # new adapter here. + workspace_adapter_registry.unregister(DefaultWorkspaceAdapter) + workspace_adapter_registry.register(TestWorkspaceAdapter) + self.workspace_type = TestWorkspaceAdapter().get_type() + self.client.force_login(self.view_user) + response = self.client.get(self.get_url(self.workspace_type)) + self.assertIn("table", response.context_data) + self.assertIsInstance(response.context_data["table"], app_tables.TestWorkspaceDataUserTable) + + def test_only_shows_workspaces_with_correct_type(self): + """Only workspaces with the same workspace_type are shown in the table.""" + workspace_adapter_registry.register(TestWorkspaceAdapter) + factories.WorkspaceFactory(workspace_type=TestWorkspaceAdapter().get_type()) + default_type = DefaultWorkspaceAdapter().get_type() + self.client.force_login(self.view_user) + response = self.client.get(self.get_url(default_type)) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 0) + + def test_view_with_filter_return_no_object(self): + factories.WorkspaceFactory.create(name="workspace1") + factories.WorkspaceFactory.create(name="workspace2") + self.client.force_login(self.view_user) + response = self.client.get(self.get_url(self.workspace_type), {"name__icontains": "abc"}) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 0) + + def test_view_with_filter_returns_one_object_exact(self): + instance = factories.WorkspaceFactory.create(name="workspace1") + factories.WorkspaceFactory.create(name="workspace2") + self.client.force_login(self.view_user) + response = self.client.get(self.get_url(self.workspace_type), {"name__icontains": "workspace1"}) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 1) + self.assertIn(instance, response.context_data["table"].data) + + def test_view_with_filter_returns_one_object_case_insensitive(self): + instance = factories.WorkspaceFactory.create(name="workspace1") + factories.WorkspaceFactory.create(name="workspace2") + self.client.force_login(self.view_user) + response = self.client.get(self.get_url(self.workspace_type), {"name__icontains": "Workspace1"}) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 1) + self.assertIn(instance, response.context_data["table"].data) + + def test_view_with_filter_returns_one_object_case_contains(self): + instance = factories.WorkspaceFactory.create(name="workspace1") + factories.WorkspaceFactory.create(name="workspace2") + self.client.force_login(self.view_user) + response = self.client.get(self.get_url(self.workspace_type), {"name__icontains": "orkspace1"}) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 1) + self.assertIn(instance, response.context_data["table"].data) - def test_returns_correct_object_match(self): - """Queryset returns the correct objects when query matches the name.""" - workspace = factories.WorkspaceFactory.create(name="test-workspace") - self.client.force_login(self.user) - response = self.client.get(self.get_url(), {"q": "test-workspace"}) - returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] - self.assertEqual(len(returned_ids), 1) - self.assertEqual(returned_ids[0], workspace.pk) + def test_view_with_filter_workspace_type(self): + instance = factories.WorkspaceFactory.create(name="workspace1") + factories.WorkspaceFactory.create(name="workspace2", workspace_type=TestWorkspaceAdapter().get_type()) + self.client.force_login(self.view_user) + response = self.client.get(self.get_url(self.workspace_type), {"name__icontains": "workspace"}) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 1) + self.assertIn(instance, response.context_data["table"].data) - def test_returns_correct_object_starting_with_query(self): - """Queryset returns the correct objects when query matches the beginning of the name.""" - workspace = factories.WorkspaceFactory.create(name="test-workspace") - self.client.force_login(self.user) - response = self.client.get(self.get_url(), {"q": "test"}) - returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] - self.assertEqual(len(returned_ids), 1) - self.assertEqual(returned_ids[0], workspace.pk) + def test_view_with_filter_returns_mutiple_objects(self): + factories.WorkspaceFactory.create(name="workspace1") + factories.WorkspaceFactory.create(name="wOrkspace1") + self.client.force_login(self.view_user) + response = self.client.get(self.get_url(self.workspace_type), {"name__icontains": "Workspace"}) + self.assertEqual(response.status_code, 200) + self.assertIn("table", response.context_data) + self.assertEqual(len(response.context_data["table"].rows), 2) - def test_returns_correct_object_containing_query(self): - """Queryset returns the correct objects when the name contains the query.""" - workspace = factories.WorkspaceFactory.create(name="test-workspace") - self.client.force_login(self.user) - response = self.client.get(self.get_url(), {"q": "work"}) - returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] - self.assertEqual(len(returned_ids), 1) - self.assertEqual(returned_ids[0], workspace.pk) + def test_view_with_default_adapter_use_default_workspace_list_template(self): + default_type = DefaultWorkspaceAdapter().get_type() + self.client.force_login(self.view_user) + response = self.client.get(self.get_url(default_type)) + self.assertTemplateUsed(response, "anvil_consortium_manager/workspace_list.html") - def test_returns_correct_object_case_insensitive(self): - """Queryset returns the correct objects when query matches the beginning of the name.""" - workspace = factories.WorkspaceFactory.create(name="test-workspace") - self.client.force_login(self.user) - response = self.client.get(self.get_url(), {"q": "TEST-WORKSPACE"}) - returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] - self.assertEqual(len(returned_ids), 1) - self.assertEqual(returned_ids[0], workspace.pk) + def test_view_with_custom_adapter_use_custom_workspace_list_template(self): + workspace_adapter_registry.unregister(DefaultWorkspaceAdapter) + workspace_adapter_registry.register(TestWorkspaceAdapter) + self.workspace_type = TestWorkspaceAdapter().get_type() + self.client.force_login(self.view_user) + response = self.client.get(self.get_url(self.workspace_type)) + self.assertTemplateUsed(response, "test_workspace_list.html") -class WorkspaceAutocompleteByTypeTest(TestCase): - """Tests for the WorkspaceAutocompleteByType view.""" +class WorkspaceDeleteTest(AnVILAPIMockTestMixin, TestCase): + api_success_code = 202 def setUp(self): """Set up test class.""" + # The superclass uses the responses package to mock API responses. + super().setUp() self.factory = RequestFactory() - # Create a user with the correct permissions. + # Create a user with both view and edit permissions. self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) ) - self.default_workspace_type = DefaultWorkspaceAdapter().get_type() - workspace_adapter_registry.register(TestWorkspaceAdapter) + self.user.user_permissions.add( + Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_EDIT_PERMISSION_CODENAME) + ) def tearDown(self): - workspace_adapter_registry.unregister(TestWorkspaceAdapter) + """Clean up after tests.""" + # Unregister all adapters. + workspace_adapter_registry._registry = {} + # Register the default adapter. + workspace_adapter_registry.register(DefaultWorkspaceAdapter) + super().tearDown() def get_url(self, *args): """Get the url for the view being tested.""" - return reverse("anvil_consortium_manager:workspaces:autocomplete_by_type", args=args) + return reverse("anvil_consortium_manager:workspaces:delete", args=args) + + def get_api_url(self, billing_project_name, workspace_name): + return self.api_client.rawls_entry_point + "/api/workspaces/" + billing_project_name + "/" + workspace_name def get_view(self): """Return the view being tested.""" - return views.WorkspaceAutocompleteByType.as_view() + return views.WorkspaceDelete.as_view() def test_view_redirect_not_logged_in(self): "View redirects to login view when user is not logged in." # Need a client for redirects. - response = self.client.get(self.get_url(self.default_workspace_type)) - self.assertRedirects( - response, - resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url(self.default_workspace_type), - ) + url = self.get_url("foo1", "foo2") + response = self.client.get(url) + self.assertRedirects(response, resolve_url(settings.LOGIN_URL) + "?next=" + url) def test_status_code_with_user_permission(self): """Returns successful response code.""" + obj = factories.WorkspaceFactory.create() self.client.force_login(self.user) - response = self.client.get(self.get_url(self.default_workspace_type)) + response = self.client.get(self.get_url(obj.billing_project.name, obj.name)) self.assertEqual(response.status_code, 200) + def test_access_with_view_permission(self): + """Raises permission denied if user has only view permission.""" + user_with_view_perm = User.objects.create_user(username="test-other", password="test-other") + user_with_view_perm.user_permissions.add( + Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) + ) + request = self.factory.get(self.get_url("foo1", "foo2")) + request.user = user_with_view_perm + with self.assertRaises(PermissionDenied): + self.get_view()(request, pk=1) + def test_access_with_limited_view_permission(self): """Raises permission denied if user has limited view permission.""" user = User.objects.create_user(username="test-limited", password="test-limited") user.user_permissions.add( Permission.objects.get(codename=models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME) ) - request = self.factory.get(self.get_url(self.default_workspace_type)) + request = self.factory.get(self.get_url("foo", "bar")) request.user = user with self.assertRaises(PermissionDenied): self.get_view()(request) @@ -12514,111 +11252,267 @@ def test_access_with_limited_view_permission(self): def test_access_without_user_permission(self): """Raises permission denied if user has no permissions.""" user_no_perms = User.objects.create_user(username="test-none", password="test-none") - request = self.factory.get(self.get_url(self.default_workspace_type)) + request = self.factory.get(self.get_url("foo1", "foo2")) request.user = user_no_perms with self.assertRaises(PermissionDenied): - self.get_view()(request, workspace_type=self.default_workspace_type) + self.get_view()(request, pk=1) - def test_404_with_unregistered_workspace_type(self): - """Raises 404 with get request if workspace type is not registered with adapter.""" - request = self.factory.get(self.get_url("foo")) + def test_view_with_invalid_pk(self): + """Returns a 404 when the object doesn't exist.""" + request = self.factory.get(self.get_url("foo1", "foo2")) request.user = self.user with self.assertRaises(Http404): - self.get_view()(request, workspace_type="foo") + self.get_view()(request, pk=1) - def test_returns_all_objects(self): - """Queryset returns all objects when there is no query.""" - workspaces = factories.DefaultWorkspaceDataFactory.create_batch(10) + def test_view_deletes_object(self): + """Posting submit to the form successfully deletes the object.""" + billing_project = factories.BillingProjectFactory.create(name="test-billing-project") + object = factories.WorkspaceFactory.create(billing_project=billing_project, name="test-workspace") + api_url = self.get_api_url(object.billing_project.name, object.name) + self.anvil_response_mock.add(responses.DELETE, api_url, status=self.api_success_code) self.client.force_login(self.user) - response = self.client.get(self.get_url(self.default_workspace_type)) - returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] - self.assertEqual(len(returned_ids), 10) - self.assertEqual(sorted(returned_ids), sorted([workspace.pk for workspace in workspaces])) + response = self.client.post(self.get_url(object.billing_project.name, object.name), {"submit": ""}) + self.assertEqual(response.status_code, 302) + self.assertEqual(models.Workspace.objects.count(), 0) + # History is added. + self.assertEqual(object.history.count(), 2) + self.assertEqual(object.history.latest().history_type, "-") - def test_returns_correct_object_match(self): - """Queryset returns the correct objects when query matches the name.""" - workspace = factories.DefaultWorkspaceDataFactory.create(workspace__name="test-workspace") + def test_success_message(self): + """Response includes a success message if successful.""" + billing_project = factories.BillingProjectFactory.create(name="test-billing-project") + object = factories.WorkspaceFactory.create(billing_project=billing_project, name="test-workspace") + api_url = self.get_api_url(object.billing_project.name, object.name) + self.anvil_response_mock.add(responses.DELETE, api_url, status=self.api_success_code) self.client.force_login(self.user) - response = self.client.get(self.get_url(self.default_workspace_type), {"q": "test-workspace"}) - returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] - self.assertEqual(len(returned_ids), 1) - self.assertEqual(returned_ids[0], workspace.pk) + response = self.client.post( + self.get_url(object.billing_project.name, object.name), + {"submit": ""}, + follow=True, + ) + messages = [m.message for m in get_messages(response.wsgi_request)] + self.assertEqual(len(messages), 1) + self.assertEqual(views.WorkspaceDelete.success_message, str(messages[0])) - def test_returns_correct_object_starting_with_query(self): - """Queryset returns the correct objects when query matches the beginning of the name.""" - workspace = factories.DefaultWorkspaceDataFactory.create(workspace__name="test-workspace") + def test_only_deletes_specified_pk(self): + """View only deletes the specified pk.""" + object = factories.WorkspaceFactory.create() + other_object = factories.WorkspaceFactory.create() + api_url = self.get_api_url(object.billing_project.name, object.name) + self.anvil_response_mock.add(responses.DELETE, api_url, status=self.api_success_code) self.client.force_login(self.user) - response = self.client.get(self.get_url(self.default_workspace_type), {"q": "test"}) - returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] - self.assertEqual(len(returned_ids), 1) - self.assertEqual(returned_ids[0], workspace.pk) + response = self.client.post(self.get_url(object.billing_project.name, object.name), {"submit": ""}) + self.assertEqual(response.status_code, 302) + self.assertEqual(models.Workspace.objects.count(), 1) + self.assertQuerySetEqual( + models.Workspace.objects.all(), + models.Workspace.objects.filter(pk=other_object.pk), + ) - def test_returns_correct_object_containing_query(self): - """Queryset returns the correct objects when the name contains the query.""" - workspace = factories.DefaultWorkspaceDataFactory.create(workspace__name="test-workspace") + def test_can_delete_workspace_with_auth_domain(self): + """A workspace can be deleted if it has an auth domain, and the auth domain group is not deleted.""" + billing_project = factories.BillingProjectFactory.create(name="test-billing-project") + object = factories.WorkspaceFactory.create(billing_project=billing_project, name="test-workspace") + auth_domain = factories.ManagedGroupFactory.create(name="test-group") + wad = models.WorkspaceAuthorizationDomain.objects.create(workspace=object, group=auth_domain) + # object.authorization_domains.add(auth_domain) + api_url = self.get_api_url(object.billing_project.name, object.name) + self.anvil_response_mock.add(responses.DELETE, api_url, status=self.api_success_code) + self.client.force_login(self.user) + response = self.client.post(self.get_url(object.billing_project.name, object.name), {"submit": ""}) + self.assertEqual(response.status_code, 302) + self.assertEqual(models.Workspace.objects.count(), 0) + self.assertEqual(models.WorkspaceAuthorizationDomain.objects.count(), 0) + # The auth domain group still exists. + self.assertEqual(models.ManagedGroup.objects.count(), 1) + models.ManagedGroup.objects.get(pk=auth_domain.pk) + # History is added for workspace. + self.assertEqual(object.history.count(), 2) + self.assertEqual(object.history.latest().history_type, "-") + # History is added for auth domain. + self.assertEqual(wad.history.count(), 2) + self.assertEqual(wad.history.latest().history_type, "-") + + def test_can_delete_workspace_that_has_been_shared_with_group(self): + """A workspace can be deleted if it has been shared with a group, and the group is not deleted.""" + billing_project = factories.BillingProjectFactory.create(name="test-billing-project") + object = factories.WorkspaceFactory.create(billing_project=billing_project, name="test-workspace") + group = factories.ManagedGroupFactory.create(name="test-group") + factories.WorkspaceGroupSharingFactory.create(workspace=object, group=group) + api_url = self.get_api_url(object.billing_project.name, object.name) + self.anvil_response_mock.add(responses.DELETE, api_url, status=self.api_success_code) + self.client.force_login(self.user) + response = self.client.post(self.get_url(object.billing_project.name, object.name), {"submit": ""}) + self.assertEqual(response.status_code, 302) + self.assertEqual(models.Workspace.objects.count(), 0) + self.assertEqual(models.WorkspaceGroupSharing.objects.count(), 0) + # The group still exists. + self.assertEqual(models.ManagedGroup.objects.count(), 1) + models.ManagedGroup.objects.get(pk=group.pk) + # History is added for workspace. + self.assertEqual(object.history.count(), 2) + self.assertEqual(object.history.latest().history_type, "-") + # History is added for WorkspaceGroupSharing. + self.assertEqual(models.WorkspaceGroupSharing.history.count(), 2) + self.assertEqual(models.WorkspaceGroupSharing.history.latest().history_type, "-") + + def test_success_url(self): + """Redirects to the expected page.""" + object = factories.WorkspaceFactory.create() + # Need to use the client instead of RequestFactory to check redirection url. + api_url = self.get_api_url(object.billing_project.name, object.name) + self.anvil_response_mock.add(responses.DELETE, api_url, status=self.api_success_code) + self.client.force_login(self.user) + response = self.client.post(self.get_url(object.billing_project.name, object.name), {"submit": ""}) + self.assertEqual(response.status_code, 302) + self.assertRedirects( + response, + reverse( + "anvil_consortium_manager:workspaces:list", + args=[DefaultWorkspaceAdapter().get_type()], + ), + ) + + def test_adapter_success_url(self): + """Redirects to the expected page.""" + # Register a new adapter. + workspace_adapter_registry.register(TestWorkspaceAdapter) + object = factories.WorkspaceFactory.create(workspace_type=TestWorkspaceAdapter().get_type()) + # Need to use the client instead of RequestFactory to check redirection url. + api_url = self.get_api_url(object.billing_project.name, object.name) + self.anvil_response_mock.add(responses.DELETE, api_url, status=self.api_success_code) self.client.force_login(self.user) - response = self.client.get(self.get_url(self.default_workspace_type), {"q": "work"}) - returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] - self.assertEqual(len(returned_ids), 1) - self.assertEqual(returned_ids[0], workspace.pk) + response = self.client.post(self.get_url(object.billing_project.name, object.name), {"submit": ""}) + self.assertEqual(response.status_code, 302) + self.assertRedirects( + response, + reverse( + "anvil_consortium_manager:workspaces:list", + args=[TestWorkspaceAdapter().get_type()], + ), + ) - def test_returns_correct_object_case_insensitive(self): - """Queryset returns the correct objects when query matches the beginning of the name.""" - workspace = factories.DefaultWorkspaceDataFactory.create(workspace__name="test-workspace") + def test_api_error(self): + """Shows a message if an AnVIL API error occurs.""" + # Need a client to check messages. + object = factories.WorkspaceFactory.create() + api_url = self.get_api_url(object.billing_project.name, object.name) + self.anvil_response_mock.add( + responses.DELETE, + api_url, + status=500, + json={"message": "workspace delete test error"}, + ) self.client.force_login(self.user) - response = self.client.get(self.get_url(self.default_workspace_type), {"q": "TEST-WORKSPACE"}) - returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] - self.assertEqual(len(returned_ids), 1) - self.assertEqual(returned_ids[0], workspace.pk) + response = self.client.post(self.get_url(object.billing_project.name, object.name), {"submit": ""}) + self.assertEqual(response.status_code, 200) + messages = [m.message for m in get_messages(response.wsgi_request)] + self.assertEqual(len(messages), 1) + self.assertIn("AnVIL API Error: workspace delete test error", str(messages[0])) + # Make sure that the object still exists. + self.assertEqual(models.Workspace.objects.count(), 1) - def test_only_specified_workspace_type(self): - """Queryset returns only objects with the specified workspace type.""" - workspace = factories.DefaultWorkspaceDataFactory.create() - other_workspace = TestWorkspaceDataFactory.create() + def test_post_does_not_delete_when_protected_fk_to_another_model(self): + """Workspace is not deleted when there is a protected foreign key reference to the workspace.""" + object = factories.DefaultWorkspaceDataFactory.create() + app_models.ProtectedWorkspace.objects.create(workspace=object.workspace) self.client.force_login(self.user) - response = self.client.get(self.get_url(workspace.workspace.workspace_type)) - returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] - self.assertEqual(len(returned_ids), 1) - self.assertEqual(returned_ids[0], workspace.pk) - response = self.client.get(self.get_url(other_workspace.workspace.workspace_type)) - returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] - self.assertEqual(len(returned_ids), 1) - self.assertEqual(returned_ids[0], other_workspace.pk) + response = self.client.post( + self.get_url(object.workspace.billing_project.name, object.workspace.name), + {"submit": ""}, + follow=True, + ) + self.assertRedirects(response, object.get_absolute_url()) + # A message is added. + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + self.assertEqual( + views.WorkspaceDelete.message_could_not_delete_workspace_from_app, + str(messages[0]), + ) + # Make sure the group still exists. + self.assertEqual(models.Workspace.objects.count(), 1) + self.assertEqual(models.DefaultWorkspaceData.objects.count(), 1) + object.refresh_from_db() - def test_custom_autocomplete_method(self): - # Workspace that will match the custom autocomplete filtering. - workspace_1 = TestWorkspaceDataFactory.create(workspace__name="TEST") - # Workspace that should not match the custom autocomplete filtering. - TestWorkspaceDataFactory.create(workspace__name="TEST-WORKSPACE") + def test_post_does_not_delete_when_workspace_data_has_protected_fk_to_another_model( + self, + ): + """Workspace is not deleted when there is a protected foreign key reference to the workspace data.""" + workspace_data = factories.DefaultWorkspaceDataFactory() + object = workspace_data.workspace + app_models.ProtectedWorkspaceData.objects.create(workspace_data=workspace_data) self.client.force_login(self.user) - response = self.client.get(self.get_url(workspace_1.workspace.workspace_type), {"q": "TEST"}) - returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] - self.assertEqual(len(returned_ids), 1) - self.assertEqual(returned_ids[0], workspace_1.pk) + response = self.client.post( + self.get_url(object.billing_project.name, object.name), + {"submit": ""}, + follow=True, + ) + self.assertRedirects(response, object.get_absolute_url()) + # A message is added. + messages = list(response.context["messages"]) + self.assertEqual(len(messages), 1) + self.assertEqual( + views.WorkspaceDelete.message_could_not_delete_workspace_from_app, + str(messages[0]), + ) + # Make sure the group still exists. + self.assertEqual(models.Workspace.objects.count(), 1) + object.refresh_from_db() - def test_custom_autocomplete_with_forwarded_value(self): - # Workspace that will match the custom autocomplete filtering. - workspace = TestWorkspaceDataFactory.create() - # Workspace that should not match the custom autocomplete filtering. - TestWorkspaceDataFactory.create() + def test_get_is_locked(self): + """View redirects with a get request if the workspace is locked.""" + billing_project = factories.BillingProjectFactory.create(name="test-billing-project") + object = factories.DefaultWorkspaceDataFactory.create( + workspace__billing_project=billing_project, + workspace__name="test-workspace", + workspace__is_locked=True, + ) self.client.force_login(self.user) response = self.client.get( - self.get_url(workspace.workspace.workspace_type), - {"forward": json.dumps({"billing_project": workspace.workspace.billing_project.pk})}, + self.get_url(object.workspace.billing_project.name, object.workspace.name), + follow=True, ) - returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] - self.assertEqual(len(returned_ids), 1) - self.assertEqual(returned_ids[0], workspace.pk) + # Make sure the workspace still exists. + self.assertIn(object.workspace, models.Workspace.objects.all()) + self.assertIn(object, models.DefaultWorkspaceData.objects.all()) + # Redirects to detail page. + self.assertRedirects(response, object.get_absolute_url()) + # With a message. + messages = [m.message for m in get_messages(response.wsgi_request)] + self.assertEqual(len(messages), 1) + self.assertEqual(views.WorkspaceDelete.message_workspace_locked, str(messages[0])) + def test_post_is_locked(self): + """View redirects with a post request if the workspace is locked.""" + billing_project = factories.BillingProjectFactory.create(name="test-billing-project") + object = factories.DefaultWorkspaceDataFactory.create( + workspace__billing_project=billing_project, + workspace__name="test-workspace", + workspace__is_locked=True, + ) + self.client.force_login(self.user) + response = self.client.post( + self.get_url(object.workspace.billing_project.name, object.workspace.name), + {"submit": ""}, + follow=True, + ) + # Make sure the workspace still exists. + self.assertIn(object.workspace, models.Workspace.objects.all()) + self.assertIn(object, models.DefaultWorkspaceData.objects.all()) + # Redirects to detail page. + self.assertRedirects(response, object.get_absolute_url()) + # With a message. + messages = [m.message for m in get_messages(response.wsgi_request)] + self.assertEqual(len(messages), 1) + self.assertEqual(views.WorkspaceDelete.message_workspace_locked, str(messages[0])) -class WorkspaceAuditTest(AnVILAPIMockTestMixin, TestCase): - """Tests for the WorkspaceAudit view.""" +class WorkspaceAutocompleteTest(TestCase): def setUp(self): """Set up test class.""" - super().setUp() self.factory = RequestFactory() - # Create a user with only view permission. + # Create a user with the correct permissions. self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) @@ -12626,56 +11520,11 @@ def setUp(self): def get_url(self, *args): """Get the url for the view being tested.""" - return reverse("anvil_consortium_manager:workspaces:audit", args=args) - - def get_api_url(self): - return self.api_client.rawls_entry_point + "/api/workspaces" - - def get_api_workspace_json(self, billing_project_name, workspace_name, access, auth_domains=[]): - """Return the json dictionary for a single workspace on AnVIL.""" - return { - "accessLevel": access, - "workspace": { - "name": workspace_name, - "namespace": billing_project_name, - "authorizationDomain": [{"membersGroupName": x} for x in auth_domains], - "isLocked": False, - }, - } - - def get_api_workspace_acl_url(self, billing_project_name, workspace_name): - return ( - self.api_client.rawls_entry_point - + "/api/workspaces/" - + billing_project_name - + "/" - + workspace_name - + "/acl" - ) - - def get_api_workspace_acl_response(self): - """Return a json for the workspace/acl method where no one else can access.""" - return { - "acl": { - self.service_account_email: { - "accessLevel": "OWNER", - "canCompute": True, - "canShare": True, - "pending": False, - } - } - } - - def get_api_bucket_options_url(self, billing_project_name, workspace_name): - return self.api_client.rawls_entry_point + "/api/workspaces/" + billing_project_name + "/" + workspace_name - - def get_api_bucket_options_response(self): - """Return a json for the workspace/acl method that is not requester pays.""" - return {"bucketOptions": {"requesterPays": False}} + return reverse("anvil_consortium_manager:workspaces:autocomplete", args=args) def get_view(self): """Return the view being tested.""" - return views.WorkspaceAudit.as_view() + return views.WorkspaceAutocomplete.as_view() def test_view_redirect_not_logged_in(self): "View redirects to login view when user is not logged in." @@ -12685,13 +11534,6 @@ def test_view_redirect_not_logged_in(self): def test_status_code_with_user_permission(self): """Returns successful response code.""" - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[], - ) self.client.force_login(self.user) response = self.client.get(self.get_url()) self.assertEqual(response.status_code, 200) @@ -12708,247 +11550,97 @@ def test_access_with_limited_view_permission(self): self.get_view()(request) def test_access_without_user_permission(self): - """Raises permission denied if user has no permissions.""" - user_no_perms = User.objects.create_user(username="test-none", password="test-none") - request = self.factory.get(self.get_url()) - request.user = user_no_perms - with self.assertRaises(PermissionDenied): - self.get_view()(request) - - def test_template(self): - """Template loads successfully.""" - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[], - ) - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertEqual(response.status_code, 200) - - def test_audit_verified(self): - """audit_verified is in the context data.""" - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[], - ) - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("verified_table", response.context_data) - self.assertIsInstance(response.context_data["verified_table"], audit.VerifiedTable) - self.assertEqual(len(response.context_data["verified_table"].rows), 0) - - def test_audit_verified_one_record(self): - """audit_verified with one verified record.""" - workspace = factories.WorkspaceFactory.create() - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[self.get_api_workspace_json(workspace.billing_project.name, workspace.name, "OWNER")], - ) - # Response to check workspace access. - workspace_acl_url = self.get_api_workspace_acl_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_workspace_acl_response(), - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("verified_table", response.context_data) - self.assertEqual(len(response.context_data["verified_table"].rows), 1) - - def test_audit_errors(self): - """audit_errors is in the context data.""" - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[], - ) - self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("error_table", response.context_data) - self.assertIsInstance(response.context_data["error_table"], audit.ErrorTable) - self.assertEqual(len(response.context_data["error_table"].rows), 0) - - def test_audit_errors_one_record(self): - """audit_errors with one error record.""" - workspace = factories.WorkspaceFactory.create() - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - # Error - we are not an owner. - json=[self.get_api_workspace_json(workspace.billing_project.name, workspace.name, "READER")], - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) + """Raises permission denied if user has no permissions.""" + user_no_perms = User.objects.create_user(username="test-none", password="test-none") + request = self.factory.get(self.get_url()) + request.user = user_no_perms + with self.assertRaises(PermissionDenied): + self.get_view()(request) + + def test_returns_all_objects(self): + """Queryset returns all objects when there is no query.""" + groups = factories.WorkspaceFactory.create_batch(10) self.client.force_login(self.user) response = self.client.get(self.get_url()) - self.assertIn("error_table", response.context_data) - self.assertEqual(len(response.context_data["error_table"].rows), 1) + returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] + self.assertEqual(len(returned_ids), 10) + self.assertEqual(sorted(returned_ids), sorted([group.pk for group in groups])) - def test_audit_not_in_app(self): - """audit_not_in_app is in the context data.""" - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[], - ) + def test_returns_correct_object_match(self): + """Queryset returns the correct objects when query matches the name.""" + workspace = factories.WorkspaceFactory.create(name="test-workspace") self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("not_in_app_table", response.context_data) - self.assertIsInstance(response.context_data["not_in_app_table"], audit.NotInAppTable) - self.assertEqual(len(response.context_data["not_in_app_table"].rows), 0) + response = self.client.get(self.get_url(), {"q": "test-workspace"}) + returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] + self.assertEqual(len(returned_ids), 1) + self.assertEqual(returned_ids[0], workspace.pk) - def test_audit_not_in_app_one_record(self): - """audit_not_in_app with one record not in app.""" - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[self.get_api_workspace_json("foo", "bar", "OWNER")], - ) + def test_returns_correct_object_starting_with_query(self): + """Queryset returns the correct objects when query matches the beginning of the name.""" + workspace = factories.WorkspaceFactory.create(name="test-workspace") self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("not_in_app_table", response.context_data) - self.assertEqual(len(response.context_data["not_in_app_table"].rows), 1) + response = self.client.get(self.get_url(), {"q": "test"}) + returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] + self.assertEqual(len(returned_ids), 1) + self.assertEqual(returned_ids[0], workspace.pk) - def test_audit_ok_is_ok(self): - """audit_ok when audit_results.ok() is True.""" - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - json=[], - ) + def test_returns_correct_object_containing_query(self): + """Queryset returns the correct objects when the name contains the query.""" + workspace = factories.WorkspaceFactory.create(name="test-workspace") self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("audit_ok", response.context_data) - self.assertEqual(response.context_data["audit_ok"], True) + response = self.client.get(self.get_url(), {"q": "work"}) + returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] + self.assertEqual(len(returned_ids), 1) + self.assertEqual(returned_ids[0], workspace.pk) - def test_audit_ok_is_not_ok(self): - """audit_ok when audit_results.ok() is False.""" - workspace = factories.WorkspaceFactory.create() - api_url = self.get_api_url() - self.anvil_response_mock.add( - responses.GET, - api_url, - status=200, - # Error - we are not admin. - json=[self.get_api_workspace_json(workspace.billing_project.name, workspace.name, "READER")], - ) - # Response to check workspace bucket options. - workspace_acl_url = self.get_api_bucket_options_url(workspace.billing_project.name, workspace.name) - self.anvil_response_mock.add( - responses.GET, - workspace_acl_url, - status=200, - json=self.get_api_bucket_options_response(), - ) + def test_returns_correct_object_case_insensitive(self): + """Queryset returns the correct objects when query matches the beginning of the name.""" + workspace = factories.WorkspaceFactory.create(name="test-workspace") self.client.force_login(self.user) - response = self.client.get(self.get_url()) - self.assertIn("audit_ok", response.context_data) - self.assertEqual(response.context_data["audit_ok"], False) + response = self.client.get(self.get_url(), {"q": "TEST-WORKSPACE"}) + returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] + self.assertEqual(len(returned_ids), 1) + self.assertEqual(returned_ids[0], workspace.pk) -class WorkspaceSharingAuditTest(AnVILAPIMockTestMixin, TestCase): - """Tests for the WorkspaceSharingAudit view.""" +class WorkspaceAutocompleteByTypeTest(TestCase): + """Tests for the WorkspaceAutocompleteByType view.""" def setUp(self): """Set up test class.""" - super().setUp() self.factory = RequestFactory() - # Create a user with both view and edit permission. + # Create a user with the correct permissions. self.user = User.objects.create_user(username="test", password="test") self.user.user_permissions.add( Permission.objects.get(codename=models.AnVILProjectManagerAccess.STAFF_VIEW_PERMISSION_CODENAME) ) - # Set this variable here because it will include the service account. - # Tests can update it with the update_api_response method. - self.api_response = {"acl": {}} - self.update_api_response(self.service_account_email, "OWNER", can_compute=True, can_share=True) - # Create a workspace for use in tests. - self.workspace = factories.WorkspaceFactory.create() - self.api_url = ( - self.api_client.rawls_entry_point - + "/api/workspaces/" - + self.workspace.billing_project.name - + "/" - + self.workspace.name - + "/acl" - ) + self.default_workspace_type = DefaultWorkspaceAdapter().get_type() + workspace_adapter_registry.register(TestWorkspaceAdapter) + + def tearDown(self): + workspace_adapter_registry.unregister(TestWorkspaceAdapter) def get_url(self, *args): """Get the url for the view being tested.""" - return reverse("anvil_consortium_manager:workspaces:audit_sharing", args=args) - - def update_api_response(self, email, access, can_compute=False, can_share=False): - """Return a paired down json for a single ACL, including the service account.""" - self.api_response["acl"].update( - { - email: { - "accessLevel": access, - "canCompute": can_compute, - "canShare": can_share, - "pending": False, - } - } - ) + return reverse("anvil_consortium_manager:workspaces:autocomplete_by_type", args=args) def get_view(self): """Return the view being tested.""" - return views.WorkspaceSharingAudit.as_view() + return views.WorkspaceAutocompleteByType.as_view() def test_view_redirect_not_logged_in(self): "View redirects to login view when user is not logged in." # Need a client for redirects. - response = self.client.get(self.get_url("foo", "bar")) + response = self.client.get(self.get_url(self.default_workspace_type)) self.assertRedirects( response, - resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url("foo", "bar"), + resolve_url(settings.LOGIN_URL) + "?next=" + self.get_url(self.default_workspace_type), ) def test_status_code_with_user_permission(self): """Returns successful response code.""" - # Group membership API call. - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) self.client.force_login(self.user) - response = self.client.get(self.get_url(self.workspace.billing_project.name, self.workspace.name)) + response = self.client.get(self.get_url(self.default_workspace_type)) self.assertEqual(response.status_code, 200) def test_access_with_limited_view_permission(self): @@ -12957,7 +11649,7 @@ def test_access_with_limited_view_permission(self): user.user_permissions.add( Permission.objects.get(codename=models.AnVILProjectManagerAccess.VIEW_PERMISSION_CODENAME) ) - request = self.factory.get(self.get_url("foo", "bar")) + request = self.factory.get(self.get_url(self.default_workspace_type)) request.user = user with self.assertRaises(PermissionDenied): self.get_view()(request) @@ -12965,165 +11657,101 @@ def test_access_with_limited_view_permission(self): def test_access_without_user_permission(self): """Raises permission denied if user has no permissions.""" user_no_perms = User.objects.create_user(username="test-none", password="test-none") - request = self.factory.get(self.get_url("foo", "bar")) + request = self.factory.get(self.get_url(self.default_workspace_type)) request.user = user_no_perms with self.assertRaises(PermissionDenied): - self.get_view()(request, billing_project_slug="foo", workspace_slug="bar") + self.get_view()(request, workspace_type=self.default_workspace_type) - def test_template(self): - """Template loads successfully.""" - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - self.client.force_login(self.user) - response = self.client.get(self.get_url(self.workspace.billing_project.name, self.workspace.name)) - self.assertEqual(response.status_code, 200) + def test_404_with_unregistered_workspace_type(self): + """Raises 404 with get request if workspace type is not registered with adapter.""" + request = self.factory.get(self.get_url("foo")) + request.user = self.user + with self.assertRaises(Http404): + self.get_view()(request, workspace_type="foo") - def test_audit_verified(self): - """audit_verified is in the context data.""" - # Group membership API call. - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - self.client.force_login(self.user) - response = self.client.get(self.get_url(self.workspace.billing_project.name, self.workspace.name)) - self.assertIn("verified_table", response.context_data) - self.assertIsInstance(response.context_data["verified_table"], audit.VerifiedTable) - self.assertEqual(len(response.context_data["verified_table"].rows), 0) - - def test_audit_verified_one_record(self): - """audit_verified with one verified record.""" - access = factories.WorkspaceGroupSharingFactory.create(workspace=self.workspace) - self.update_api_response(access.group.email, "READER") - # Group membership API call. - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) + def test_returns_all_objects(self): + """Queryset returns all objects when there is no query.""" + workspaces = factories.DefaultWorkspaceDataFactory.create_batch(10) self.client.force_login(self.user) - response = self.client.get(self.get_url(self.workspace.billing_project.name, self.workspace.name)) - self.assertIn("verified_table", response.context_data) - self.assertEqual(len(response.context_data["verified_table"].rows), 1) + response = self.client.get(self.get_url(self.default_workspace_type)) + returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] + self.assertEqual(len(returned_ids), 10) + self.assertEqual(sorted(returned_ids), sorted([workspace.pk for workspace in workspaces])) - def test_audit_errors(self): - """audit_errors is in the context data.""" - # Group membership API call. - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) - self.client.force_login(self.user) - response = self.client.get(self.get_url(self.workspace.billing_project.name, self.workspace.name)) - self.assertIn("error_table", response.context_data) - self.assertIsInstance(response.context_data["error_table"], audit.ErrorTable) - self.assertEqual(len(response.context_data["error_table"].rows), 0) - - def test_audit_errors_one_record(self): - """audit_errors with one error record.""" - access = factories.WorkspaceGroupSharingFactory.create(workspace=self.workspace) - # Different access recorded. - self.update_api_response(access.group.email, "WRITER") - # Group membership API call. - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) + def test_returns_correct_object_match(self): + """Queryset returns the correct objects when query matches the name.""" + workspace = factories.DefaultWorkspaceDataFactory.create(workspace__name="test-workspace") self.client.force_login(self.user) - response = self.client.get(self.get_url(self.workspace.billing_project.name, self.workspace.name)) - self.assertIn("error_table", response.context_data) - self.assertEqual(len(response.context_data["error_table"].rows), 1) + response = self.client.get(self.get_url(self.default_workspace_type), {"q": "test-workspace"}) + returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] + self.assertEqual(len(returned_ids), 1) + self.assertEqual(returned_ids[0], workspace.pk) - def test_audit_not_in_app(self): - """audit_not_in_app is in the context data.""" - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - # Admin insetad of member. - # json=self.get_api_group_members_json_response(), - ) + def test_returns_correct_object_starting_with_query(self): + """Queryset returns the correct objects when query matches the beginning of the name.""" + workspace = factories.DefaultWorkspaceDataFactory.create(workspace__name="test-workspace") self.client.force_login(self.user) - response = self.client.get(self.get_url(self.workspace.billing_project.name, self.workspace.name)) - self.assertIn("not_in_app_table", response.context_data) - self.assertIsInstance(response.context_data["not_in_app_table"], audit.NotInAppTable) - self.assertEqual(len(response.context_data["not_in_app_table"].rows), 0) + response = self.client.get(self.get_url(self.default_workspace_type), {"q": "test"}) + returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] + self.assertEqual(len(returned_ids), 1) + self.assertEqual(returned_ids[0], workspace.pk) - def test_audit_not_in_app_one_record(self): - """audit_not_in_app with one record not in app.""" - self.update_api_response("foo@bar.com", "READER") - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - ) + def test_returns_correct_object_containing_query(self): + """Queryset returns the correct objects when the name contains the query.""" + workspace = factories.DefaultWorkspaceDataFactory.create(workspace__name="test-workspace") self.client.force_login(self.user) - response = self.client.get(self.get_url(self.workspace.billing_project.name, self.workspace.name)) - self.assertIn("not_in_app_table", response.context_data) - self.assertEqual(len(response.context_data["not_in_app_table"].rows), 1) + response = self.client.get(self.get_url(self.default_workspace_type), {"q": "work"}) + returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] + self.assertEqual(len(returned_ids), 1) + self.assertEqual(returned_ids[0], workspace.pk) - def test_audit_ok_is_ok(self): - """audit_ok when audit_results.ok() is True.""" - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - # Admin insetad of member. - # json=self.get_api_group_members_json_response(), - ) + def test_returns_correct_object_case_insensitive(self): + """Queryset returns the correct objects when query matches the beginning of the name.""" + workspace = factories.DefaultWorkspaceDataFactory.create(workspace__name="test-workspace") self.client.force_login(self.user) - response = self.client.get(self.get_url(self.workspace.billing_project.name, self.workspace.name)) - self.assertIn("audit_ok", response.context_data) - self.assertEqual(response.context_data["audit_ok"], True) + response = self.client.get(self.get_url(self.default_workspace_type), {"q": "TEST-WORKSPACE"}) + returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] + self.assertEqual(len(returned_ids), 1) + self.assertEqual(returned_ids[0], workspace.pk) - def test_audit_ok_is_not_ok(self): - """audit_ok when audit_results.ok() is False.""" - self.update_api_response("foo@bar.com", "READER") - self.anvil_response_mock.add( - responses.GET, - self.api_url, - status=200, - json=self.api_response, - # Not in app - # json=self.get_api_group_members_json_response(members=["foo@bar.com"]), - ) + def test_only_specified_workspace_type(self): + """Queryset returns only objects with the specified workspace type.""" + workspace = factories.DefaultWorkspaceDataFactory.create() + other_workspace = TestWorkspaceDataFactory.create() self.client.force_login(self.user) - response = self.client.get(self.get_url(self.workspace.billing_project.name, self.workspace.name)) - self.assertIn("audit_ok", response.context_data) - self.assertEqual(response.context_data["audit_ok"], False) + response = self.client.get(self.get_url(workspace.workspace.workspace_type)) + returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] + self.assertEqual(len(returned_ids), 1) + self.assertEqual(returned_ids[0], workspace.pk) + response = self.client.get(self.get_url(other_workspace.workspace.workspace_type)) + returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] + self.assertEqual(len(returned_ids), 1) + self.assertEqual(returned_ids[0], other_workspace.pk) - def test_workspace_does_not_exist_in_app(self): - """Raises a 404 error with an invalid workspace slug.""" - billing_project = factories.BillingProjectFactory.create() - request = self.factory.get(self.get_url(billing_project.name, self.workspace.name)) - request.user = self.user - with self.assertRaises(Http404): - self.get_view()( - request, - billing_project_slug=billing_project.name, - workspace_slug=self.workspace.name, - ) + def test_custom_autocomplete_method(self): + # Workspace that will match the custom autocomplete filtering. + workspace_1 = TestWorkspaceDataFactory.create(workspace__name="TEST") + # Workspace that should not match the custom autocomplete filtering. + TestWorkspaceDataFactory.create(workspace__name="TEST-WORKSPACE") + self.client.force_login(self.user) + response = self.client.get(self.get_url(workspace_1.workspace.workspace_type), {"q": "TEST"}) + returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] + self.assertEqual(len(returned_ids), 1) + self.assertEqual(returned_ids[0], workspace_1.pk) - def test_billing_project_does_not_exist_in_app(self): - """Raises a 404 error with an invalid billing project slug.""" - request = self.factory.get(self.get_url("foo", self.workspace.name)) - request.user = self.user - with self.assertRaises(Http404): - self.get_view()(request, billing_project_slug="foo", workspace_slug=self.workspace.name) + def test_custom_autocomplete_with_forwarded_value(self): + # Workspace that will match the custom autocomplete filtering. + workspace = TestWorkspaceDataFactory.create() + # Workspace that should not match the custom autocomplete filtering. + TestWorkspaceDataFactory.create() + self.client.force_login(self.user) + response = self.client.get( + self.get_url(workspace.workspace.workspace_type), + {"forward": json.dumps({"billing_project": workspace.workspace.billing_project.pk})}, + ) + returned_ids = [int(x["id"]) for x in json.loads(response.content.decode("utf-8"))["results"]] + self.assertEqual(len(returned_ids), 1) + self.assertEqual(returned_ids[0], workspace.pk) class GroupGroupMembershipDetailTest(TestCase): diff --git a/anvil_consortium_manager/urls.py b/anvil_consortium_manager/urls.py index 999d5a5b..37976c4d 100644 --- a/anvil_consortium_manager/urls.py +++ b/anvil_consortium_manager/urls.py @@ -13,7 +13,6 @@ views.BillingProjectAutocomplete.as_view(), name="autocomplete", ), - path("audit/", views.BillingProjectAudit.as_view(), name="audit"), path("/", views.BillingProjectDetail.as_view(), name="detail"), path("/update/", views.BillingProjectUpdate.as_view(), name="update"), ], @@ -56,7 +55,6 @@ views.GroupAccountMembershipCreateByAccount.as_view(), name="add_to_group", ), - path("audit/", views.AccountAudit.as_view(), name="audit"), ], "accounts", ) @@ -132,18 +130,12 @@ views.ManagedGroupAutocomplete.as_view(), name="autocomplete", ), - path("audit/", views.ManagedGroupAudit.as_view(), name="audit"), path( "visualization/", views.ManagedGroupVisualization.as_view(), name="visualization", ), path("/", views.ManagedGroupDetail.as_view(), name="detail"), - path( - "/audit/", - views.ManagedGroupMembershipAudit.as_view(), - name="audit_membership", - ), path("/delete", views.ManagedGroupDelete.as_view(), name="delete"), path("/member_groups/", include(member_group_patterns)), path("/member_accounts/", include(member_account_patterns)), @@ -223,7 +215,6 @@ # views.WorkspaceClone.as_view(), # name="clone", # ), - path("audit/", views.WorkspaceAudit.as_view(), name="audit"), path( "//delete/", views.WorkspaceDelete.as_view(), @@ -234,11 +225,6 @@ views.WorkspaceDetail.as_view(), name="detail", ), - path( - "//audit/", - views.WorkspaceSharingAudit.as_view(), - name="audit_sharing", - ), path( "//sharing/", include(workspace_sharing_patterns), @@ -303,4 +289,5 @@ path("group_group_membership/", include(group_group_membership_patterns)), path("group_account_membership/", include(group_account_membership_patterns)), path("workspace_group_sharing/", include(workspace_group_sharing_patterns)), + path("audit/", include("anvil_consortium_manager.auditor.urls")), ] diff --git a/anvil_consortium_manager/viewmixins.py b/anvil_consortium_manager/viewmixins.py index 2b1b33ce..17f3e304 100644 --- a/anvil_consortium_manager/viewmixins.py +++ b/anvil_consortium_manager/viewmixins.py @@ -4,9 +4,7 @@ import numpy as np import plotly import plotly.graph_objects as go -from django.core.exceptions import ImproperlyConfigured from django.http import Http404 -from django.utils import timezone from django.utils.translation import gettext_lazy as _ from django.views.generic.base import ContextMixin from django.views.generic.detail import SingleObjectMixin @@ -15,40 +13,6 @@ from .adapters.account import get_account_adapter from .adapters.managed_group import get_managed_group_adapter from .adapters.workspace import workspace_adapter_registry -from .audit import audit - - -class AnVILAuditMixin: - """Mixin to display AnVIL audit results.""" - - audit_class = None - - def get_audit_instance(self): - if not self.audit_class: - raise ImproperlyConfigured( - "%(cls)s is missing an audit class. Define %(cls)s.audit_class or override " - "%(cls)s.get_audit_instance()." % {"cls": self.__class__.__name__} - ) - else: - return self.audit_class() - - def run_audit(self): - self.audit_results = self.get_audit_instance() - self.audit_results.run_audit() - - def get(self, request, *args, **kwargs): - self.run_audit() - return super().get(request, *args, **kwargs) - - def get_context_data(self, *args, **kwargs): - """Add audit results to the context data.""" - context = super().get_context_data(*args, **kwargs) - context["audit_timestamp"] = timezone.now() - context["audit_ok"] = self.audit_results.ok() - context["verified_table"] = audit.VerifiedTable(self.audit_results.get_verified_results()) - context["error_table"] = audit.ErrorTable(self.audit_results.get_error_results()) - context["not_in_app_table"] = audit.NotInAppTable(self.audit_results.get_not_in_app_results()) - return context class AccountAdapterMixin: diff --git a/anvil_consortium_manager/views.py b/anvil_consortium_manager/views.py index 1b785b59..61240fac 100644 --- a/anvil_consortium_manager/views.py +++ b/anvil_consortium_manager/views.py @@ -19,7 +19,6 @@ from .adapters.account import get_account_adapter from .adapters.workspace import workspace_adapter_registry from .anvil_api import AnVILAPIClient, AnVILAPIError -from .audit import audit from .tokens import account_verification_token @@ -140,13 +139,6 @@ def get_queryset(self): return qs -class BillingProjectAudit(auth.AnVILConsortiumManagerStaffViewRequired, viewmixins.AnVILAuditMixin, TemplateView): - """View to run an audit on Workspaces and display the results.""" - - template_name = "anvil_consortium_manager/billing_project_audit.html" - audit_class = audit.BillingProjectAudit - - class AccountDetail( auth.AnVILConsortiumManagerStaffViewRequired, viewmixins.SingleAccountMixin, @@ -619,13 +611,6 @@ def get_queryset(self): return qs -class AccountAudit(auth.AnVILConsortiumManagerStaffViewRequired, viewmixins.AnVILAuditMixin, TemplateView): - """View to run an audit on Accounts and display the results.""" - - template_name = "anvil_consortium_manager/account_audit.html" - audit_class = audit.AccountAudit - - class AccountUnlinkUser( auth.AnVILConsortiumManagerStaffEditRequired, viewmixins.SingleAccountMixin, SuccessMessageMixin, FormView ): @@ -894,44 +879,6 @@ def get_queryset(self): return qs -class ManagedGroupAudit(auth.AnVILConsortiumManagerStaffViewRequired, viewmixins.AnVILAuditMixin, TemplateView): - """View to run an audit on ManagedGroups and display the results.""" - - template_name = "anvil_consortium_manager/managedgroup_audit.html" - audit_class = audit.ManagedGroupAudit - - -class ManagedGroupMembershipAudit( - auth.AnVILConsortiumManagerStaffViewRequired, - SingleObjectMixin, - viewmixins.AnVILAuditMixin, - TemplateView, -): - """View to run an audit on ManagedGroups and display the results.""" - - model = models.ManagedGroup - slug_field = "name" - template_name = "anvil_consortium_manager/managedgroup_membership_audit.html" - message_not_managed_by_app = "Cannot audit membership because group is not managed by this app." - - def get(self, request, *args, **kwargs): - self.object = self.get_object() - # Check if managed by the app. - if not self.object.is_managed_by_app: - messages.add_message( - self.request, - messages.ERROR, - self.message_not_managed_by_app, - ) - # Redirect to the object detail page. - return HttpResponseRedirect(self.object.get_absolute_url()) - # Otherwise, return the response. - return super().get(request, *args, **kwargs) - - def get_audit_instance(self): - return audit.ManagedGroupMembershipAudit(self.object) - - class WorkspaceLandingPage( auth.AnVILConsortiumManagerViewRequired, viewmixins.RegisteredWorkspaceAdaptersMixin, @@ -1656,53 +1603,6 @@ def form_valid(self, form): return response -class WorkspaceAudit(auth.AnVILConsortiumManagerStaffViewRequired, viewmixins.AnVILAuditMixin, TemplateView): - """View to run an audit on Workspaces and display the results.""" - - template_name = "anvil_consortium_manager/workspace_audit.html" - audit_class = audit.WorkspaceAudit - - -class WorkspaceSharingAudit( - auth.AnVILConsortiumManagerStaffViewRequired, - SingleObjectMixin, - viewmixins.AnVILAuditMixin, - TemplateView, -): - """View to run an audit on access to a specific Workspace and display the results.""" - - model = models.Workspace - template_name = "anvil_consortium_manager/workspace_sharing_audit.html" - - def get_object(self, queryset=None): - """Return the object the view is displaying.""" - - # Use a custom queryset if provided; this is required for subclasses - # like DateDetailView - if queryset is None: - queryset = self.get_queryset() - # Filter the queryset based on kwargs. - billing_project_slug = self.kwargs.get("billing_project_slug", None) - workspace_slug = self.kwargs.get("workspace_slug", None) - queryset = queryset.filter(billing_project__name=billing_project_slug, name=workspace_slug) - try: - # Get the single item from the filtered queryset - obj = queryset.get() - except queryset.model.DoesNotExist: - raise Http404( - _("No %(verbose_name)s found matching the query") % {"verbose_name": queryset.model._meta.verbose_name} - ) - return obj - - def get(self, request, *args, **kwargs): - self.object = self.get_object() - # Otherwise, return the response. - return super().get(request, *args, **kwargs) - - def get_audit_instance(self): - return audit.WorkspaceSharingAudit(self.object) - - class WorkspaceAutocomplete(auth.AnVILConsortiumManagerStaffViewRequired, autocomplete.Select2QuerySetView): """View to provide autocompletion for Workspaces. diff --git a/docs/Makefile b/docs/Makefile index b45e4001..65487b75 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -21,7 +21,7 @@ livehtml: # Outputs rst files from django application code apidocs: - sphinx-apidoc -T -e -o $(SOURCEDIR)/api $(APP) $(APP)/tests $(APP)/migrations + sphinx-apidoc -T -e -o $(SOURCEDIR)/api $(APP) $(APP)/tests $(APP)/migrations $(APP)/auditor/tests # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). diff --git a/docs/api/anvil_consortium_manager.adapters.managed_group.rst b/docs/api/anvil_consortium_manager.adapters.managed_group.rst new file mode 100644 index 00000000..16085d4a --- /dev/null +++ b/docs/api/anvil_consortium_manager.adapters.managed_group.rst @@ -0,0 +1,7 @@ +anvil\_consortium\_manager.adapters.managed\_group module +========================================================= + +.. automodule:: anvil_consortium_manager.adapters.managed_group + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/anvil_consortium_manager.adapters.rst b/docs/api/anvil_consortium_manager.adapters.rst index c1f2b6ef..db92b601 100644 --- a/docs/api/anvil_consortium_manager.adapters.rst +++ b/docs/api/anvil_consortium_manager.adapters.rst @@ -9,6 +9,7 @@ Submodules anvil_consortium_manager.adapters.account anvil_consortium_manager.adapters.default + anvil_consortium_manager.adapters.managed_group anvil_consortium_manager.adapters.workspace Module contents diff --git a/docs/api/anvil_consortium_manager.app_settings.rst b/docs/api/anvil_consortium_manager.app_settings.rst new file mode 100644 index 00000000..eeb9177e --- /dev/null +++ b/docs/api/anvil_consortium_manager.app_settings.rst @@ -0,0 +1,7 @@ +anvil\_consortium\_manager.app\_settings module +=============================================== + +.. automodule:: anvil_consortium_manager.app_settings + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/anvil_consortium_manager.audit.audit.rst b/docs/api/anvil_consortium_manager.audit.audit.rst deleted file mode 100644 index 47ece9d3..00000000 --- a/docs/api/anvil_consortium_manager.audit.audit.rst +++ /dev/null @@ -1,7 +0,0 @@ -anvil\_consortium\_manager.audit.audit module -============================================= - -.. automodule:: anvil_consortium_manager.audit.audit - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/anvil_consortium_manager.audit.rst b/docs/api/anvil_consortium_manager.audit.rst deleted file mode 100644 index 68eab8fd..00000000 --- a/docs/api/anvil_consortium_manager.audit.rst +++ /dev/null @@ -1,18 +0,0 @@ -anvil\_consortium\_manager.audit package -======================================== - -Submodules ----------- - -.. toctree:: - :maxdepth: 4 - - anvil_consortium_manager.audit.audit - -Module contents ---------------- - -.. automodule:: anvil_consortium_manager.audit - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/anvil_consortium_manager.auditor.admin.rst b/docs/api/anvil_consortium_manager.auditor.admin.rst new file mode 100644 index 00000000..6b9072d4 --- /dev/null +++ b/docs/api/anvil_consortium_manager.auditor.admin.rst @@ -0,0 +1,7 @@ +anvil\_consortium\_manager.auditor.admin module +=============================================== + +.. automodule:: anvil_consortium_manager.auditor.admin + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/anvil_consortium_manager.auditor.apps.rst b/docs/api/anvil_consortium_manager.auditor.apps.rst new file mode 100644 index 00000000..2efa95a6 --- /dev/null +++ b/docs/api/anvil_consortium_manager.auditor.apps.rst @@ -0,0 +1,7 @@ +anvil\_consortium\_manager.auditor.apps module +============================================== + +.. automodule:: anvil_consortium_manager.auditor.apps + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/anvil_consortium_manager.auditor.audit.accounts.rst b/docs/api/anvil_consortium_manager.auditor.audit.accounts.rst new file mode 100644 index 00000000..ae4a8580 --- /dev/null +++ b/docs/api/anvil_consortium_manager.auditor.audit.accounts.rst @@ -0,0 +1,7 @@ +anvil\_consortium\_manager.auditor.audit.accounts module +======================================================== + +.. automodule:: anvil_consortium_manager.auditor.audit.accounts + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/anvil_consortium_manager.auditor.audit.base.rst b/docs/api/anvil_consortium_manager.auditor.audit.base.rst new file mode 100644 index 00000000..e55f0904 --- /dev/null +++ b/docs/api/anvil_consortium_manager.auditor.audit.base.rst @@ -0,0 +1,7 @@ +anvil\_consortium\_manager.auditor.audit.base module +==================================================== + +.. automodule:: anvil_consortium_manager.auditor.audit.base + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/anvil_consortium_manager.auditor.audit.billing_projects.rst b/docs/api/anvil_consortium_manager.auditor.audit.billing_projects.rst new file mode 100644 index 00000000..5e39d7d6 --- /dev/null +++ b/docs/api/anvil_consortium_manager.auditor.audit.billing_projects.rst @@ -0,0 +1,7 @@ +anvil\_consortium\_manager.auditor.audit.billing\_projects module +================================================================= + +.. automodule:: anvil_consortium_manager.auditor.audit.billing_projects + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/anvil_consortium_manager.auditor.audit.managed_groups.rst b/docs/api/anvil_consortium_manager.auditor.audit.managed_groups.rst new file mode 100644 index 00000000..cba6a64a --- /dev/null +++ b/docs/api/anvil_consortium_manager.auditor.audit.managed_groups.rst @@ -0,0 +1,7 @@ +anvil\_consortium\_manager.auditor.audit.managed\_groups module +=============================================================== + +.. automodule:: anvil_consortium_manager.auditor.audit.managed_groups + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/anvil_consortium_manager.auditor.audit.rst b/docs/api/anvil_consortium_manager.auditor.audit.rst new file mode 100644 index 00000000..8f6d5d83 --- /dev/null +++ b/docs/api/anvil_consortium_manager.auditor.audit.rst @@ -0,0 +1,22 @@ +anvil\_consortium\_manager.auditor.audit package +================================================ + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + anvil_consortium_manager.auditor.audit.accounts + anvil_consortium_manager.auditor.audit.base + anvil_consortium_manager.auditor.audit.billing_projects + anvil_consortium_manager.auditor.audit.managed_groups + anvil_consortium_manager.auditor.audit.workspaces + +Module contents +--------------- + +.. automodule:: anvil_consortium_manager.auditor.audit + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/anvil_consortium_manager.auditor.audit.workspaces.rst b/docs/api/anvil_consortium_manager.auditor.audit.workspaces.rst new file mode 100644 index 00000000..bdd6099b --- /dev/null +++ b/docs/api/anvil_consortium_manager.auditor.audit.workspaces.rst @@ -0,0 +1,7 @@ +anvil\_consortium\_manager.auditor.audit.workspaces module +========================================================== + +.. automodule:: anvil_consortium_manager.auditor.audit.workspaces + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/anvil_consortium_manager.auditor.forms.rst b/docs/api/anvil_consortium_manager.auditor.forms.rst new file mode 100644 index 00000000..3cadca4b --- /dev/null +++ b/docs/api/anvil_consortium_manager.auditor.forms.rst @@ -0,0 +1,7 @@ +anvil\_consortium\_manager.auditor.forms module +=============================================== + +.. automodule:: anvil_consortium_manager.auditor.forms + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/anvil_consortium_manager.auditor.migrations.0001_initial.rst b/docs/api/anvil_consortium_manager.auditor.migrations.0001_initial.rst new file mode 100644 index 00000000..b27a73f6 --- /dev/null +++ b/docs/api/anvil_consortium_manager.auditor.migrations.0001_initial.rst @@ -0,0 +1,7 @@ +anvil\_consortium\_manager.auditor.migrations.0001\_initial module +================================================================== + +.. automodule:: anvil_consortium_manager.auditor.migrations.0001_initial + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/anvil_consortium_manager.auditor.migrations.rst b/docs/api/anvil_consortium_manager.auditor.migrations.rst new file mode 100644 index 00000000..7787b769 --- /dev/null +++ b/docs/api/anvil_consortium_manager.auditor.migrations.rst @@ -0,0 +1,18 @@ +anvil\_consortium\_manager.auditor.migrations package +===================================================== + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + anvil_consortium_manager.auditor.migrations.0001_initial + +Module contents +--------------- + +.. automodule:: anvil_consortium_manager.auditor.migrations + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/anvil_consortium_manager.auditor.models.rst b/docs/api/anvil_consortium_manager.auditor.models.rst new file mode 100644 index 00000000..69fcd9da --- /dev/null +++ b/docs/api/anvil_consortium_manager.auditor.models.rst @@ -0,0 +1,7 @@ +anvil\_consortium\_manager.auditor.models module +================================================ + +.. automodule:: anvil_consortium_manager.auditor.models + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/anvil_consortium_manager.auditor.rst b/docs/api/anvil_consortium_manager.auditor.rst new file mode 100644 index 00000000..9e4874cc --- /dev/null +++ b/docs/api/anvil_consortium_manager.auditor.rst @@ -0,0 +1,34 @@ +anvil\_consortium\_manager.auditor package +========================================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + anvil_consortium_manager.auditor.audit + anvil_consortium_manager.auditor.migrations + +Submodules +---------- + +.. toctree:: + :maxdepth: 4 + + anvil_consortium_manager.auditor.admin + anvil_consortium_manager.auditor.apps + anvil_consortium_manager.auditor.forms + anvil_consortium_manager.auditor.models + anvil_consortium_manager.auditor.tables + anvil_consortium_manager.auditor.urls + anvil_consortium_manager.auditor.viewmixins + anvil_consortium_manager.auditor.views + +Module contents +--------------- + +.. automodule:: anvil_consortium_manager.auditor + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/anvil_consortium_manager.auditor.tables.rst b/docs/api/anvil_consortium_manager.auditor.tables.rst new file mode 100644 index 00000000..d94d0334 --- /dev/null +++ b/docs/api/anvil_consortium_manager.auditor.tables.rst @@ -0,0 +1,7 @@ +anvil\_consortium\_manager.auditor.tables module +================================================ + +.. automodule:: anvil_consortium_manager.auditor.tables + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/anvil_consortium_manager.auditor.urls.rst b/docs/api/anvil_consortium_manager.auditor.urls.rst new file mode 100644 index 00000000..03f80437 --- /dev/null +++ b/docs/api/anvil_consortium_manager.auditor.urls.rst @@ -0,0 +1,7 @@ +anvil\_consortium\_manager.auditor.urls module +============================================== + +.. automodule:: anvil_consortium_manager.auditor.urls + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/anvil_consortium_manager.auditor.viewmixins.rst b/docs/api/anvil_consortium_manager.auditor.viewmixins.rst new file mode 100644 index 00000000..b0122c7a --- /dev/null +++ b/docs/api/anvil_consortium_manager.auditor.viewmixins.rst @@ -0,0 +1,7 @@ +anvil\_consortium\_manager.auditor.viewmixins module +==================================================== + +.. automodule:: anvil_consortium_manager.auditor.viewmixins + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/anvil_consortium_manager.auditor.views.rst b/docs/api/anvil_consortium_manager.auditor.views.rst new file mode 100644 index 00000000..c6ee3fe1 --- /dev/null +++ b/docs/api/anvil_consortium_manager.auditor.views.rst @@ -0,0 +1,7 @@ +anvil\_consortium\_manager.auditor.views module +=============================================== + +.. automodule:: anvil_consortium_manager.auditor.views + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/anvil_consortium_manager.management.commands.convert_mariadb_uuid_fields.rst b/docs/api/anvil_consortium_manager.management.commands.convert_mariadb_uuid_fields.rst new file mode 100644 index 00000000..d524595b --- /dev/null +++ b/docs/api/anvil_consortium_manager.management.commands.convert_mariadb_uuid_fields.rst @@ -0,0 +1,7 @@ +anvil\_consortium\_manager.management.commands.convert\_mariadb\_uuid\_fields module +==================================================================================== + +.. automodule:: anvil_consortium_manager.management.commands.convert_mariadb_uuid_fields + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/api/anvil_consortium_manager.management.commands.rst b/docs/api/anvil_consortium_manager.management.commands.rst index 9dc1e85c..f6a42daa 100644 --- a/docs/api/anvil_consortium_manager.management.commands.rst +++ b/docs/api/anvil_consortium_manager.management.commands.rst @@ -7,7 +7,7 @@ Submodules .. toctree:: :maxdepth: 4 - anvil_consortium_manager.management.commands.run_anvil_audit + anvil_consortium_manager.management.commands.convert_mariadb_uuid_fields Module contents --------------- diff --git a/docs/api/anvil_consortium_manager.management.commands.run_anvil_audit.rst b/docs/api/anvil_consortium_manager.management.commands.run_anvil_audit.rst deleted file mode 100644 index 7fb9ce47..00000000 --- a/docs/api/anvil_consortium_manager.management.commands.run_anvil_audit.rst +++ /dev/null @@ -1,7 +0,0 @@ -anvil\_consortium\_manager.management.commands.run\_anvil\_audit module -======================================================================= - -.. automodule:: anvil_consortium_manager.management.commands.run_anvil_audit - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/api/anvil_consortium_manager.rst b/docs/api/anvil_consortium_manager.rst index 139e6671..f9238712 100644 --- a/docs/api/anvil_consortium_manager.rst +++ b/docs/api/anvil_consortium_manager.rst @@ -8,7 +8,7 @@ Subpackages :maxdepth: 4 anvil_consortium_manager.adapters - anvil_consortium_manager.audit + anvil_consortium_manager.auditor anvil_consortium_manager.management Submodules @@ -19,6 +19,7 @@ Submodules anvil_consortium_manager.admin anvil_consortium_manager.anvil_api + anvil_consortium_manager.app_settings anvil_consortium_manager.apps anvil_consortium_manager.auth anvil_consortium_manager.exceptions diff --git a/docs/auditing.rst b/docs/auditing.rst index 9f4bf390..c68ca3f8 100644 --- a/docs/auditing.rst +++ b/docs/auditing.rst @@ -3,19 +3,19 @@ Auditing information in the app =============================== -Periodically, you should verify that the information in the app matches what's actually on AnVIL. -The app provides a number of model methods, typically named `anvil_audit`, to help with this auditing, as well as objects that store the results of an audit. +The :mod:`~anvil_consortium_manager.auditor` app provides functionality to audit information stored in the app against what is actually on AnVIL. Audit results classes --------------------- -Results from an audit are returned as an object that is a subclass of :class:`~anvil_consortium_manager.anvil_audit.AnVILAuditResults`. -The subclasses have a method :meth:`~anvil_consortium_manager.anvil_audit.AnVILAuditResults.ok` that indicates if the audit was successful or if any errors were detected. -It also can list the set of model instances in the app that were audited against AnVIL using :meth:`~anvil_consortium_manager.anvil_audit.AnVILAuditResults.get_verified`; -a dictionary of model instances with detected errors and the errors themselves using :meth:`~anvil_consortium_manager.anvil_audit.AnVILAuditResults.get_errors`; -and the set of records that exist on AnVIL but are not in the app using :meth:`~anvil_consortium_manager.anvil_audit.AnVILAuditResults.get_not_in_app`. +Results from an audit are returned as an object that is a subclass of :class:`~anvil_consortium_manager.auditor.audit.base.AnVILAudit`. +The subclasses have a method :meth:`~anvil_consortium_manager.auditor.audit.base.AnVILAudit.ok` that indicates if the audit was successful or if any errors were detected. +It also can list the set of model instances in the app that were audited against AnVIL using :meth:`~anvil_consortium_manager.auditor.audit.base.AnVILAudit.get_verified_results`; +a dictionary of model instances with detected errors and the errors themselves using :meth:`~anvil_consortium_manager.auditor.audit.base.AnVILAudit.get_error_results`; +and the set of records that exist on AnVIL but are not in the app using :meth:`~anvil_consortium_manager.auditor.audit.base.AnVILAudit.get_not_in_app_results`; +and any "not in app" records that have been marked as ignored :meth:`~anvil_consortium_manager.auditor.audit.base.AnVILAudit.get_ignored_results`. -Different models check different things and have different potential errors. +Audits for different models check different things and will report different potential errors. Model-specific auditing @@ -24,8 +24,7 @@ Model-specific auditing Billing project auditing ~~~~~~~~~~~~~~~~~~~~~~~~ -The :class:`~anvil_consortium_manager.models.BillingProject` model provides a class method :meth:`~anvil_consortium_manager.models.BillingProject.anvil_audit` that runs on all :class:`~anvil_consortium_manager.models.BillingProject` model instances in the app. -This method runs the following checks: +The :class:`~anvil_consortium_manager.auditor.audit.billing_projects.BillingProjectAudit` class can be used to audit all :class:`~anvil_consortium_manager.models.BillingProject` model instances in the app. It runs the following checks: 1. All :class:`~anvil_consortium_manager.models.BillingProject` model instances in the app also exist on AnVIL. @@ -35,8 +34,7 @@ It does not check if there are Billing Projects on AnVIL that don't have a recor Account auditing ~~~~~~~~~~~~~~~~ -The :class:`~anvil_consortium_manager.models.Account` model provides a class method :meth:`~anvil_consortium_manager.models.Account.anvil_audit` that runs on all :class:`~anvil_consortium_manager.models.Account` model instances in the app. -This method runs the following checks: +The :class:`~anvil_consortium_manager.auditor.audit.accounts.AccountAudit` class can be used to audit all :class:`~anvil_consortium_manager.models.Account` model instances in the app. It runs the following checks: 1. All :class:`~anvil_consortium_manager.models.Account` model instances in the app also exist on AnVIL. @@ -45,16 +43,14 @@ It does not check if there are Accounts on AnVIL that don't have a record in the Managed Group auditing ~~~~~~~~~~~~~~~~~~~~~~ -The :class:`~anvil_consortium_manager.models.ManagedGroup` model provides two options for auditing: an instance method :meth:`~anvil_consortium_manager.models.ManagedGroup.anvil_audit` to check membership for a single :class:`~anvil_consortium_manager.models.ManagedGroup`, and a class method :meth:`~anvil_consortium_manager.models.ManagedGroup.anvil_audit` that runs on all :class:`~anvil_consortium_manager.models.ManagedGroup` model instances in the app. - -The :meth:`~anvil_consortium_manager.models.ManagedGroup.anvil_audit_membership` method runs the following checks: +The :class:`~anvil_consortium_manager.auditor.audit.managed_groups.ManagedGroupAudit` class can be used to audit all :class:`~anvil_consortium_manager.models.ManagedGroup` model instances in the app. It runs the following checks: 1. All :class:`~anvil_consortium_manager.models.ManagedGroup` model instances in the app also exist on AnVIL. 2. The service account running the app has the same role (admin vs member) in the app as on AnVIL. - 3. The membership of each group in the app matches the membership on AnVIL (using :meth:`~anvil_consortium_manager.models.ManagedGroup.anvil_audit_membership` method for each ManagedGroup). + 3. The membership of each group in the app matches the membership on AnVIL (by running an :class:`~anvil_consortium_manager.auditor.audit.managed_groups.ManagedGroupMembershipAudit` audit for each ManagedGroup). 4. No groups that have the app service account as an Admin exist on AnVIL. -The :meth:`~anvil_consortium_manager.models.ManagedGroup.anvil_audit_membership` method runs the following checks for a single :class:`~anvil_consortium_manager.models.ManagedGroup` instance: +Membership auditing for a single group can be done using the :class:`~anvil_consortium_manager.auditor.audit.managed_groups.ManagedGroupMembershipAudit` class. This class performs the following checks: 1. All account members of this :class:`~anvil_consortium_manager.models.ManagedGroup` in the app are also members in AnVIL. 2. All account admin of this :class:`~anvil_consortium_manager.models.ManagedGroup` in the app are also admin in AnVIL. @@ -64,30 +60,35 @@ The :meth:`~anvil_consortium_manager.models.ManagedGroup.anvil_audit_membership` 6. All members in AnVIL are also recorded in the app. +If desired, specific membership records can be ignored by creating an :class:`~anvil_consortium_manager.auditor.models.IgnoredManagedGroupMembership` instance in the app. +Ignored records will be included in the audit results, but will not be considered errors. + + Workspace auditing ~~~~~~~~~~~~~~~~~~ -As for ManagedGroups, the :class:`~anvil_consortium_manager.models.Workspace` model provides two options for auditing: an instance method :meth:`~anvil_consortium_manager.models.Workspace.anvil_audit` to check access for a single :class:`~anvil_consortium_manager.models.Workspace`, and a class method :meth:`~anvil_consortium_manager.models.Workspace.anvil_audit` that runs on all :class:`~anvil_consortium_manager.models.Workspace` model instances in the app. -The :meth:`~anvil_consortium_manager.models.Workspace.anvil_audit` method runs the following checks: + +The :class:`~anvil_consortium_manager.auditor.audit.workspaces.WorkspaceAudit` class can be used to audit all :class:`~anvil_consortium_manager.models.Workspace` model instances in the app. It runs the following checks: 1. All :class:`~anvil_consortium_manager.models.Workspace` model instances in the app also exist on AnVIL. 2. The service account running the app is an owner on AnVIL of all the :class:`~anvil_consortium_manager.models.Workspace` model instances. 3. The :class:`~anvil_consortium_manager.models.Workspace` has the same authorization domains in the app as on AnVIL. - 4. The access to each :class:`~anvil_consortium_manager.models.Workspace` in the app matches the access on AnVIL (using :meth:`~anvil_consortium_manager.models.Workspace.anvil_audit_access` method for each Workspace). + 4. The access to each :class:`~anvil_consortium_manager.models.Workspace` in the app matches the access on AnVIL (by running an :class:`~anvil_consortium_manager.auditor.audit.workspaces.WorkspaceSharingAudit` audit for each Workspace). 5. No workspaces that have the app service account as an owner exist on AnVIL. 6. The workspace ``is_locked`` status matches AnVIL. 7. The workspace ``is_requester_pays`` status matches AnVIL. -The :meth:`~anvil_consortium_manager.models.Workspace.anvil_audit_membership` method runs the following checks for a single :class:`~anvil_consortium_manager.models.Workspace` instance: +Sharing for a workspace can be audited using the :class:`~anvil_consortium_manager.auditor.audit.workspaces.WorkspaceSharingAudit` class. This class performs the following checks: 1. All groups that have access in the app also have access in AnVIL. 2. Each :class:`~anvil_consortium_manager.models.ManagedGroup` that has access in the app has the same access in AnVIL. 3. The :attr:`~anvil_consortium_manager.models.WorkspaceGroupSharing.can_compute` value is the same in the app and on AnVIL. - 4. The :attr:`~anvil_consortium_manager.models.WorkspaceGroupSharing.can_share` value is the same in the app and on AnVIL. + 4. The ``can_share`` value is as expected on AnVIL based on the group's ``role``. 5. No groups or accounts on AnVIL have access to the workspace that are not recorded in the app. + Running audits -------------- @@ -96,15 +97,15 @@ Auditing views The app provides a number of views for auditing various models. - - :class:`~anvil_consortium_manager.models.BillingProject`: :class:`~anvil_consortium_manager.views.BillingProjectAudit` (accessible from default navbar) - - :class:`~anvil_consortium_manager.models.Account`: :class:`~anvil_consortium_manager.views.AccountAudit` (accessible from default navbar) - - :class:`~anvil_consortium_manager.models.ManagedGroup`: :class:`~anvil_consortium_manager.views.ManagedGroupAudit` (accessible from default navbar) - - :class:`~anvil_consortium_manager.models.Workspace`: :class:`~anvil_consortium_manager.views.WorkspaceAudit` (accessible from default navbar) + - :class:`~anvil_consortium_manager.models.BillingProject`: :class:`~anvil_consortium_manager.auditor.views.BillingProjectAudit` (accessible from default navbar) + - :class:`~anvil_consortium_manager.models.Account`: :class:`~anvil_consortium_manager.views.auditor.accounts.AccountAudit` (accessible from default navbar) + - :class:`~anvil_consortium_manager.models.ManagedGroup`: :class:`~anvil_consortium_manager.views.auditor.managed_groups.ManagedGroupAudit` (accessible from default navbar) + - :class:`~anvil_consortium_manager.models.Workspace`: :class:`~anvil_consortium_manager.views.auditor.workspaces.WorkspaceAudit` (accessible from default navbar) Workspaces and ManagedGroups have additional audit views that can audit the sharing and membership, respectively. -- :class:`~anvil_consortium_manager.models.ManagedGroup` membership: :class:`~anvil_consortium_manager.views.ManagedGroupMembershipAudit` (accessible from Managed Group detail page) -- :class:`~anvil_consortium_manager.models.Workspace` sharing: :class:`~anvil_consortium_manager.views.WorkspaceSharingAudit` (accessible from the Workspace detail page) +- :class:`~anvil_consortium_manager.models.ManagedGroup` membership: :class:`~anvil_consortium_manager.auditor.views.ManagedGroupMembershipAudit` (accessible from Managed Group detail page) +- :class:`~anvil_consortium_manager.models.Workspace` sharing: :class:`~anvil_consortium_manager.auditor.views.WorkspaceSharingAudit` (accessible from the Workspace detail page) Auditing via management command @@ -127,3 +128,8 @@ Here are some examples of calling this command: python manage.py run_anvil_audit --models BillingProject Account More information can be found in the help for ``run_anvil_audit``. + +.. code-block:: bash + + # To audit all models and print a report to the terminal. + python manage.py run_anvil_audit --help diff --git a/docs/index.rst b/docs/index.rst index c667aa9a..76f0d007 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -20,7 +20,7 @@ Welcome to django-anvil-consortium-manager's documentation! management_commands .. toctree:: - :maxdepth: 6 + :maxdepth: 3 :caption: Reference api/anvil_consortium_manager diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 05b7a337..95625da6 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -34,6 +34,8 @@ Required Settings # This app: "anvil_consortium_manager", + # The associated app for auditing information against AnVIL (required): + "anvil_consortium_manager.auditor", ] 2. Set the ``ANVIL_API_SERVICE_ACCOUNT_FILE`` setting to the path to the service account credentials file. diff --git a/example_site/settings.py b/example_site/settings.py index b2552e62..432e32bc 100644 --- a/example_site/settings.py +++ b/example_site/settings.py @@ -92,6 +92,7 @@ "django_filters", # This app. "anvil_consortium_manager", + "anvil_consortium_manager.auditor", # Autocomplete. # note these are supposed to come before django.contrib.admin. "dal", diff --git a/pyproject.toml b/pyproject.toml index 695ecd53..0bf13423 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -90,7 +90,7 @@ exclude_lines = ["@abstractproperty"] [tool.pytest.ini_options] addopts = "--ignore=anvil_consortium_manager/tests/test_app --ds=anvil_consortium_manager.tests.settings.test" -python_files = ["anvil_consortium_manager/tests/test*.py"] +python_files = ["anvil_consortium_manager/tests/test*.py", "anvil_consortium_manager/auditor/tests/test*.py"] python_classes = ["!TestWorkspaceDataFactory"] # HATCH